Android 10:如何将文件保存到名为“/sdcard/my-app/”的外部存储目录中?

42
在 Android Pie 之前,我一直将应用程序需要存储的文件存储在 /sdcard/my-app/ 中,可以通过以下方式获取: File fBaseDir = new File(Environment.getExternalStorageDirectory(), "my-app"); 我的一个应用程序在 /sdcard/my-app/favicons 中存储了数百个(多达数千个)网站图标,在用户备份该应用程序时,它会被存储在 /sdcard/my-app/backups 中。通过 Google Chrome 共享给我的应用程序的网页截图存储在 /sdcard/my-app/screenshots 中。
当我卸载应用程序后,我可以重新安装它,并且仍然可以使用所有的网站图标、截图和备份来恢复状态,而无需重新下载所有网站图标(并有可能丢失其他数据)。
我还可以轻松地创建 /sdcard/my-app/ 目录的备份,将其复制到我的 PC 中,并在另一台手机上恢复它,例如当我迁移到新设备时。
我还可以使用文件浏览器查看该目录中的文件,并选择将它们发送电子邮件或复制到 Google Drive,或者专门删除其中一些文件(如旧备份文件)。自 Android 10 开始,这种方法已经不再可行。我无法将图标添加到“Images Media-Folder”中,因为它们不是真正的图片,这会使视图变得杂乱无序,可能会导致我的开发者帐户因此被 Play Store 禁止。我也不想将所有这些数据存储在应用程序专用目录中,因为当应用被卸载时,这些数据将被删除,并且像文件浏览器之类的其他应用程序也无法访问它们。
那么我的选择是什么?手动选择文件或目录不是一个选项,除非只需要选择一次目录,然后永久授权访问(读写文件)。
我从未在 /sdcard/my-app/ 之外读取或写入过任何内容。

2
首先,仍然有android:requestLegacyExternalStorage模式,尽管这是暂时的。第二种方法是使用SAF,让用户选择根目录并授予您的应用程序访问权限,然后您可以使用DocumentFile处理复制、移动和其他操作。 - Darshan
@DarShan 谢谢,android:requestLegacyExternalStorage 在开发过程中会很有用。使用 SAF 授权访问是永久的吗? - Daniel F
1
android:requestLegacyExternalStorage will work on production too, untill the next major release. About the SAF permission being permanent, you can save the Uri that you get in onActivityResult() and use grantPermission() and getContentResolver().takePersistableUriPermission() - Darshan
1
没问题,Android 11/R,随便。 :) - Darshan
6
您的使用情境是完全合理的,令人震惊的是,谷歌没有关注这个“功能”将无法逆转地破坏多少应用程序。 - Andrew Koster
显示剩余3条评论
3个回答

28

在Android Q中,默认情况下禁用了应用程序访问其私有文件夹之外的直接文件访问。以下是您可以在自己的情况下使用的一些策略:

  1. 使用清单选项requestLegacyExternalStorage,以获得旧行为,但是它将不再与Android R一起使用,因此这只是一个短期解决方案;
  2. 使用getExternalFilesDir()方法保存文件。这是您的私有文件夹,其他应用程序只能在具有READ_EXTERNAL_STORAGE权限的情况下访问这些文件。在这种情况下,最好使用FileProvider授予其他应用程序访问您的文件。
  3. 使用StorageManager类的getPrimaryStorageVolume().createOpenDocumentTreeIntent()方法请求访问外部主要卷。在这种情况下,您需要用户同意,并且您将无法直接使用File api,但是使用DocumentFile类,您拥有非常相似的界面,因此这是最接近旧行为的解决方案。如果您需要在前台和后台执行操作(即没有用户交互的情况下),则适用。唯一的例外是请求权限时的第一次交互。

我链接了Flipper库以实现第3点,它可帮助像旧Android版本中一样管理文件。


