How to save an image in a subdirectory on android Q whilst remaining backwards compatible
Asked Answered
I

4

24

I'm creating a simple image editor app and therefore need to load and save image files. I'd like the saved files to appear in the gallery in a separate album. From Android API 28 to 29, there have been drastic changes to what extent an app is able to access storage. I'm able to do what I want in Android Q (API 29) but that way is not backwards compatible.

When I want to achieve the same result in lower API versions, I have so far only found way's, which require the use of deprecated code (as of API 29).

These include:

  • the use of the MediaStore.Images.Media.DATA column
  • getting the file path to the external storage via Environment.getExternalStoragePublicDirectory(...)
  • inserting the image directly via MediaStore.Images.Media.insertImage(...)

My question is: is it possible to implement it in such a way, so it's backwards compatible, but doesn't require deprecated code? If not, is it okay to use deprecated code in this situation or will these methods soon be deleted from the sdk? In any case it feels very bad to use deprecated methods so I'd rather not :)

This is the way I found which works with API 29:

ContentValues values = new ContentValues();
String filename = System.currentTimeMillis() + ".jpg";

values.put(MediaStore.Images.Media.TITLE, filename);
values.put(MediaStore.Images.Media.DISPLAY_NAME, filename);
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis() / 1000);
values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis());
values.put(MediaStore.Images.Media.RELATIVE_PATH, "PATH/TO/ALBUM");

getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,values);

I then use the URI returned by the insert method to save the bitmap. The Problem is that the field RELATIVE_PATH was introduced in API 29 so when I run the code on a lower version, the image is put into the "Pictures" folder and not the "PATH/TO/ALBUM" folder.

Incandescent answered 14/7, 2019 at 20:36 Comment(11)
My guess is that you will need to use two different storage strategies, one for API Level 29+ and one for older devices. "is it okay to use deprecated code in this situation" -- yes. "Deprecated" for stuff like this means "we want you to use other things". If you are using those "other things" on newer Android versions, your code running on older Android versions can use the deprecated APIs without a problem. Very rarely do classes and methods get removed from the SDK such that your code would no longer build, and I do not expect that to happen for any of your options.Humoresque
@Humoresque Thank you for your thoughts, this is exactly what I'm doing now and I guess it's the only way which works. If you want you can convert your comment to an answer and I will accept it.Incandescent
@Incandescent How did you get the image saving working? The uri from insert method is basically something like content://media/external/images/media/123 I couldn't get a file path from that. Do you mind posting the code? Thanks,Chamois
Actually, I just figured this out. Thank @multimodcrafter, without your post I wouldn't be able to save a picture into gallery in Android Q.Chamois
@BaoLei I came across the same problem that you had regarding the output of insert pointing to content://media/external/images/etc. What did you do?Yippee
Ok, got it finally working by creating the outputstream for the file with the descriptor returned by resolver.openFileDescriptor(uri, "w", null)Yippee
@DavidSantiagoTuriño Nice. I created a stream directly based on the uri using contentResolver.openOutputStream(uri) , I guess under the hood it's the same as your approach. My code is documented here: #36625256Chamois
Hi how do i get the filepath from the uri in Android Q ? Has someone figured this out ? Any help/info would be very niceFrederigo
@Frederigo AFAIK you can't get a filepath since the uri might be pointing to some other kind of storage (e.g. google drive). If you wan't to access the file you can use the contentresolver's openOutputstream method. What do you need the path for?Incandescent
@Incandescent To set the image into an imageview ! So if i have the uri i have always access to the uri under Android Q ? So i could use it in my app (save the uris to a database again) and use it maybe for an image slider ? I'm still testing this. In my opinion openoutputstream is maybe not a good solution because of memory errors ?! Also as described in document provider here developer.android.com/guide/topics/providers/document-provider . i am testing the getBitmapFromUri function with works fine with media.store tooFrederigo
@Frederigo well the uri can be used directly with an image view as described hereIncandescent
H
7

is it okay to use deprecated code in this situation or will these methods soon be deleted from the sdk?

The DATA option will not work on Android Q, as that data is not included in query() results, even if you ask for it you cannot use the paths returned by it, even if they get returned.

The Environment.getExternalStoragePublicDirectory(...) option will not work by default on Android Q, though you can add a manifest entry to re-enable it. However, that manifest entry may be removed in Android R, so unless you are short on time, I would not go this route.

AFAIK, MediaStore.Images.Media.insertImage(...) still works, even though it is deprecated.

is it possible to implement it in such a way, so it's backwards compatible, but doesn't require deprecated code?

My guess is that you will need to use two different storage strategies, one for API Level 29+ and one for older devices. I took that approach in this sample app, though there I am working with video content, not images, so insertImage() was not an option.

