在Android 11中保存文件到外部存储(SDK 30)

30

我正在使用Android 11 (SDK版本30)编写一款新的应用程序,但是我简单地找不到有关如何将文件保存到外部存储的示例。

我阅读了他们的文档,现在知道他们基本上忽略了清单权限(READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE)。他们还忽略了manifest.xml应用程序标记中的android:requestLegacyExternalStorage="true"

在他们的文档https://developer.android.com/about/versions/11/privacy/storage中,他们写道您需要启用DEFAULT_SCOPED_STORAGEFORCE_ENABLE_SCOPED_STORAGE标志以在应用程序中启用作用域存储。

我需要在哪里启用这些标志?当我完成后,如何获得实际的写入外部存储权限?有人可以提供有效的代码吗?
我想要保存.gif、.png和.mp3文件。所以我不想写入图库。

提前感谢。


2
阅读此内容:https://dev59.com/BVIG5IYBdhLWcg3w01A9#66366102 - Thoriya Prahalad
5个回答

32

适用于所有API,包括API 30,Android 11:

public static File commonDocumentDirPath(String FolderName)
{
    File dir = null;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
    {
        dir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + "/" + FolderName);
    }
    else
    {
        dir = new File(Environment.getExternalStorageDirectory() + "/" + FolderName);
    }

    // Make sure the path directory exists.
    if (!dir.exists())
    {
        // Make it, if it doesn't exit
        boolean success = dir.mkdirs();
        if (!success)
        {
            dir = null;
        }
    }
    return dir;
}

现在,使用这个commonDocumentDirPath保存文件。

顺便提一下,从评论中得知,getExternalStoragePublicDirectory在特定的作用域下现在可以在Api 30、Android 11上使用了。感谢CommonsWare的提示。


15
getExternalStoragePublicDirectory和getExternalStorageDirectory都已被弃用。 - Bhavesh Moradiya
2
@BhaveshMoradiya,是的,但现在在Android 11中,上述方法对所有版本都能很好地工作。感谢commonsware的提示。 - Noor Hossain
@NoorHossain,能否分享一下关于相机图像存储的完整代码片段?我想拍摄一张个人头像并将其存储。我尝试了其他可用的方法(使用管理存储权限),但在上传到 Playstore 时它会阻止我的应用程序。你能帮帮我吗? - mr.volatile
@NoorHossain 我在这里提出了一个问题,请知道的话回答一下.. https://stackoverflow.com/q/67984238/7850506 - mr.volatile
@AdityaPatil,你不知道吗?getExternalStoragePublicDirectory 又来了! - Noor Hossain
显示剩余7条评论

10

您可以将文件保存到外部存储的公共目录中,

例如文档、下载、相机拍摄的图片等等。

像以前10版本之前一样使用通常的方式即可。


5
但是,在卸载并重新安装应用程序之后,该应用程序无法识别这些文件。 - Noor Hossain
是的,我们知道,但这并不是被问到的。而且.. 使用saf应用程序可以。 - blackapps
2
什么是SAF?应用程序如何识别在卸载后重新安装之前创建的旧文件? - Noor Hossain
问题是关于Android 11。 - still_dreaming_1
不要。像我在帖子中说的那样,将所有公共目录都排除在外。但并非所有类型的文件都可以放到任何地方。 - blackapps
显示剩余4条评论

7

**最简单的答案和测试 (Java)**

private void createFile(String title) {
        Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setType("text/html");
        intent.putExtra(Intent.EXTRA_TITLE, title);

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Uri.parse("/Documents"));
    }
    createInvoiceActivityResultLauncher.launch(intent);
}

