java.io.FileNotFoundException打开失败:EEXIST(文件已存在) Android 11

10
我试图从服务器下载一张图片并保存到外部存储器,但在Android 11中,当我尝试创建文件时会出现错误。我已经授权访问外部存储器。
我在网上搜索了一下,他们建议我将此代码放在清单中,但对于Android 11来说它不起作用。
android:requestLegacyExternalStorage="true"

清单文件
<uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:requestLegacyExternalStorage="true"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.TestDwonloadImgApp"
        android:usesCleartextTraffic="true">
        <activity android:name=".MainActivity2">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".MainActivity">
        </activity>
    </application>

主活动

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ImageView img = findViewById(R.id.img);

        ImmagineInterface ii = RetrofitManager.retrofit.create(ImmagineInterface.class);
        Call<ResponseBody> call = ii.downloadFile("/immaginimusei/arte-scienza.jpg");
        call.enqueue(new Callback<ResponseBody>() {
            @Override
            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                if (response.code() == 200) {
                    boolean result = writeResponseBody(response.body(), "/immaginimusei/arte-scienza.jpg");
                    if(result) {
                        Bitmap bitmap = BitmapFactory.decodeFile(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString() + "/ArtHunter/immaginimusei/arte-scienza.jpg");
                        img.setImageBitmap(bitmap);
                    }
                }
            }

            @Override
            public void onFailure(Call<ResponseBody> call, Throwable t) {
                Bitmap bitmap = BitmapFactory.decodeFile(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString() + "/ArtHunter/immaginimusei/arte-scienza.jpg");
                img.setImageBitmap(bitmap);
            }
        });
    }
}

编写响应体

public static boolean writeResponseBody(ResponseBody body, String dir1) {
        try {
            String state = Environment.getExternalStorageState();
            if (Environment.MEDIA_MOUNTED.equals(state)) {
                // todo change the file location/name according to your needs

                String path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString() + "/ArtHunter";

                String path1 = path + dir1;
                File f = new File(path1);
                String path2 = f.getPath();
                String nome = f.getName();
                path2 = path2.replaceAll("/" + nome, "");

                File directory = new File(path2);
                if (!directory.exists())
                    directory.mkdirs();

                File img = new File(path2, nome);
                if (img.exists())
                    return true;
                img.createNewFile();

                InputStream inputStream = null;
                FileOutputStream outputStream = null;

                try {
                    byte[] fileReader = new byte[4096];

                    inputStream = body.byteStream();
                    outputStream = new FileOutputStream(img); //error here!

                    while (true) {
                        int read = inputStream.read(fileReader);

                        if (read == -1) {
                            break;
                        }

                        outputStream.write(fileReader, 0, read);

                    }

                    outputStream.flush();

                    return true;
                } catch (IOException e) {
                    e.printStackTrace();
                    return false;
                } finally {
                    if (inputStream != null) {
                        inputStream.close();
                    }

                    if (outputStream != null) {
                        outputStream.close();
                    }
                }
            }
            return false;
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
    }

错误

/System.err: java.io.FileNotFoundException: /storage/emulated/0/Download/ArtHunter/immaginimusei/arte-scienza.jpg: open failed: EEXIST (File exists)
W/System.err:     at libcore.io.IoBridge.open(IoBridge.java:492)
        at java.io.FileOutputStream.<init>(FileOutputStream.java:236)
        at java.io.FileOutputStream.<init>(FileOutputStream.java:186)
        at com.theapplegeek.testdwonloadimgapp.MainActivity.writeResponseBody(MainActivity.java:93)
        at com.theapplegeek.testdwonloadimgapp.MainActivity$1.onResponse(MainActivity.java:47)
        at retrofit2.DefaultCallAdapterFactory$ExecutorCallbackCall$1.lambda$onResponse$0$DefaultCallAdapterFactory$ExecutorCallbackCall$1(DefaultCallAdapterFactory.java:89)
        at retrofit2.-$$Lambda$DefaultCallAdapterFactory$ExecutorCallbackCall$1$hVGjmafRi6VitDIrPNdoFizVAdk.run(Unknown Source:6)
        at android.os.Handler.handleCallback(Handler.java:938)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:245)
        at android.app.ActivityThread.main(ActivityThread.java:8004)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:631)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:978)
    Caused by: android.system.ErrnoException: open failed: EEXIST (File exists)
        at libcore.io.Linux.open(Native Method)
        at libcore.io.ForwardingOs.open(ForwardingOs.java:166)
        at libcore.io.BlockGuardOs.open(BlockGuardOs.java:254)
W/System.err:     at libcore.io.ForwardingOs.open(ForwardingOs.java:166)
        at android.app.ActivityThread$AndroidOs.open(ActivityThread.java:7865)
        at libcore.io.IoBridge.open(IoBridge.java:478)
        ... 13 more

1
path2 = path2.replaceAll("/" + nome, ""); 这里你在构建路径,但是没有人能够理解你的思路,我认为你自己也迷失了方向。请进一步检查mkdirs的返回值,如果为false则进行相应处理。移除.createNewFile调用。请重新编写你的代码。同样的问题也存在于这里。 - blackapps
你解决了吗?我不想使用android:requestLegacyExternalStorage。 - Shweta Chauhan
3个回答

13
在Android 11中,android:requestLegacyExternalStorage="true"将被简单地忽略掉,因为它只是Android < 11的旧应用程序不出问题的临时解决方案。现在,你必须使用:
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>

