getExternalStoragePublicDirectory deprecated in Android Q

Android

Android Problem Overview


As getExternalStoragePublicDirectory has been deprecated in Android Q, and the recommendation is to use other means. then how can we specify that we want to store the generated photos from a camera app into the DCIM folder, or a custom sub-folder within the DCIM?

The documentation states that the next 3 options are the new preferred alternatives:

  1. Context#getExternalFilesDir(String)
  2. MediaStore
  3. Intent#ACTION_OPEN_DOCUMENT

Option 1 is out of the questions as it would mean that the photos get deleted if the app gets uninstalled.

Option 3 is also not a choice, as it would require the user to pick the location through the SAF file explorer.

We are left with option 2, the MediaStore; but at the time of this question there is no documentation on how to use it as a replacement for getExternalStoragePublicDirectory in Android Q.

Android Solutions


Solution 1 - Android

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.

Solution 2 - Android

@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.

Solution 3 - Android

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");
     }
 }

Solution 4 - Android

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.

Solution 5 - Android

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.

Solution 6 - Android

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)!!
                    }

Solution 7 - Android

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()

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionPerracoLabsView Question on Stackoverflow
Solution 1 - AndroidCommonsWareView Answer on Stackoverflow
Solution 2 - AndroidGaurav MallView Answer on Stackoverflow
Solution 3 - AndroidCodemakerView Answer on Stackoverflow
Solution 4 - AndroidRitika GyananiView Answer on Stackoverflow
Solution 5 - AndroidNick KovalskyView Answer on Stackoverflow
Solution 6 - AndroidMoriView Answer on Stackoverflow
Solution 7 - AndroidoiyioView Answer on Stackoverflow