private void createInvoice(Uri uri) {
    try {
        ParcelFileDescriptor pfd = getContentResolver().
                openFileDescriptor(uri, "w");
        if (pfd != null) {
            FileOutputStream fileOutputStream = new FileOutputStream(pfd.getFileDescriptor());
            fileOutputStream.write(invoice_html.getBytes());
            fileOutputStream.close();
            pfd.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

/////////////////////////////////////////////////////
// You can do the assignment inside onAttach or onCreate, i.e, before the activity is displayed


    String invoice_html;
    ActivityResultLauncher<Intent> createInvoiceActivityResultLauncher;

    @Override
    protected void onCreate(Bundle savedInstanceState) {

       invoice_html = "<h1>Just for testing received...</h1>";
       createInvoiceActivityResultLauncher = registerForActivityResult(
                new ActivityResultContracts.StartActivityForResult(),
                result -> {
                    if (result.getResultCode() == Activity.RESULT_OK) {
                        // There are no request codes
                        Uri uri = null;
                        if (result.getData() != null) {
                            uri = result.getData().getData();
                            createInvoice(uri);
                            // Perform operations on the document using its URI.
                        }
                    }
                });

经过一整天的努力,我终于找到了解决PDF保存问题的方法。非常感谢! - Sava Dimitrijević
这是一个完美的答案! - Duy Pham
使用/Documents可以工作,但/Downloads无法工作。您也可以在Documents文件夹中创建自己选择的文件夹。 - Sadique Khan

4

我使用这种方法,它对我真的很有效。 我希望我能帮助你。如果有不清楚的地方,请随时问我。

   Bitmap imageBitmap;

   OutputStream outputStream ;
    
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
            {
                ContentResolver resolver = context.getContentResolver();
                ContentValues contentValues = new ContentValues();
                contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME,"Image_"+".jpg");
                contentValues.put(MediaStore.MediaColumns.MIME_TYPE,"image/jpeg");
                contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH,Environment.DIRECTORY_PICTURES + File.separator+"TestFolder");
                Uri imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,contentValues);
                try {
                    outputStream =  resolver.openOutputStream(Objects.requireNonNull(imageUri) );
                    imageBitmap.compress(Bitmap.CompressFormat.JPEG,100,outputStream);
                    Objects.requireNonNull(outputStream);
                    Toast.makeText(context, "Image Saved", Toast.LENGTH_SHORT).show();
                    
                } catch (Exception e) {
                    Toast.makeText(context, "Image Not Not  Saved: \n "+e, Toast.LENGTH_SHORT).show();
    
                    e.printStackTrace();
                }
    
            }

清单文件(添加许可)

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

嗨,@Mark Nashat,如何处理低于Q版本的Android?我的意思是如何处理两种情况? - Kundan Jha
通过这段代码,我们可以获取外部存储设备中的文件路径: File filePath = Environment.getExternalStorageDirectory(); 然后,根据应用程序名字,创建一个文件夹: File dir = new File(filePath.getAbsolutePath() + "/" + context.getString(R.string.app_name) + "/"); 最后,在这个文件夹中创建一个以当前时间命名的 JPG 文件: File file1 = new File(dir, System.currentTimeMillis() + ".jpg"); - Mark Nashat
尝试 { outputStream = new FileOutputStream(file1); bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream); Toast.makeText(context, "图片已保存", Toast.LENGTH_SHORT).show(); } catch (FileNotFoundException e) { Toast.makeText(context, "图片未保存:\n" + e, Toast.LENGTH_SHORT).show(); e.printStackTrace(); } 尝试 { outputStream.flush(); outputStream.close(); } catch (IOException e) { e.printStackTrace() } } - Mark Nashat
非常感谢@Mark Nashat。还有最后一个问题是如何在Android 10+中知道外部存储中是否存在文件。实际上,如果文件不存在,我想创建该文件,否则覆盖它。(就像您上面提供的答案建议的那样) - Kundan Jha

1

我知道这个问题有点老了,但是这里有一个可行的解决方案,可以在除了根目录之外的任何地方组织您的文件。

首先,在您的build.gradle文件中,实现SAF框架的DocumentFile类:

implementation 'androidx.documentfile:documentfile:1.0.1'

