apksherlock
apksherlock

Reputation: 8371

MediaStore.Images.Media.insertImage deprecated

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 ?

Upvotes: 47

Views: 30015

Answers (6)

tao.liu
tao.liu

Reputation: 1

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

    }

Upvotes: 0

rtsketo
rtsketo

Reputation: 1246

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

Upvotes: 6

iCantC
iCantC

Reputation: 3180

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!

Upvotes: 20

Manav Brar
Manav Brar

Reputation: 51

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

Upvotes: 1

Manav Brar
Manav Brar

Reputation: 51

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>

Upvotes: 3

apksherlock
apksherlock

Reputation: 8371

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

Upvotes: 18

Related Questions