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.

petrnohejl
  • 7,581
  • 3
  • 51
  • 63
multimodcrafter
  • 343
  • 1
  • 2
  • 7
  • 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. – CommonsWare Jul 15 '19 at 10:48
  • @CommonsWare 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. – multimodcrafter Jul 22 '19 at 11:20
  • @multimodcrafter 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, – Bao Lei Jul 29 '19 at 02:15
  • 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. – Bao Lei Jul 29 '19 at 02:57
  • @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? – David Santiago Turiño Jul 30 '19 at 15:40
  • Ok, got it finally working by creating the outputstream for the file with the descriptor returned by `resolver.openFileDescriptor(uri, "w", null)` – David Santiago Turiño Jul 30 '19 at 16:02
  • 1
    @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: https://stackoverflow.com/questions/36624756/how-to-save-bitmap-to-android-gallery/57265702?fbclid=IwAR3ao3-EXYztWeEfOhIaFE553MX74yC6o-Smi2A38kwlI6MrHpm74yTC4IU#57265702 – Bao Lei Jul 30 '19 at 20:55
  • Hi how do i get the filepath from the uri in Android Q ? Has someone figured this out ? Any help/info would be very nice – Frank Aug 05 '19 at 00:12
  • @Frank 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? – multimodcrafter Aug 05 '19 at 06:50
  • @multimodcrafter 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 https://developer.android.com/guide/topics/providers/document-provider . i am testing the getBitmapFromUri function with works fine with media.store too – Frank Aug 06 '19 at 01:01
  • @Frank well the uri can be used directly with an image view as described [here](https://developer.android.com/reference/android/widget/ImageView#setImageURI(android.net.Uri)) – multimodcrafter Aug 06 '19 at 19:49

4 Answers4

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.

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • 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. – Chenhe Mar 13 '20 at 14:36
  • @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. – CommonsWare Mar 13 '20 at 14:46
  • @CommonsWare 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()` – Hossam Hassan May 08 '20 at 11:15
  • @HossamHassan: Correct. `RELATIVE_PATH` is not available on Android 9 and older, so I have to use different techniques. – CommonsWare May 08 '20 at 11:30
  • After reading many questions, and digging @CommonsWare 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. – superjos Feb 08 '21 at 18:33
  • 1
    @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. – CommonsWare Feb 08 '21 at 19:00
  • 1
    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()`) – superjos Feb 09 '21 at 09:31
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();
    

}
Ismail Osunlana
  • 434
  • 5
  • 9
  • Didn't work for me - still got Primary directory "name_directory" not allowed for content://media/external/images/media; allowed directories are [DCIM, Pictures]. – TinaFrieda Aug 19 '22 at 11:03
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)
        }
    }
}
Alex Shevelev
  • 681
  • 7
  • 14
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.

biqqles
  • 303
  • 3
  • 10