接下来调用此方法请求权限以使 SAF 正常运行(用户安装后只需执行一次):

 private void requestDocumentTreePermissions() {
    // Choose a directory using the system's file picker.
    new AlertDialog.Builder(this)
            .setMessage("*Please Select A Folder For The App To Organize The Videos*")
            .setPositiveButton("Ok", new DialogInterface.OnClickListener() {
                @RequiresApi(api = Build.VERSION_CODES.Q)
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    StorageManager sm = (StorageManager) getSystemService(Context.STORAGE_SERVICE);
                    Intent intent = sm.getPrimaryStorageVolume().createOpenDocumentTreeIntent();

                    String startDir = "Documents";
                    Uri uri = intent.getParcelableExtra("android.provider.extra.INITIAL_URI");

                    String scheme = uri.toString();


                    scheme = scheme.replace("/root/", "/document/");
                    scheme += "%3A" + startDir;

                    uri = Uri.parse(scheme);
                    Uri rootUri = DocumentsContract.buildDocumentUri(
                            EXTERNAL_STORAGE_PROVIDER_AUTHORITY,
                            uri.toString()
                    );
                    Uri treeUri = DocumentsContract.buildTreeDocumentUri(
                            EXTERNAL_STORAGE_PROVIDER_AUTHORITY,
                            uri.toString()
                    );
                    uri = Uri.parse(scheme);
                    Uri treeUri2 = DocumentsContract.buildTreeDocumentUri(
                            EXTERNAL_STORAGE_PROVIDER_AUTHORITY,
                            uri.toString()
                    );
                    List<Uri> uriTreeList = new ArrayList<>();
                    uriTreeList.add(treeUri);
                    uriTreeList.add(treeUri2);
                    getPrimaryVolume().createOpenDocumentTreeIntent()
                            .putExtra(EXTRA_INITIAL_URI, rootUri);
                    Intent intent2 = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
                    // Optionally, specify a URI for the directory that should be opened in
                    // the system file picker when it loads.
                    intent2.addFlags(
                            Intent.FLAG_GRANT_READ_URI_PERMISSION
                                    | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                                    | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
                                    | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
                    intent2.putExtra(EXTRA_INITIAL_URI, rootUri);
                    startActivityForResult(intent2, 99);
                }
            })
            .setCancelable(false)
            .show();


}

接下来存储一些持久化权限:

    @Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == 99 && resultCode == RESULT_OK) {
        //get back the document tree URI (in this case we expect the documents root directory)
        Uri uri = data.getData();
        //now we grant permanent persistant permissions to our contentResolver and we are free to open up sub directory Uris as we please until the app is uninstalled
        getSharedPreferences().edit().putString(ACCESS_FOLDER, uri.toString()).apply();
        final int takeFlags = (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        getApplicationContext().getContentResolver().takePersistableUriPermission(uri, takeFlags);
        //simply recreate the activity although you could call some function at this point
        recreate();
    }
}

您可以在正确的文件上调用documentFile的rename方法。
DocumentFile df = DocumentFile.fromTreeUri(MainActivity.this, uri);
df = df.findFile("CurrentName")
df.renameTo("NewName");

您还可以使用内容解析器打开InputStreams和OutputStreams,因为该DocumentFile的持久URI权限已授予您的内容解析器,使用以下代码片段:

getContentResolver().openInputStream(df.getUri());
getContentResolver().openOutputStream(df.getUri());

InputStreams 用于读取,OutputStreams 用于保存

您可以使用以下命令列出文件

df.listFiles();

或者您可以使用以下命令列出文件:

public static DocumentFile findFileInDirectoryMatchingName(Context mContext, Uri mUri, String name) {
    final ContentResolver resolver = mContext.getContentResolver();
    final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(mUri,
            DocumentsContract.getDocumentId(mUri));
    Cursor c = null;
    try {
        c = resolver.query(childrenUri, new String[]{
                DocumentsContract.Document.COLUMN_DOCUMENT_ID,
                DocumentsContract.Document.COLUMN_DISPLAY_NAME,
                DocumentsContract.Document.COLUMN_MIME_TYPE,
                DocumentsContract.Document.COLUMN_LAST_MODIFIED

        }, DocumentsContract.Document.COLUMN_DISPLAY_NAME + " LIKE '?%'", new String[]{name}, null);
        c.moveToFirst();
        while (!c.isAfterLast()) {
            final String filename = c.getString(1);
            final String mimeType = c.getString(2);
            final Long lastModified = c.getLong(3);
            if (filename.contains(name)) {
                final String documentId = c.getString(0);
                final Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(mUri,
                        documentId);

                return DocumentFile.fromTreeUri(mContext, documentUri);
            }
            c.moveToNext();
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (c != null) {
            c.close();
        }
    }

    return null;
}

哪种方法比df.listFiles()方法运行更快

源代码(这是我自己的实现,但这里是原始的SF问题) 在针对Android 11(Api 30)时重命名视频/图像


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