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.

PerracoLabs
  • 16,449
  • 15
  • 74
  • 127

5 Answers5

91

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
  • 986,068
  • 189
  • 2,389
  • 2,491
  • 4
    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. – PerracoLabs Jun 05 '19 at 22:41
  • 1
    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? – S.R Jul 01 '19 at 14:50
  • 1
    @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`. – CommonsWare Jul 01 '19 at 14:53
  • 1
    last question! Is using `getParentFile` on `getExternalFilesDir()` **4 times** safe on all devices to get what I'm looking for? – S.R Jul 01 '19 at 14:59
  • 1
    @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. – CommonsWare Jul 01 '19 at 15:03
  • 2
    Add your own article: https://commonsware.com/blog/2019/04/22/death-external-storage-more-story.html. – CoolMind Jul 15 '19 at 10:09
  • 2
    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. – Max Jul 20 '19 at 16:18
  • 2
    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,...) ? – android developer Jul 23 '19 at 18:06
  • 2
    @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. – CommonsWare Jul 23 '19 at 18:10
  • 2
    @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`. – CommonsWare Jul 23 '19 at 18:11
  • 1
    @CommonsWare 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... – android developer Jul 23 '19 at 18:23
  • 3
    @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. – CommonsWare Jul 23 '19 at 18:23
  • 1
    @CommonsWare 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) . – android developer Jul 23 '19 at 18:36
  • 2
    @androiddeveloper: I'm sure that you have already read https://developer.android.com/preview/privacy/scoped-storage and https://developer.android.com/preview/features#create-files-external-storage. I am not aware of anything more in the standard documentation for Android Q changes on those topics. – CommonsWare Jul 23 '19 at 18:42
  • 1
    @CommonsWare 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. – android developer Jul 24 '19 at 07:24
  • 2
    @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 https://developer.android.com/preview/privacy/scoped-storage. – CommonsWare Jul 24 '19 at 10:58
  • 2
    @CommonsWare 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? – android developer Jul 24 '19 at 11:42
  • 2
    @androiddeveloper: That description matches my understanding. – CommonsWare Jul 24 '19 at 11:48
  • 1
    @CommonsWare I see . Thank you. – android developer Jul 24 '19 at 13:29
  • 1
    @CommonsWare 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... Odd – android developer Jul 24 '19 at 18:03
  • 2
    "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. – Max Aug 13 '19 at 16:59
  • 1
    Is there also a Java Example for this? – Gaurav Mall Aug 25 '19 at 20:48
  • 1
    @GauravMall: Somebody might have one -- I do not. Sorry! – CommonsWare Aug 25 '19 at 20:49
  • 1
    Oh okay. By the way is this code on some documentation? – Gaurav Mall Aug 25 '19 at 20:51
  • 1
    Posted an answer in Java. Thanks for the answer :) – Gaurav Mall Aug 25 '19 at 20:58
  • 1
    @GauravMall: "is this code on some documentation?" -- I think I created it based on [this example of storing a video via `MediaStore`](https://gitlab.com/commonsguy/cw-android-q/tree/v0.5/ConferenceVideos). I just adjusted it to be for images. – CommonsWare Aug 25 '19 at 21:35
  • 1
    @CommonsWare How can i create a folder in android Q, Since getExternalStoragePublicDirectory() is deprecated. Help will be appreciated. – Tushar Pingale Oct 20 '19 at 14:36
  • 1
    @TusharPingale: Use the methods on `Context`, such as `getExternalFilesDir()`. See [this](https://stackoverflow.com/a/58379655/115145) and [this](https://commonsware.com/blog/2019/06/07/death-external-storage-end-saga.html). – CommonsWare Oct 20 '19 at 14:39
  • 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 ? THANKS – Christian Feb 06 '20 at 16:27
  • @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 https://commonsware.com/blog/2020/01/11/scoped-storage-stories-diabolical-details-downloads.html – CommonsWare Feb 06 '20 at 19:19
  • @CommonsWare 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? – HB. May 09 '20 at 06:41
  • @HB.: If you `insert()` into `MediaStore` or `update()` contents of the `MediaStore`, `MediaStore` is "refreshed" immediately to reflect what you inserted or updated. – CommonsWare May 09 '20 at 10:54
  • @CommonsWare But I first create a `Uri` as you did above, then I pass the `FileDescriptor` of that `Uri` (as you mentioned here - https://stackoverflow.com/a/46897595/5550161) 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. – HB. May 10 '20 at 06:57
  • 1
    @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. – CommonsWare May 10 '20 at 11:12
  • Can you help me with https://stackoverflow.com/questions/63543414/rename-file-of-the-external-storage-which-is-created-by-app-in-android-10-worki – jazzbpn Aug 24 '20 at 03:44
  • @CommonsWare how to transfer file to `MediaStore` after recording video file to `getFilesDir()` or `getExternalFilesDir()` – user924 Sep 01 '20 at 08:17
  • 1
    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)` ? – android developer May 01 '21 at 14:01
  • 1
    @androiddeveloper: For read operations, you can call [`getDirectory()` on `StorageVolume`](https://developer.android.com/reference/kotlin/android/os/storage/StorageVolume#getdirectory) 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. – CommonsWare May 01 '21 at 14:13
  • 1
    @CommonsWare 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 instead – android developer May 01 '21 at 16:54
  • 1
    @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. – CommonsWare May 01 '21 at 17:02
  • 1
    @CommonsWare 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? – android developer May 01 '21 at 19:53
  • 1
    @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. – CommonsWare May 01 '21 at 20:00
  • 1
    @CommonsWare 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 . – android developer May 01 '21 at 20:34
  • 1
    @androiddeveloper: Ah, sorry, I have no idea. – CommonsWare May 01 '21 at 20:36
  • @CommonsWare That's ok. You help a lot everywhere. Thank you very much for your time. – android developer May 01 '21 at 20:40
  • 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? – Bhavin Patel Jul 05 '21 at 13:10
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.

Gaurav Mall
  • 2,372
  • 1
  • 17
  • 33
  • Gaurav, you answer is good for java guys but can u mention what is the syntax for txt file? – Panache Dec 26 '19 at 04:20
  • 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 help – Panache Dec 28 '19 at 14:30
  • Gaurav, just a question where is path of source file in this code, is it not required? let us discuss here https://stackoverflow.com/questions/59511147/create-copy-file-in-android-q-using-contentresolver – Panache Dec 28 '19 at 14:55
  • 2
    For text file remember this `Uri uri = getContentResolver().insert(MediaStore.Files.getContentUri("external"), values);` – Sam Chen Mar 23 '20 at 15:11
  • Can you help me with https://stackoverflow.com/questions/63543414/rename-file-of-the-external-storage-which-is-created-by-app-in-android-10-worki – jazzbpn Aug 24 '20 at 03:44
  • val imgUri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) – BAIJU SHARMA Sep 13 '20 at 08:58
  • 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? – Bhavin Patel Jul 05 '21 at 13:12
  • @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. – Gaurav Mall Jul 06 '21 at 11:50
  • then how whatsapp did ? I didn't gave permission for manage_file_access and still they did move whole folder inside `Android\media\com.whatsapp` – Bhavin Patel Jul 07 '21 at 10:02
  • I can move my folder using [this](https://stackoverflow.com/a/68265107/6333971) code but I'm not sure this gonna work after live in playstore. Hope using [preserveLegacyExternalStorage](https://developer.android.com/training/data-storage/use-cases#if_your_app_targets) will help maybe – Bhavin Patel Jul 07 '21 at 10:08
  • @SamChen does .xls file included to text files ? – Pif Aug 13 '21 at 13:09
  • @Pif You can try to set the `mimeType` to `"text/xls"`, I have never tried. – Sam Chen Aug 13 '21 at 13:31
  • @SamChen i try `text/xls` & `application/vnd.ms-excel` both save my generated file, but file size is 0kb, any idea whats wrong ? – Pif Aug 13 '21 at 13:47
  • @Pif 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. – Sam Chen Aug 13 '21 at 14:37
  • 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. – Johann Dec 14 '21 at 15:51
4

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");
     }
 }
Codemaker2015
  • 12,190
  • 6
  • 97
  • 81
  • 3
    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+. – Micer Feb 02 '21 at 17:20
  • 1
    It won't work if you missed requestLegacyExternalStorage="true" on the target device api version is greater than 28+ – Codemaker2015 Feb 03 '21 at 07:33
  • 2
    This is not good as it is just a temporary solution according to documentation as well. Use MediaStore and ContentValues – Undefined function Apr 26 '21 at 16:02
  • 1
    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 32 – tuxdost Sep 02 '22 at 15:40
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.

  • 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? – Bhavin Patel Jul 05 '21 at 13:12
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()

oiyio
  • 5,219
  • 4
  • 42
  • 54