MediaStore.Images.Media.insertImage deprecated
Asked Answered
H

6

47

I used to save images using MediaStore.Images.Media.insertImage but insertImage method is now deprecated. The docs say:

This method was deprecated in API level 29. inserting of images should be performed using MediaColumns#IS_PENDING, which offers richer control over lifecycle.

I don't really get it, since MediaColumns.IS_PENDING is just a flag, how am I supposed to use it?

Should I use ContentValues ?

Hypercorrection answered 30/8, 2019 at 12:36 Comment(4)
Call insert() with a ContentValues to get a Uri that you can use for writing out your content. For the IS_PENDING stuff, your insert() call would have IS_PENDING set to 1. Then, after you write out the content, you would update() the item with IS_PENDING set to 0. See this code snippet for an example, though in my case I am saving a video, not an image.Insalivate
Question: What is happening in this piece of code: uri?.let { resolver.openOutputStream(uri)?.use { outputStream -> val sink = Okio.buffer(Okio.sink(outputStream)) response.body()?.source()?.let { sink.writeAll(it) } sink.close() }Hypercorrection
In my case, I am downloading a video from a URL to the device. response.body().source() gives me an Okio Source representing the bytes of the video that I am downloading. Okio.buffer(Okio.sink(outputStream)) gives me an Okio Sink representing where I am writing the bytes to, and writeAll() writes all the bytes from the Source to the Sink. See this SO answer for the Square-approved approach.Insalivate
check my answer here https://mcmap.net/q/74982/-how-to-take-a-screenshot-of-a-current-activity-and-then-share-itPeltate
H
18

SOLVED

The code suggested from @CommonsWare has no problem, except the fact that if you are programming with targetSdkVersion 29, you must add the condition:

val contentValues = ContentValues().apply {
            put(MediaStore.MediaColumns.DISPLAY_NAME, System.currentTimeMillis().toString())
            put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { //this one
                put(MediaStore.MediaColumns.RELATIVE_PATH, relativeLocation)
                put(MediaStore.MediaColumns.IS_PENDING, 1)
            }
        }
Hypercorrection answered 31/8, 2019 at 11:13 Comment(4)
How do you use those content values after that?Ethnogeny
check out this clas I used the exact same code cuz it's this use case: github.com/coroutineDispatcher/pocket_treasure/blob/master/…Hypercorrection
What happen if I didn't set MediaStore.MediaColumns.IS_PENDING to 1?Bernice
The file you wrote won't be accessible to other apps.Ruyter
A
20

This method was deprecated in API level 29. inserting of images should be performed using MediaColumns#IS_PENDING, which offers richer control over the lifecycle.

Either I'm too stupid to understand the docs or the Google team really needs to refactor the documentation.

Anyways, posting the complete answer from the links provided by CommonsWare and coroutineDispatcher

Step 1: Decide on which API level you are

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) saveImageInQ(imageBitMap)
    else saveImageInLegacy(imageBitMap)

Step 2: Save the image in Q style

//Make sure to call this function on a worker thread, else it will block main thread
fun saveImageInQ(bitmap: Bitmap):Uri {   
    val filename = "IMG_${System.currentTimeMillis()}.jpg"
    var fos: OutputStream? = null
    val imageUri: Uri? = null
    val contentValues = ContentValues().apply {
        put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
        put(MediaStore.MediaColumns.MIME_TYPE, "image/jpg")
        put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
        put(MediaStore.Video.Media.IS_PENDING, 1)
    }

    //use application context to get contentResolver
    val contentResolver = application.contentResolver

    contentResolver.also { resolver ->               
        imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
        fos = imageUri?.let { resolver.openOutputStream(it) }
    }

    fos?.use { bitmap.compress(Bitmap.CompressFormat.JPEG, 70, it) }

    contentValues.clear()
    contentValues.put(MediaStore.Video.Media.IS_PENDING, 0)
    resolver.update(imageUri, contentValues, null, null)
          
    return imageUri
}

Step 3: If not on Q save the image in legacy style

