33

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 ?

mikejonesguy
  • 8,919
  • 2
  • 33
  • 49
coroutineDispatcher
  • 6,476
  • 5
  • 27
  • 54
  • 3
    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](https://gitlab.com/commonsguy/cw-android-q/blob/v0.5/ConferenceVideos/src/main/java/com/commonsware/android/conferencevideos/VideoRepository.kt#L69-102) for an example, though in my case I am saving a video, not an image. – CommonsWare Aug 30 '19 at 12:41
  • 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() }` – coroutineDispatcher Aug 30 '19 at 12:54
  • 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](https://stackoverflow.com/a/29012988/115145) for the Square-approved approach. – CommonsWare Aug 30 '19 at 13:05
  • check my answer here https://stackoverflow.com/a/68110559/6039240 – Amr Jun 24 '21 at 06:26

4 Answers4

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)
            }
        }
coroutineDispatcher
  • 6,476
  • 5
  • 27
  • 54
11

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!

AmrDeveloper
  • 2,559
  • 1
  • 16
  • 25
iCantC
  • 2,262
  • 1
  • 13
  • 28
2

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>
Manav Brar
  • 41
  • 3
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)
             }
        }
   }
Manav Brar
  • 41
  • 3
  • 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 short – Manav Brar May 15 '21 at 16:17