在 Android Q 中已弃用 getExternalStoragePublicDirectory

由于 公共目录Android Q中已经被弃用,因此建议使用其他方法。那么,我们如何指定我们要存储生成的照片从一个相机应用程序到 DCIM文件夹,或自定义子文件夹内的 DCIM

文件指出,接下来的3种选择是新的首选方案:

  1. 上下文 # getExternalFilesDir (String)
  2. 意图 # ACTION _ OPEN _ DOCUMENT
  3. MediaStore

选项1是出的问题,因为这将意味着照片被删除,如果应用程序被卸载。

选项2也不是一个选项,因为它要求用户通过 SAF 文件浏览器选择位置。

我们只剩下选项3,MediaStore; 但是在这个问题出现的时候,还没有关于如何使用它替代 Android Q 中 getExternalStoragePublicDirectory 的文档。

78707 次浏览

Based on the docs, use DCIM/... for the RELATIVE_PATH, where ... is whatever your custom subdirectory would be. So, you would wind up with something like this:

      val resolver = context.contentResolver
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, "CuteKitten001")
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/PerracoLabs")
}


val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)


resolver.openOutputStream(uri).use {
// TODO something with the stream
}

Note that since RELATIVE_PATH is new to API Level 29, you would need to use this approach on newer devices and use getExternalStoragePublicDirectory() on older ones.

@CommonsWare answer is amazing. But for those who want it in Java, you need to try this:

ContentResolver resolver = context.getContentResolver();
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType);
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);


Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);

As per the suggestion of @SamChen the code should look like this for text files:

Uri uri = resolver.insert(MediaStore.Files.getContentUri("external"), contentValues);

Because we wouldn't want txt files lingering in the Images folder.

So, the place where I have mimeType, you enter the mime type you want. For example if you wanted txt (@Panache) you should replace mimeType with this string: "text/plain". Here is a list of mime types: https://www.freeformatter.com/mime-types-list.html

Also, where I have the variable name, you replace it with the name of the file in your case.

Apps targeting Android Q - API 29+ disabled storage access by default due to security issues. If you want to enable it to add the following attribute in the AndroidManifest.xml:

<manifest ... >
<!-- This attribute is "false" by default for Android Q or higher -->
<application android:requestLegacyExternalStorage="true" ... >
...
</application>
</manifest>

then you have to use getExternalStorageDirectory() instead of getExternalStoragePublicDirectory().

Example: If you want to create a directory in the internal storage if not exists.

 File mediaStorageDir = new File(Environment.getExternalStorageDirectory() + "/SampleFolder");


// Create the storage directory if it does not exist
if (! mediaStorageDir.exists()){
if (! mediaStorageDir.mkdirs()){
Log.d("error", "failed to create directory");
}
}

import android.content.ContentValues
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity


class MyActivity : AppCompatActivity() {


@RequiresApi(Build.VERSION_CODES.Q)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_mine)
val editText: EditText = findViewById(R.id.edt)
val write: Button = findViewById(R.id.Output)
val read: Button = findViewById(R.id.Input)
val textView: TextView = findViewById(R.id.textView)


val resolver = this.contentResolver
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, "myDoc1")
put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
put(MediaStore.MediaColumns.RELATIVE_PATH, "Documents")
}
val uri: Uri? = resolver.insert(MediaStore.Files.getContentUri("external"), contentValues)
Log.d("Uri", "$uri")


write.setOnClickListener {
val edt : String = editText.text.toString()
if (uri != null) {
resolver.openOutputStream(uri).use {
it?.write("$edt".toByteArray())
it?.close()


}
}
}
read.setOnClickListener {
if (uri != null) {
resolver.openInputStream(uri).use {
val data = ByteArray(50)
it?.read(data)
textView.text = String(data)


}
}
}
}
}

Here, I am storing a text file in phone's Document folder by writing text into edit text and by clicking button 'Write' it will save the file with the text written. On clicking button 'Read' it will bring the text from that file and then display it in the text view.

It will not run on devices that are below android Q or android 10 as RELATIVE_PATH can only be used in these versions.

For Xamarin.Android the following code from a published project could help:

    Java.IO.File jFolder;


if ((int)Android.OS.Build.VERSION.SdkInt >= 29)
{
jFolder = new Java.IO.File(Android.App.Application.Context.GetExternalFilesDir(Environment.DirectoryDcim), "Camera");
}
else
{
jFolder = new Java.IO.File(Environment.GetExternalStoragePublicDirectory(Environment.DirectoryDcim), "Camera");
}


if (!jFolder.Exists())
jFolder.Mkdirs();


var filename = GenerateJpgFileName();
var jFile = new Java.IO.File(jFolder, filename);
var fullFilename = jFile.AbsoluteFile.ToString();


using (var output = new System.IO.FileStream(fullFilename, System.IO.FileMode.Create))
{
outputBitmap.Compress(Bitmap.CompressFormat.Jpeg, 90, output);
output.Close();
}

Not using permissions in Android, it's done in from the shared project using Xamarin.Essentials.

Generally I used this way :

     var data: File =Environment.getExternalStoragePublicDirectory
(Environment.DIRECTORY_DOWNLOADS)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
data = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)!!
}

If you want to save your file in a app specific external storage, yes you can use context.getExternalFilesDir(). Many answers point out that.

However, this is not the answer of this question because getExternalFilesDir() is app specific external storage, getExternalStoragePublicDirectory() is shared storage.

For example, you want to save a downloaded pdf file to "Shared" Download directory. How do you do that ? For api 29 and above, you can do that without no permission.

For api 28 and below, you need getExternalStoragePublicDirectory() method but it is deprecated. What if you don't want to use that deprecated method? Then you can use SAF file explorer(Intent#ACTION_OPEN_DOCUMENT). As said in the question, this requires the user to pick the location manually.

This is what Google wants exactly. To improve user privacy, direct access to shared/external storage devices is deprecated.

When an app targets Build.VERSION_CODES.Q, the path returned from this method is no longer directly accessible to apps. Apps can continue to access content stored on shared/external storage by migrating to alternatives such as Context#getExternalFilesDir(String), MediaStore, or Intent#ACTION_OPEN_DOCUMENT.

Details are given in the following link:

https://developer.android.com/reference/android/os/Environment#getExternalStorageDirectory()