Reputation: 8371
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
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
Reputation: 1246
You can skip the additions to AndroidManifest.xml
and filepaths.xml
if your app already uses a file provider.
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
.
/**
* 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) }
}
}
/**
* 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) }
}
}
<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>
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path
name="whatever"
path="/" />
</paths>
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
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
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
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
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