getExternalStoragePublicDirectory deprecated in Android Q
Asked Answered
M

5

117

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. Intent#ACTION_OPEN_DOCUMENT
  3. MediaStore

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

Option 2 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 3, 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.

Maidel answered 5/6, 2019 at 21:53 Comment(0)
P
92

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.

Probably answered 5/6, 2019 at 22:13 Comment(47)
Thanks for the answer. As a side note, I just saw in the documentation that there is also a new field "IS_PENDING", which I'm guessing that maybe I should also include when creating the ContentValues, so to be safe while writing the file, and then update it once the file is fully saved.Maidel
What if I wanted to get root path to do other operations, not just inserting a photo. for example I had Environment.getExternalStorageDirectory() + "/myFolder". now what should I do to get this path with MediaStore as String/File/Uri?Octavia
@S.R: You do not have filesystem access to arbitrary locations on external storage on Android Q (by default) and Android R (for all apps). So, you should be focusing on doing something else for your other operations, using an approach that does not require filesystem access. Or, stick to getExternalFilesDir() on Context.Probably
last question! Is using getParentFile on getExternalFilesDir() 4 times safe on all devices to get what I'm looking for?Octavia
@S.R: No. First, as I wrote, you do not have filesystem access to arbitrary locations on external storage on Android Q (by default) and Android R (for all apps). You will not have access to the location that you are trying to access. Beyond that, there is no requirement for your algorithm to work on any particular device.Probably
Add your own article: commonsware.com/blog/2019/04/22/….Dorcia
I wonder is there really no option to implement this once. This works, but I figured this much before. I don't necessarily wanna keep using getExternalStoragePublicDirectory() as well as the new approach. Now I'm having multiple if / else to determine Android version threwout the codebase to do it both the new and the old way.Egad
This new approach doesn't let you get access to previously created files, no? For example, if I made a camera app that puts the files in the folder you've mentioned, and then I remove and re-install my app, I can't access them anymore, no? I think the storage permission was replaced by other permissions, that are more restricted, no (maybe can handle only media files) ? Also, is there a list of all possible files types that are allowed to be created? Are those only media files, or any file (like PDF, ZIP, APK,...) ?Moulder
@androiddeveloper: "if I made a camera app that puts the files in the folder you've mentioned, and then I remove and re-install my app, I can't access them anymore, no?" -- correct. From Android's standpoint, files from a previous installation of your app are the same as files from some arbitrary other app. "I think the storage permission was replaced by other permissions, that are more restricted, no (maybe can handle only media files) ?" -- they got rid of those in Beta 3.Probably
@androiddeveloper: "is there a list of all possible files types that are allowed to be created?" -- not that I am aware of. "Are those only media files, or any file (like PDF, ZIP, APK,...) ?" -- I think that you are limited to media files, in terms of using MediaStore.Probably
@Probably They changed their decisions a lot on Q's beta versions. It's very hard to follow and know what really is going on. So there is now only SAF to access files? And MediaStore to get global ones that are only for media files (which I don't know what "media" could be, except for maybe some common image, video and audio files) ? It's quite sad actually, that a PC that's connected to the device can show you all files easily and with paths (though not the real paths), yet on the device itself we have to use SAF...Moulder
@androiddeveloper: "So there is now only SAF to access files?" -- AFAIK, yes. "And MediaStore to get global ones that are only for media files (which I don't know what "media" could be, except for maybe some common image, video and audio files) ?" -- AFAIK, yes.Probably
@Probably Can you please point me to the most updated docs about the MediaStore ? I find it a bad decision that Google doesn't even explain what "media" is. I should write them about it. I searched in many places and still couldn't find. I've noticed you wrote at least 10 articles about storage permission. My guess is that you will make more when Q is really out. I hope there will be a way to overcome the file-path restrictions. Many libraries require it, including the Android framework itself (parse APK and DB files, for exmaple) .Moulder
@androiddeveloper: I'm sure that you have already read developer.android.com/preview/privacy/scoped-storage and developer.android.com/preview/…. I am not aware of anything more in the standard documentation for Android Q changes on those topics.Probably
@Probably Thank you. I think that looking at the docs you've shown, it means that "media" means "Photos, which are stored in MediaStore.Images.", "Videos, which are stored in MediaStore.Video.", "Music files, which are stored in MediaStore.Audio." . Reaching each of those, it seems it's all about mimetype: audio/*, video/*, image/* . What do media player apps (video, audio and images) request from the OS on Q , exactly, if it's not storage permission? What does the user see ? I wonder if Google updated its samples for Q in this matter.Moulder
@androiddeveloper: "What do media player apps (video, audio and images) request from the OS on Q , exactly, if it's not storage permission?" -- they request READ_EXTERNAL_STORAGE. That is covered in developer.android.com/preview/privacy/scoped-storage.Probably
@Probably I see. Seems I've skipped this part. So the Storage permission got reduced very much, as I've remembered. It allows access to just media files, and even then it can't get all the information about them (metadata) by default. And it doesn't even allow access to those files using file-path, because if it did, it would mean I could just use InputStream to get all that's inside the media file, including the protected metadata... Right?Moulder
@androiddeveloper: That description matches my understanding.Probably
@Probably I see . Thank you.Moulder
@Probably Wait, about media files,it says that in order to get all of the metadata, we will need a permission of "ACCESS_MEDIA_LOCATION" . When adding it, I've noticed it belongs to the storage permission group. So why is it even needed? After all, it will get granted together with storage permission... OddMoulder
"ACCESS_MEDIA_LOCATION" is apparently their soul reason for abstracting us away from the filesystem. Without this permission you cannot see the location timestamp where fotos where taken (normally that's in the .jpg format, so you could extract it from the file itself, not the mediastore). When you do not have the permission the .jpg you can access threw the input stream that mediastore gives you will have those informations removed from the stream. Storage permission is only used so that mediastore gives you access to media that you yourself did not create.Egad
Is there also a Java Example for this?Opalescent
@GauravMall: Somebody might have one -- I do not. Sorry!Probably
Oh okay. By the way is this code on some documentation?Opalescent
Posted an answer in Java. Thanks for the answer :)Opalescent
@GauravMall: "is this code on some documentation?" -- I think I created it based on this example of storing a video via MediaStore. I just adjusted it to be for images.Probably
@Probably How can i create a folder in android Q, Since getExternalStoragePublicDirectory() is deprecated. Help will be appreciated.Kirbykirch
@TusharPingale: Use the methods on Context, such as getExternalFilesDir(). See this and this.Probably
Problem with MEDIA STORE : it's made for media files (audios, pictures, and videos). My app save 2 databases (sqlite) in the DOWNLOAD directory, and need these files to work... I don't want to download and save these db files in MOVIES or another media directory, I want to continue to download my files in a sub-directory of DOWNLOAD directory, like I do since several years ! I cannot upgrade my app sdk target to 29 due to this problem. Is someone has a solution ? THANKSKurt
@Christian: "Problem with MEDIA STORE : it's made for media files (audios, pictures, and videos)" -- Android 10 also added MediaStore.Downloads. "I want to continue to download my files in a sub-directory of DOWNLOAD directory" -- use MediaStore.Downloads. See commonsware.com/blog/2020/01/11/…Probably
@Probably The problem with this is that there is no way of "refreshing" files created by my application(without restarting the device, or waiting an X amount of time), or is there? MediaScannerConnection requires me to pass a file object. There is also no way of knowing when MediaRecorder is done writing a file - before Q we could use FileObserver. That leaves me with one option, to use getExternalFilesDir(). The problem with this is that the file will not be available to the user in a public directory (Environment.DIRECTORY_MOVIES). Do you have any suggestions?Gravely
@HB.: If you insert() into MediaStore or update() contents of the MediaStore, MediaStore is "refreshed" immediately to reflect what you inserted or updated.Probably
@Probably But I first create a Uri as you did above, then I pass the FileDescriptor of that Uri (as you mentioned here - https://mcmap.net/q/88416/-setoutputfile-in-mediarecorder-in-storage-access-framework) to MediaRecorder. There is no way of knowing when MediaRecorder is done writing the file, without using FileObserver - If I want to keep using FileObserver, I have to request legacy storage - If I request legacy storage, I might as well keep using getExternalStoragePublicDirectory(). So, I have these options? - Use a deprecated API or hope that MediaRecorder is done writing the file by the time I need it.Gravely
@HB.: File a bug report to get MediaRecorder updated with a better API. Or, see if there is a third-party media recording library that offers you a better option. Or, record to a file on getFilesDir() and only transfer it into the MediaStore when recording is complete.Probably
Can you help me with #63543914Trici
@Probably how to transfer file to MediaStore after recording video file to getFilesDir() or getExternalFilesDir()Astronaut
I don't get though: If I want to reach the path of any of the common ones (like of "Downloads"), how am I supposed to do it, instead of Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) ?Moulder
@androiddeveloper: For read operations, you can call getDirectory() on StorageVolume on Android 11+. This roughly maps to Environment.getExternalStoragePublicDirectory(), but on a per-volume basis. StorageManager gives you access to the roster of StorageVolume objects.Probably
@Probably Where is the part on the alternative to choose the type of directory (Environment.DIRECTORY_...) ? Do you mean that I just add it to the path of the volume? Was it deprecated since we can have multiple volumes, each has the same folders? This is how I'm supposed to use it now: File(storageManager.primaryStorageVolume.directory!!,Environment.DIRECTORY_DOWNLOADS) ? If so, please update your answer because that's the new way to use it insteadMoulder
@androiddeveloper: "Do you mean that I just add it to the path of the volume?" -- presumably, yes. "Was it deprecated since we can have multiple volumes, each has the same folders?" -- I do not know about the "each has the same folders" part. "please update your answer" -- no, because this is unrelated to the answer. The question and the answer are about write operations, whereas "the path of any of the common ones" is only going to be relevant for read operations at best.Probably
@Probably OK sorry and thank you. Is it a good assumption though that it's non-nullable for the primary storage volume, and also that the same folders (like DIRECTORY_DOWNLOADS for example) are standard and fine to use on other storage volumes?Moulder
@androiddeveloper: "Is it a good assumption though that it's non-nullable for the primary storage volume" -- it is probably worth a null check, but I have no idea what null would mean in that context. "also that the same folders (like DIRECTORY_DOWNLOADS for example) are standard and fine to use on other storage volumes?" -- I would not assume that those directories exist.Probably
@Probably The directories might not exist even on the primary volume. I just ask if it's a safe assumption that they are standard and worth using this way, or maybe only the folders (of DIRECTORY_DOWNLOADS and the rest) of the primary volume are considered standard .Moulder
@androiddeveloper: Ah, sorry, I have no idea.Probably
@Probably That's ok. You help a lot everywhere. Thank you very much for your time.Moulder
How to move my files from public directory to Android/Media/com.myapp directory in android 11 like whatsapp did without asking any permission? how to get public directory path if getExternalStoragePublicDirectory is now deprecated in Q?Spode
thumb up only for "perracolabs" directory nameGiusto
O
38

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

Opalescent answered 25/8, 2019 at 20:58 Comment(15)
Gaurav, you answer is good for java guys but can u mention what is the syntax for txt file?Larrisa
Gaurav i m getting error, can not insert text/plain in to MediaStore.Images.Media. I have passed name as file.getName and mimetype = text/plain. pls helpLarrisa
Gaurav, just a question where is path of source file in this code, is it not required? let us discuss here #59511647Larrisa
For text file remember this Uri uri = getContentResolver().insert(MediaStore.Files.getContentUri("external"), values);We
Can you help me with #63543914Trici
val imgUri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)Rummer
How to move my files from public directory to Android/Media/com.myapp directory in android 11 like whatsapp did without asking any permission? how to get public directory path if getExternalStoragePublicDirectory is now deprecated in Q?Spode
@bdevloper I don't think you can do it without asking for permissions. That's the whole issue that's being discussed here. And basically, from my code, you can replace Environment.DIRECTORY_DOWNLOADS with any directory you want.Opalescent
then how whatsapp did ? I didn't gave permission for manage_file_access and still they did move whole folder inside Android\media\com.whatsappSpode
I can move my folder using this code but I'm not sure this gonna work after live in playstore. Hope using preserveLegacyExternalStorage will help maybeSpode
@SamChen does .xls file included to text files ?Olympium
@Olympium You can try to set the mimeType to "text/xls", I have never tried.We
@SamChen i try text/xls & application/vnd.ms-excel both save my generated file, but file size is 0kb, any idea whats wrong ?Olympium
@Olympium I don't know, maybe the excel file is not recognized as regular text file by MediaStore, but I have an idea, you can convert the excel file to json file, text/json is acceptable I can guarantee. Then you convert it back to excel. Meanwhile ask StackOverflow.We
Using the MediaStore is very useless if you need to store a bunch of files that have a mixture of recognized and unrecognized mimes. Exceptions will be thrown if you do this.Stillas
O
5

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");
     }
 }
Ous answered 25/1, 2021 at 14:56 Comment(4)
Do you really need to use requestLegacyExternalStorage="true" for scoped storage? I think once you use getExternalStorageDirectory(), it's not necessary. Also, legacy flag is ignored on Api 30+.Filicide
It won't work if you missed requestLegacyExternalStorage="true" on the target device api version is greater than 28+Ous
This is not good as it is just a temporary solution according to documentation as well. Use MediaStore and ContentValuesTrollop
requestLegacyExternalStorage will be ignored once you target API 30+ (Android 11+) For me getExternalStoragePublicDirectory() is still working and is not marked as deprecated with targetSdk 31 and compileSdk 32Croesus
D
2

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.

Diathermy answered 29/1, 2021 at 11:44 Comment(1)
How to move my files from public directory to Android/Media/com.myapp directory in android 11 like whatsapp did without asking any permission? how to get public directory path if getExternalStoragePublicDirectory is now deprecated in Q?Spode
L
0

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

Laticialaticiferous answered 10/5, 2021 at 10:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.