Humoresque answered 22/7, 2019 at 11:36 Comment(7)
After actual testing( Samsung galaxy s10), DATA column is still available on Android Q. I think you misunderstood the meaning of the document. It says you can not access the file using file path, instead of not being able to get the path itself.Emancipate
@Chenhe: "DATA column is still available on Android Q" -- it's also possible that the rules changed sometime between when I wrote this and when Android 10 shipped, or that my answer was based on earlier developer previews of Android Q.Humoresque
@Humoresque but the sample you just mentioned is using two different approaches to save files/ the old approach which is inside downloadLegacy() and the new approach for android Q and above which is in download()Staircase
@HossamHassan: Correct. RELATIVE_PATH is not available on Android 9 and older, so I have to use different techniques.Humoresque
After reading many questions, and digging @Humoresque example: is it correct to say that on Android 9 and older, one cannot save a video within its own sub-directory under Videos directory? So one can only save a video directly within Videos directory.Giro
@superjos: On Android 9 and older, you should be able to use Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES) to get the Videos directory, then create a subdirectory under there. My sample happens to download directly to Videos, because I was lazy when I wrote the sample.Humoresque
Thanks for helping. Just to others that might overlook as I did: Environment.DIRECTORY_MOVIES might as well not exist yet, so when busy creating your custom subdirectory (mySubDir.mkdir()) one should make sure the whole subtree exists (mySubDir.mkdirs())Giro
N
3

This is the code that works for me. This code saves an image to a subdirectory folder on your phone. It checks the android version of the phone, if its above android q, it runs the required codes and if its below, it runs the code in the else statement.

Source: https://androidnoon.com/save-file-in-android-10-and-below-using-scoped-storage-in-android-studio/

 private void saveImageToStorage(Bitmap bitmap) throws IOException {
    OutputStream imageOutStream;
   
     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {

        ContentValues values = new ContentValues();
        values.put(MediaStore.Images.Media.DISPLAY_NAME, 
        "image_screenshot.jpg");
        values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
        values.put(MediaStore.Images.Media.RELATIVE_PATH, 
         Environment.DIRECTORY_PICTURES + File.pathSeparator + "AppName");

        Uri uri = 
     getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 
    values);

        imageOutStream = getContentResolver().openOutputStream(uri);

    } else {

        String imagesDir = 
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES). toString() + "/AppName";
        File image = new File(imagesDir, "image_screenshot.jpg");
        imageOutStream = new FileOutputStream(image);
    }

   
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, imageOutStream);
        imageOutStream.close();
    

}
Negrete answered 26/6, 2020 at 19:5 Comment(1)
Didn't work for me - still got Primary directory "name_directory" not allowed for content://media/external/images/media; allowed directories are [DCIM, Pictures].Rhubarb
S
1

For old API (<29) I place an image into the external media directory and scan it via MediaScannerConnection.

Let's see my code.

This function creates an image file. Pay attention to an appName variable - it's is a name of an album in which the image will be displayed.

override fun createImageFile(appName: String): File {
    val dir = File(appContext.externalMediaDirs[0], appName)
    if(!dir.exists()) {
        ir.mkdir()
    }

    return File(dir, createFileName())
}

Then, I place an image into the file, and, at last, I run a media scanner like this:

private suspend fun scanNewFile(shot: File): Uri? {
    return suspendCancellableCoroutine { continuation ->
        MediaScannerConnection.scanFile(
            appContext, 
            arrayOf<String>(shot.absolutePath), 
            arrayOf(imageMimeType)) { _, uri -> continuation.resume(uri)
        }
    }
}
Staphyloplasty answered 22/9, 2020 at 19:31 Comment(0)
K
1

After some trial and error, I discovered that it is possible to use MediaStore in a backwards compatible way, such that as much code as possible is shared between the implementations for different versions. The only trick is to remember that if you use MediaColumns.DATA, you need to create the file yourself.

Let's look at the code from my project (Kotlin). This example is for saving audio, not images, but you only need to substitute MIME_TYPE and DIRECTORY_MUSIC for whatever you require.

private fun newFile(): FileDescriptor? {
    // Create a file descriptor for a new recording.
    val date = DateFormat.getDateTimeInstance().format(Calendar.getInstance().time)
    val filename = "$date.mp3"

    val values = ContentValues().apply {
        put(MediaColumns.TITLE, date)
        put(MediaColumns.MIME_TYPE, "audio/mp3")

        // store the file in a subdirectory
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            put(MediaColumns.DISPLAY_NAME, filename)
            put(MediaColumns.RELATIVE_PATH, saveTo)
        } else {
            // RELATIVE_PATH was added in Q, so work around it by using DATA and creating the file manually
            @Suppress("DEPRECATION")
            val music = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).path

            with(File("$music/P2oggle/$filename")) {
                @Suppress("DEPRECATION")
                put(MediaColumns.DATA, path)

                parentFile!!.mkdir()
                createNewFile()
            }
        }
    }

    val uri = contentResolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values)!!
    return contentResolver.openFileDescriptor(uri, "w")?.fileDescriptor
}

On Android 10 and above, we use DISPLAY_NAME to set the filename and RELATIVE_PATH to set the subdirectory. On older versions, we use DATA and create the file (and its directory) manually. After this, the implementation for both is the same: we simply extract the file descriptor from MediaStore and return it for use.

Kaiserism answered 13/6, 2021 at 12:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.