如果我将文件保存到应用程序目录(context.getExternalFilesDir),那么我可以通过使用File mFile = new File(PathToExternalFilesDir);访问该文件吗?然后,当调用mFile.getAbsoluteFile()时,它将返回一个file:// Uri - 那么我的应用程序是否可以使用该文件Uri? - HB.
您的私人目录中允许文件接口。 - greywolf82
@HB。你说得对,自从Android-Q以来,应用程序即使没有任何权限也可以访问其特定目录。此外,其他应用程序将无法访问特定应用程序沙盒目录。请参阅作用域存储影响总结表 https://developer.android.com/training/data-storage/files/external-scoped。 - Anoop M Maddasseri
1
你的第二点很棘手。我尝试使用DownloadManager并将目标设置为getExternalFilesDir()。下载似乎已经完成,但是一旦我检查文件是否存在,它就不存在了。 - New Guy
抱歉兄弟!我不知道如何检查文件是否存在,也无法正确理解如何使用Flipper编写代码。因此,我会给这个答案投反对票!因为我真的需要帮助.. - Jayesh Rathod
这是我的先前答案。它可以100%工作。https://dev59.com/Qrvpa4cB1Zd3GeqPAOPl#67360128 - mahesh kumar

3
从Android 11开始,如果您需要访问外部文件目录,则需要在清单中添加 MANAGE_EXTERNAL_STORAGE 权限。
应用程序可以通过以下方式向用户请求所有文件访问权限: 1. 在清单中声明 MANAGE_EXTERNAL_STORAGE 权限。 2. 使用 ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION 意图操作将用户引导到系统设置页面,在此页面上他们可以为您的应用程序启用以下选项: 允许访问并管理所有文件。 3. 要确定您的应用程序是否已被授予 MANAGE_EXTERNAL_STORAGE 权限,请调用 Environment.isExternalStorageManager()
阅读此处:此处

2
这不是完全正确的。只要您不在Play商店上发布应用程序,它就可以正常工作。但是,如果您发布了应用程序,则必须满足特定的用例集才能获得权限。https://developer.android.com/training/data-storage/manage-all-files#all-files-access-google-play - Pztar

1

针对Android Q以上版本这里输入图片描述,以下代码可以很好地保存图像到外部存储。请不要忘记添加清单权限。



        package com.myretro.saveimager;
        
        import androidx.appcompat.app.AppCompatActivity;
        
        import android.content.ContentResolver;
        import android.content.ContentValues;
        import android.graphics.Bitmap;
        import android.graphics.drawable.BitmapDrawable;
        import android.net.Uri;
        import android.os.Build;
        import android.os.Bundle;
        import android.os.Environment;
        import android.provider.MediaStore;
        import android.widget.Button;
        import android.widget.ImageView;
        import android.widget.Toast;
        
        import java.io.File;
        import java.io.FileNotFoundException;
        import java.io.OutputStream;
        
        public class MainActivity extends AppCompatActivity {
            private Button btnsave;
            private ImageView myImage;
        
            @Override
            protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.activity_main);
                btnsave = findViewById(R.id.button);
                myImage = findViewById(R.id.imageView);
        
        
                btnsave.setOnClickListener(view -> {
                    BitmapDrawable bitmap1 = (BitmapDrawable) myImage.getDrawable();
                    Bitmap bitmap = bitmap1.getBitmap();
        
                    saveImmageIntoExternalStrogaeaboveQ(bitmap);
                });
            }
        
            private void saveImmageIntoExternalStrogaeaboveQ(Bitmap bitmap) {
                OutputStream outputStream;
        
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    ContentResolver contentResolver = getContentResolver();
        
                    ContentValues mValue = new ContentValues();
        
                    mValue.put(MediaStore.MediaColumns.DISPLAY_NAME, "hacker" + ".jpg");
                    mValue.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpg");
                    mValue.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + File.separator + "myimage");
                    Uri imageuri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mValue);
        
                    try {
                        outputStream = contentResolver.openOutputStream(imageuri);
                        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
                        Toast.makeText(this, "Image Saved SuccessFull", Toast.LENGTH_SHORT).show();
                    } catch (FileNotFoundException e) {
                        Toast.makeText(this, "Something went wrong " + e.getMessage(), Toast.LENGTH_SHORT).show();
                        e.printStackTrace();
                    }
        
                } else {
                    //old method to write
                }
    
      }
    
    

<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/> - Aakash Kumar

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