//Make sure to call this function on a worker thread, else it will block main thread
fun saveTheImageLegacyStyle(bitmap:Bitmap){
    val imagesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
    val image = File(imagesDir, filename)
    fos = FileOutputStream(image)
    fos?.use {bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it)}
}

This should get you rolling!

Antananarivo answered 26/3, 2021 at 12:41 Comment(3)
in the Step 2, resolver variable is missing.. would you update the code please?Scissor
resolver is the contentResolver object you get from your applicationContextAntananarivo
thanks for the completed code....i use your function saveImageInQ and its working like charmCrowther
H
18

SOLVED

The code suggested from @CommonsWare has no problem, except the fact that if you are programming with targetSdkVersion 29, you must add the condition:

val contentValues = ContentValues().apply {
            put(MediaStore.MediaColumns.DISPLAY_NAME, System.currentTimeMillis().toString())
            put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { //this one
                put(MediaStore.MediaColumns.RELATIVE_PATH, relativeLocation)
                put(MediaStore.MediaColumns.IS_PENDING, 1)
            }
        }
Hypercorrection answered 31/8, 2019 at 11:13 Comment(4)
How do you use those content values after that?Ethnogeny
check out this clas I used the exact same code cuz it's this use case: github.com/coroutineDispatcher/pocket_treasure/blob/master/…Hypercorrection
What happen if I didn't set MediaStore.MediaColumns.IS_PENDING to 1?Bernice
The file you wrote won't be accessible to other apps.Ruyter
C
6

Sum up of all the answers, but also refactored versions of each one.

You can skip the additions to AndroidManifest.xml and filepaths.xml if your app already uses a file provider.

Update (12/2022)

I had to replace getExternalStoragePublicDirectory(DIRECTORY_PICTURES) with applicationContext.getExternalFilesDir(DIRECTORY_PICTURES) as it seems to be crashing in earlier versions of Android. Please be sure you provide the root directory in filepaths.xml.

Save as PNG
/**
 * Saves a bitmap as a PNG file.
 *
 * Note that `.png` extension is added to the filename.
 */
fun Bitmap.saveAsPNG(filename: String) = "$filename.png".let { name ->
    if (SDK_INT < Q) {
        @Suppress("DEPRECATION")
        val file = File(applicationContext.getExternalFilesDir(DIRECTORY_PICTURES), name)
        FileOutputStream(file).use { compress(PNG, 100, it) }
        MediaScannerConnection.scanFile(applicationContext,
            arrayOf(file.absolutePath), null, null)
        FileProvider.getUriForFile(applicationContext,
            "${ applicationContext.packageName }.provider", file)
    } else {
        val values = ContentValues().apply {
            put(DISPLAY_NAME, name)
            put(MIME_TYPE, "image/png")
            put(RELATIVE_PATH, DIRECTORY_DCIM)
            put(IS_PENDING, 1)
        }

        val resolver = applicationContext.contentResolver
        val uri = resolver.insert(EXTERNAL_CONTENT_URI, values)
        uri?.let { resolver.openOutputStream(it) }
            ?.use { compress(PNG, 100, it) }

        values.clear()
        values.put(IS_PENDING, 0)
        uri?.also {
            resolver.update(it, values, null, null) }
    }
}
Save as JPG
/**
 * Saves a bitmap as a Jpeg file.
 *
 * Note that `.jpg` extension is added to the filename.
 */
fun Bitmap.saveAsJPG(filename: String) = "$filename.jpg".let { name ->
    if (SDK_INT < Q)
        @Suppress("DEPRECATION")
        FileOutputStream(File(applicationContext.getExternalFilesDir(DIRECTORY_PICTURES), name))
            .use { compress(JPEG, 100, it) }
    else {
        val values = ContentValues().apply {
            put(DISPLAY_NAME, name)
            put(MIME_TYPE, "image/jpg")
            put(RELATIVE_PATH, DIRECTORY_PICTURES)
            put(IS_PENDING, 1)
        }

        val resolver = applicationContext.contentResolver
        val uri = resolver.insert(EXTERNAL_CONTENT_URI, values)
        uri?.let { resolver.openOutputStream(it) }
            ?.use { compress(JPEG, 70, it) }

        values.clear()
        values.put(IS_PENDING, 0)
        uri?.also {
            resolver.update(it, values, null, null) }
    }
}
AndroidManifest.xml
<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.fileProvider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/filepaths" />
</provider>
res/xml/filepaths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths>
    <cache-path
        name="whatever"
        path="/" />
