How to save an image in Android Q using MediaStore?
android, image, java, kotlin, mediastore
Solution
Try the next method. Android Q (and above) already takes care of creating the folders if they don’t exist. The example is hard-coded to output into the DCIM folder. If you need a sub-folder then append the sub-folder name as next:
final String relativeLocation = Environment.DIRECTORY_DCIM + File.separator + “YourSubforderName”;
Consider that the compress format should be related to the mime-type parameter. For example, with a JPEG compress format the mime-type would be "image/jpeg", and so on. Probably you may also want to pass the compress quality as a parameter, in this example is hardcoded to 95.
Java:
@NonNull
public Uri saveBitmap(@NonNull final Context context, @NonNull final Bitmap bitmap,
@NonNull final Bitmap.CompressFormat format,
@NonNull final String mimeType,
@NonNull final String displayName) throws IOException {
final ContentValues values = new ContentValues();
values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName);
values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType);
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM);
final ContentResolver resolver = context.getContentResolver();
Uri uri = null;
try {
final Uri contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
uri = resolver.insert(contentUri, values);
if (uri == null)
throw new IOException("Failed to create new MediaStore record.");
try (final OutputStream stream = resolver.openOutputStream(uri)) {
if (stream == null)
throw new IOException("Failed to open output stream.");
if (!bitmap.compress(format, 95, stream))
throw new IOException("Failed to save bitmap.");
}
return uri;
}
catch (IOException e) {
if (uri != null) {
// Don't leave an orphan entry in the MediaStore
resolver.delete(uri, null, null);
}
throw e;
}
}
Kotlin:
@Throws(IOException::class)
fun saveBitmap(
context: Context, bitmap: Bitmap, format: Bitmap.CompressFormat,
mimeType: String, displayName: String
): Uri {
val values = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
}
val resolver = context.contentResolver
var uri: Uri? = null
try {
uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
?: throw IOException("Failed to create new MediaStore record.")
resolver.openOutputStream(uri)?.use {
if (!bitmap.compress(format, 95, it))
throw IOException("Failed to save bitmap.")
} ?: throw IOException("Failed to open output stream.")
return uri
} catch (e: IOException) {
uri?.let { orphanUri ->
// Don't leave an orphan entry in the MediaStore
resolver.delete(orphanUri, null, null)
}
throw e
}
}
Kotlin variant, with a more functional style:
@Throws(IOException::class)
fun saveBitmap(
context: Context, bitmap: Bitmap, format: Bitmap.CompressFormat,
mimeType: String, displayName: String
): Uri {
val values = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
}
var uri: Uri? = null
return runCatching {
with(context.contentResolver) {
insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)?.also {
uri = it // Keep uri reference so it can be removed on failure
openOutputStream(it)?.use { stream ->
if (!bitmap.compress(format, 95, stream))
throw IOException("Failed to save bitmap.")
} ?: throw IOException("Failed to open output stream.")
} ?: throw IOException("Failed to create new MediaStore record.")
}
}.getOrElse {
uri?.let { orphanUri ->
// Don't leave an orphan entry in the MediaStore
context.contentResolver.delete(orphanUri, null, null)
}
throw it
}
}
Problem
Here is a link to the new Android Q Scoped Storage. According to this Android Developers Best Practices Blog, `storing shared media files` (which is my case) should be done using the MediaStore API. Digging into the docs and I cannot find a relevant function. Here is my trial in Kotlin: ``` val bitmap = getImageBitmap() // I have a bitmap from a function or callback or whatever val name = "example.png" // I have a name val picturesDirectory = getExternalFilesDir(Environment.DIRECTORY_PICTURES)!! // Make sure the directory "Android/data/com.mypackage.etc/files/Pictures" exists if (!picturesDirectory.exists()) { picturesDirectory.mkdirs() } try { val out = FileOutputStream(File(picturesDirectory, name)) bitmap.compress(Bitmap.CompressFormat.PNG, 100, out) out.flush() out.close() } catch(e: Exception) { // handle the error } ``` The result is that my image is saved here `Android/data/com.mypackage.etc/files/Pictures/example.png` as described in the Best Practices Blog as `Storing app-internal files` My question is: How to save an image using the MediaStore API? Answers in Java are equally acceptable. EDIT But there are 3 more points. Here is my code: ``` val name = "Myimage" val relativeLocation = Environment.DIRECTORY_PICTURES + File.pathSeparator + "AppName" val contentValues = ContentValues().apply { put(MediaStore.Images.ImageColumns.DISPLAY_NAME, name) put(MediaStore.MediaColumns.MIME_TYPE, "image/png") // without this part causes "Failed to create new MediaStore record" exception to be invoked (uri is null below) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { put(MediaStore.Images.ImageColumns.RELATIVE_PATH, relativeLocation) } } val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI var stream: OutputStream? = null var uri: Uri? = null try { uri = contentResolver.insert(contentUri, contentValues) if (uri == null) { throw IOException("Failed to create new MediaStore record.") } stream = contentResolver.openOutputStream(uri) if (stream == null) { throw IOException("Failed to get output stream.") } if (!bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)) { throw IOException("Failed to save bitmap.") } Snackbar.make(mCoordinator, R.string.image_saved_success, Snackbar.LENGTH_INDEFINITE).setAction("Open") { val intent = Intent() intent.type = "image/*" intent.action = Intent.ACTION_VIEW intent.data = contentUri startActivity(Intent.createChooser(intent, "Select Gallery App")) }.show() } catch(e: IOException) { if (uri != null) { contentResolver.delete(uri, null, null) } throw IOException(e) } finally { stream?.close() } ``` 1- The image saved doesn't get its correct name "Myimage.png" I tried using "Myimage" and "Myimage.PNG" but neither worked. The image always gets a name made up of numbers like: `1563468625314.jpg` Which bring us to the second problem: 2- The image is saved as `jpg` even though I compress the bitmap in the format of `png`. Not a big issue. Just curious why. 3- The relativeLocation bit causes an exception on Devices less than Android Q. After surrounding with the "Android Version Check" if statement, the images are saved directly in the root of the `Pictures` folder. EDIT 2 Changed to: ``` uri = contentResolver.insert(contentUri, contentValues) if (uri == null) { throw IOException("Failed to create new MediaStore record.") } val cursor = contentResolver.query(uri, null, null, null, null) DatabaseUtils.dumpCursor(cursor) cursor!!.close() stream = contentResolver.openOutputStream(uri) ``` Here are the logs ``` I/System.out: >>>>> Dumping cursor android.content.ContentResolver$CursorWrapperInner@76da9d1 I/System.out: 0 { I/System.out: _id=25417 I/System.out: _data=/storage/emulated/0/Pictures/1563640732667.jpg I/System.out: _size=null I/System.out: _display_name=Myimage I/System.out: mime_type=image/png I/System.out: title=1563640732667 I/System.out: date_added=1563640732 I/System.out: is_hdr=null I/System.out: date_modified=null I/System.out: description=null I/System.out: picasa_id=null I/System.out: isprivate=null I/System.out: latitude=null I/System.out: longitude=null I/System.out: datetaken=null I/System.out: orientation=null I/System.out: mini_thumb_magic=null I/System.out: bucket_id=-1617409521 I/System.out: bucket_display_name=Pictures I/System.out: width=null I/System.out: height=null I/System.out: is_hw_privacy=null I/System.out: hw_voice_offset=null I/System.out: is_hw_favorite=null I/System.out: hw_image_refocus=null I/System.out: album_sort_index=null I/System.out: bucket_display_name_alias=null I/System.out: is_hw_burst=0 I/System.out: hw_rectify_offset=null I/System.out: special_file_type=0 I/System.out: special_file_offset=null I/System.out: cam_perception=null I/System.out: cam_exif_flag=null I/System.out: } I/System.out: <<<<< ``` I noticed the `title` to be matching the name so I tried adding: `put(MediaStore.Images.ImageColumns.TITLE, name)` It still didn't work and here are the new logs: ``` I/System.out: >>>>> Dumping cursor android.content.ContentResolver$CursorWrapperInner@51021a5 I/System.out: 0 { I/System.out: _id=25418 I/System.out: _data=/storage/emulated/0/Pictures/1563640934803.jpg I/System.out: _size=null I/System.out: _display_name=Myimage I/System.out: mime_type=image/png I/System.out: title=Myimage I/System.out: date_added=1563640934 I/System.out: is_hdr=null I/System.out: date_modified=null I/System.out: description=null I/System.out: picasa_id=null I/System.out: isprivate=null I/System.out: latitude=null I/System.out: longitude=null I/System.out: datetaken=null I/System.out: orientation=null I/System.out: mini_thumb_magic=null I/System.out: bucket_id=-1617409521 I/System.out: bucket_display_name=Pictures I/System.out: width=null I/System.out: height=null I/System.out: is_hw_privacy=null I/System.out: hw_voice_offset=null I/System.out: is_hw_favorite=null I/System.out: hw_image_refocus=null I/System.out: album_sort_index=null I/System.out: bucket_display_name_alias=null I/System.out: is_hw_burst=0 I/System.out: hw_rectify_offset=null I/System.out: special_file_type=0 I/System.out: special_file_offset=null I/System.out: cam_perception=null I/System.out: cam_exif_flag=null I/System.out: } I/System.out: <<<<< ``` And I can't change `date_added` to a name. And `MediaStore.MediaColumns.DATA` is deprecated.
Related problems
- How to take a screenshot of a current Activity and then share it?
- When should I use File.separator and when File.pathSeparator?
- Rename file of the Mediastore which is created by app in android 10. Working on Android API 30 but shows error in API 29
- How to save Document file in external storage after android API LEVEL28 With Android SAF(Storage Access Framework))