你也可以使用 SAF 来避免所有这些“权限”麻烦。这是 Google 推荐的适用于不需要管理大多数内部存储数据的应用程序的方法。请参考:https://developer.android.com/guide/topics/providers/document-provider

但是,如果你不想破坏你的应用程序并失去所有的辛勤工作,请考虑

if(Environment.isExternalStorageManager())
{
    internal = new File("/sdcard");
    internalContents = internal.listFiles();
}
else
{
    Intent permissionIntent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
    startActivity(permissionIntent);
}

这将打开一个设置页面,在那里您将能够为您的应用程序授予存储访问权限。如果应用程序已经获得了权限,则您将能够访问目录。请在onCreate()方法的最开始位置放置此内容,设置布局资源之后。

最好不要对您构建的任何未来应用程序执行此操作。


1
据我所知,/Download的子目录是共享存储,应该可以在没有MANAGE_EXTERNAL_STORAGE权限或SAF的情况下访问共享存储。我目前遇到了类似的问题,需要寻找解决方案来访问共享存储的子目录。 - Taifun
我已经有一段时间没有在Android上工作了,但是如果没有使用SAF访问下载存储,我们需要使用ROOM持久性框架,这将是一个巨大的麻烦,如果我只是偶尔访问。上一次我在Android上工作时,有一件非常奇怪的事情发生,如果你调用getFilesDir()来访问Downloads,它会在应用程序的内部存储目录中创建一个名为这样的文件夹。我可能会建议任何不从事文件管理器应用程序的人远离直接访问本地,因为随着每个迭代,Android都会搞砸它。 - mindoverflow
我有同样的问题:我想在一个文件夹中创建一个文件,我之前已经授予权限(通过ACTION_OPEN_DOCUMENT_TREE),但它的工作是不可预测的。 例如,文件名为“demoobstacle.kml”时可以正常工作,但文件名为“DemoObstacle.kml”时会抛出与问题中相同的异常。 通过使用ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION权限,它可以工作,但我不明白问题出在哪里。 (我在其他设备上尝试过(其他Android版本),并且任何文件名都可以完美地工作。) 有什么想法吗? - jack-wilson
我认为对于单个文件的创建,我们应该使用ACTION_OPEN_DOCUMENT。但是在某个时候,我曾经深度参与过Android开发,并建议您使用他们的Room Persistence API来完成所有操作。我不知道他们的最终目标是什么,但他们试图像iOS一样在安全性方面限制一切。目前,文件访问有两种方法。其中一种适用于10之前的版本,另一种适用于10及以后的版本。但总的来说,未来的迭代版本也将摆脱SAF以及任何手动非MIME类型访问。如果我们希望应用程序在每次Android发布后不会被破坏,最好使用他们推荐的方法。:( - mindoverflow
@mindoverflow 调用需要 API 级别 30(当前最低级别为 23):android.os.Environment#isExternalStorageManager 如何在较低的 Android 版本中实现此功能 - undefined
你好朋友,这超出了原始问题的范围,但是对于API <= 30的方法非常常见,而且在SO上很容易找到。然而,由于你不知道这一点,我认为你可能正在设计一个新的应用程序。如果是这样,请避免直接访问存储以供较低级别的API使用。对于这些情况,请改用SAF。谷歌已经完全改变了模型,并且几乎每次迭代都这样做,对于在Play商店上发布的应用程序来说,这真的是一个麻烦,因为谷歌不断要求进行大量的重构,否则就有被移除的风险... - undefined

3

前往您的手机设置 -> 应用程序 -> 选择您的应用程序 -> 权限 -> 存储 -> 选择允许管理所有文件

这对我起作用了。


1

在创建文件夹方面,Android 变得过于复杂。

如果您想避免太多问题,不使用像 android:requestLegacyExternalStorage="true" 这样的东西,并添加许多权限,如 MANAGE_EXTERNAL_STORAGEREAD_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE,这可能会是一个噩梦,即使是在将应用程序发布到市场时也一样。

此外,从 Android 30 版本开始,您不能再扩展 android:requestLegacyExternalStorage="true"。因此,您将再次遇到问题。

建议的做法是开始将录音保存在专用应用文件夹内,因为在未来,Android 将不再让您创建文件夹,即使使用旧有方法也是如此。您可以使用标准目录来放置任何文件,如 DIRECTORY_MUSICDIRECTORY_PICTURES 等。

例如,在 Android 33 中,我创建了自己的保存音频记录的方法,它运行得非常完美。我不需要在我的 Manifest 中添加任何内容。

override fun startRecording(): Boolean {
    try {
        val recordingStoragePath = "${app.getExternalFilesDir(Environment.DIRECTORY_MUSIC)}"
        recordingFilePath = "$recordingStoragePath/${fileRecordingUtils.generateFileName()}"
        mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC)
        mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
        // Audio Quality Normal
        mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
        mediaRecorder.setAudioSamplingRate(8000)
        // Set path
        mediaRecorder.setOutputFile(recordingFilePath)
        mediaRecorder.prepare()
        mediaRecorder.start()
        Toast.makeText(app, "Recording Started", Toast.LENGTH_SHORT).show()
    } catch (e: IOException) {
        Toast.makeText(app, "Recording Failed. Problem accessing storage.", Toast.LENGTH_SHORT).show()
        mediaRecorder.reset()
        return false
    } catch (e: Exception) {
        mediaRecorder.reset()
        return false
    }
    return true
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接