</paths>
Imports (in case you are missing something)
import android.content.ContentValues
import android.graphics.Bitmap
import android.graphics.Bitmap.CompressFormat.JPEG
import android.graphics.Bitmap.CompressFormat.PNG
import android.media.MediaScannerConnection
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.Q
import android.os.Environment.DIRECTORY_DCIM
import android.os.Environment.DIRECTORY_PICTURES
import android.os.Environment.getExternalStoragePublicDirectory
import android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI
import android.provider.MediaStore.MediaColumns.DISPLAY_NAME
import android.provider.MediaStore.MediaColumns.MIME_TYPE
import android.provider.MediaStore.MediaColumns.RELATIVE_PATH
import android.provider.MediaStore.Video.Media.IS_PENDING
import androidx.core.content.FileProvider
import java.io.File
import java.io.FileOutputStream
Continuo answered 31/8, 2022 at 10:29 Comment(1)
is there java version?Hinckley
H
3

Thanks for contributing iCantC on Step 2: Save the image in Q style.

I ran into some issues with memory usage in Android Studio, which I had to open Sublime to fix. To fix this error:

e: java.lang.OutOfMemoryError: Java heap space

This is the code I used as my use case is for PNG images, any value of bitmap.compress less than 100 is likely not useful. Previous version would not work on API 30 so I updated contentValues RELATIVE_PATH to DIRECTORY_DCIM also contentResolver.insert(EXTERNAL_CONTENT_URI, ...

   
    private val dateFormatter = SimpleDateFormat(
        "yyyy.MM.dd 'at' HH:mm:ss z", Locale.getDefault()
    )
    private val legacyOrQ: (Bitmap) -> Uri = { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
        saveImageInQ(it) else legacySave(it) }
    
    private fun saveImageInQ(bitmap: Bitmap): Uri {
        val filename = "${title}_of_${dateFormatter.format(Date())}.png"
        val fos: OutputStream?
        val contentValues = ContentValues().apply {
            put(DISPLAY_NAME, filename)
            put(MIME_TYPE, "image/png")
            put(RELATIVE_PATH, DIRECTORY_DCIM)
            put(IS_PENDING, 1)
        }

        //use application context to get contentResolver
        val contentResolver = applicationContext.contentResolver
        val uri = contentResolver.insert(EXTERNAL_CONTENT_URI, contentValues)
        uri?.let { contentResolver.openOutputStream(it) }.also { fos = it }
        fos?.use { bitmap.compress(Bitmap.CompressFormat.PNG, 100, it) }
        fos?.flush()
        fos?.close()

        contentValues.clear()
        contentValues.put(IS_PENDING, 0)
        uri?.let {
            contentResolver.update(it, contentValues, null, null)
        }
        return uri!!
    }

Step 3: If not on Q save the image in legacy style

private fun legacySave(bitmap: Bitmap): Uri {
        val appContext = applicationContext
        val filename = "${title}_of_${dateFormatter.format(Date())}.png"
        val directory = getExternalStoragePublicDirectory(DIRECTORY_PICTURES)
        val file = File(directory, filename)
        val outStream = FileOutputStream(file)
        bitmap.compress(Bitmap.CompressFormat.PNG, 100, outStream)
        outStream.flush()
        outStream.close()
        MediaScannerConnection.scanFile(appContext, arrayOf(file.absolutePath),
            null, null)
        return FileProvider.getUriForFile(appContext, "${appContext.packageName}.provider",
            file)
    }

Step 4: Create a custom FileProvider

package com.example.background.workers.provider

import androidx.core.content.FileProvider

class WorkerFileProvider : FileProvider() {

}

step 5: update your AndroidManifest.xml

changes from

<activity android:name=".MyActivity" />

to

<activity android:name=".MyActivity">
            <intent-filter>
                <action android:name="android.intent.action.PICK"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.OPENABLE"/>
                <data android:mimeType="image/png"/>
            </intent-filter>
        </activity>
        <provider
            android:name=".workers.provider.WorkerFileProvider"
            android:authorities="${applicationId}.provider"
            android:exported="false"
            android:grantUriPermissions="true"
            android:permission="android.permission.MANAGE_DOCUMENTS">
            <intent-filter>
                <action android:name="android.content.action.DOCUMENTS_PROVIDER"/>
            </intent-filter>
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths"/>
        </provider>

step 6: add a resource under xml for FILE_PROVIDER_PATHS in my case I needed the pictures folder

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="pictures" path="Pictures"/>
</paths>
Hippodrome answered 12/5, 2021 at 17:30 Comment(0)
H
1

I am adding a second answer as I am not sure if anyone cares about version checks but if you do their are more steps, hmm... Starting from

step 5: update your AndroidManifest.xml

changes from

<activity android:name=".MyActivity" />

to

        <activity android:name=".legacy.LegacyMyActivity"/>
        <activity android:name=".MyActivity" />
        <provider
            android:name=".workers.provider.WorkerFileProvider"
            android:authorities="${applicationId}.provider"
            android:exported="false"
            android:grantUriPermissions="true"
            android:enabled="@bool/atMostKitkat"
            android:permission="android.permission.MANAGE_DOCUMENTS">
            <intent-filter>
                <action android:name="android.content.action.DOCUMENTS_PROVIDER"/>
            </intent-filter>
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths"/>
        </provider>
        <activity-alias android:name=".legacy.LegacyMyActivity"
            android:targetActivity=".MyActivity"
            android:enabled="@bool/atMostJellyBeanMR2">
            <intent-filter>
                <action android:name="android.intent.action.PICK" />
                <category android:name="android.intent.category.OPENABLE" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="image/png" />
            </intent-filter>
        </activity-alias>

step 6: add a resource under xml for FILE_PROVIDER_PATHS is same as my previous answer

step 7: add a resource under res/values for bool.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <bool name="atMostJellyBeanMR2">true</bool>
    <bool name="atMostKitkat">false</bool>
</resources>

step 8: and another under res/values-v19

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <bool name="atMostJellyBeanMR2">false</bool>
    <bool name="atMostKitkat">true</bool>
</resources>

step 9: finally if you need to view the saved file so the important change is actionView.addFlags(FLAG_GRANT_READ_URI_PERMISSION)

   binding.seeFileButton.setOnClickListener {
        viewModel.outputUri?.let { currentUri ->
                 val actionView = Intent(Intent.ACTION_VIEW, currentUri)
                 actionView.addFlags(FLAG_GRANT_READ_URI_PERMISSION)
                 actionView.resolveActivity(packageManager)?.run {
                    startActivity(actionView)
             }
        }
   }
Hippodrome answered 15/5, 2021 at 15:54 Comment(1)
some would argue that version check is an important consideration, which I agree but just don't know by how much. Also I wanted to keep my original answer a little shortHippodrome
M
0
fun saveImage29(bitmap: Bitmap){
        val insertUri =
            contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, ContentValues())
        try {
            val outputStream = insertUri?.let { contentResolver.openOutputStream(it, "rw") }
            if (bitmap.compress(Bitmap.CompressFormat.JPEG,90,outputStream)){
                toast("保存成功")
            }else{
                toast("保存失败")
            }
        }catch (e:FileNotFoundException){
            e.printStackTrace()
        }

    }
Marlomarlon answered 25/8, 2023 at 9:31 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Socket

© 2022 - 2024 — McMap. All rights reserved.