How to save an image in Android Q using MediaStore?
JavaAndroidImageKotlinMediastoreJava Problem Overview
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.
Thanks in Advance!
EDIT
Thanks to PerracoLabs
That really helped!
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.
Another Thank you.
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.
Thanks in Advance!
Java Solutions
Solution 1 - Java
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
}
}
Solution 2 - Java
private void saveImage(Bitmap bitmap, @NonNull String name) throws IOException {
OutputStream fos;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ContentResolver resolver = getContentResolver();
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name + ".jpg");
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpg");
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);
Uri imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
fos = resolver.openOutputStream(Objects.requireNonNull(imageUri));
} else {
String imagesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).toString();
File image = new File(imagesDir, name + ".jpg");
fos = new FileOutputStream(image);
}
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
Objects.requireNonNull(fos).close();
}
Image will store in Pictures Folder @ root level
see in live https://youtu.be/695HqaiwzQ0 i created tutorial
Solution 3 - Java
This is what i always use. You can try it.
private void saveImageToStorage() throws IOException {
OutputStream imageOutStream;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, "image_screenshot.jpg");
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);
Uri uri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
imageOutStream = getContentResolver().openOutputStream(uri);
} else {
String imagePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).toString();
File image = new File(imagePath, "image_screenshotjpg");
imageOutStream = new FileOutputStream(image);
}
try {
bitmapObject.compress(Bitmap.CompressFormat.JPEG, 100, imageOutStream);
} finally {
imageOutStream.close();
}
}
Solution 4 - Java
If anyone is looking how to save a photo into the DCIM folder, in a way that will appear in Google Photos later: (based on: https://github.com/yasirkula/UnityNativeGallery/blob/670d9e2b8328f7796dd95d29dd80fadd8935b804/JAR%20Source/NativeGallery.java#L73-L96)
ContentValue values = new ContentValues();
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM);
values.put(MediaStore.MediaColumns.DATE_TAKEN, System.currentTimeMillis());
values.put(MediaStore.MediaColumns.IS_PENDING, true);
Uri uri = context.getContentResolver().insert(externalContentUri, values);
if (uri != null) {
try {
if (WriteFileToStream(originalFile, context.getContentResolver().openOutputStream(uri))) {
values.put(MediaStore.MediaColumns.IS_PENDING, false);
context.getContentResolver().update(uri, values, null, null);
}
} catch (Exception e) {
context.getContentResolver().delete( uri, null, null );
}
}
Where WriteFileToStream
is a standard method copying from file to stream.
Solution 5 - Java
Here is my version for 2022, this version was tested in Emulator SDK 27 and 30 also on Samsung S22 Phone.
TL:DR
For SDK < 29 you need following the code here, and need little add code after successfully take picture. You can see at my savePictureQ(...)
function below
Otherwise, if you are SDK >= 29 just pass the URI at MediaStore.EXTRA_OUTPUT
extras from contentResolver.insert(...)
function
Since startActivityForResult(Intent)
already deprecated my version using registerForActivityResult(...)
private val cameraLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
val name: String = viewModel.savePictureQ()
if (name != "") requireActivity().applicationContext.deleteFile(name)
val cr = requireContext().contentResolver
val uri = viewModel.getTargetUri()
if (uri != null) {
val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val source = ImageDecoder.createSource(cr, uri)
ImageDecoder.decodeBitmap(source)
} else MediaStore.Images.Media.getBitmap(cr, uri)
val resized = Bitmap.createScaledBitmap(bitmap, 512, 512, true)
}
}
}
I call the Intent in another file named Repository.kt, I also using fake viewModel to call Repository code. Here is how I call my viewModel code
private lateinit var viewModel: MenuViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
viewModel = MenuViewModel(Injection.provideRepository(requireContext()))
...
}
private fun permissionCheck() {
val granted = PackageManager.PERMISSION_GRANTED
val permissions = arrayOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.CAMERA
)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (ActivityCompat.checkSelfPermission(
requireContext(),
permissions[0]
) != granted && ActivityCompat.checkSelfPermission(
requireContext(),
permissions[1]
) != granted && ActivityCompat.checkSelfPermission(
requireContext(),
permissions[2]
) != granted
) ActivityCompat.requestPermissions(
requireActivity(), permissions, MainActivity.REQUEST_CODE_PERMISSION
) else MainActivity.accepted = true
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (ActivityCompat.checkSelfPermission(
requireContext(),
permissions[2]
) != granted && ActivityCompat.checkSelfPermission(
requireContext(),
Manifest.permission.ACCESS_MEDIA_LOCATION
) != granted
) ActivityCompat.requestPermissions(
requireActivity(),
arrayOf(permissions[2], Manifest.permission.ACCESS_MEDIA_LOCATION),
MainActivity.REQUEST_CODE_PERMISSION
) else MainActivity.accepted = true
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
bind.fromCamera.setOnClickListener {
permissionCheck()
if (`permission granted check`) {
viewModel.getCameraIntent(cameraLauncher)
}
}
...
}
in my fake viewModel:
class MenuViewModel(private val repository: IRepository) {
fun getTargetUri() = repository.getTargetUri()
fun getCameraIntent(launcher: ActivityResultLauncher<Intent>) =
repository.createTakePictureIntent(launcher)
fun savePictureQ(): String = repository.savePictureQ()
}
in my repository code:
class Repository private constructor(private val context: Context) : IRepository {
companion object {
@Volatile
private var INSTANCE: IRepository? = null
fun getInstance(context: Context) = INSTANCE ?: synchronized(this) {
INSTANCE ?: Repository(context).apply { INSTANCE = this }
}
}
private var currentPath = ""
private var targetUri: Uri? = null
private fun createImageFile(): File { // create temporary file for SDK < 29
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
return File.createTempFile(timestamp, ".jpg", storageDir)
.apply { currentPath = absolutePath }
}
override fun savePictureQ() : String { // Saving picture and added to Gallery for SDK < 29
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
val f = File(currentPath)
val cr = context.contentResolver
val bitmap = BitmapFactory.decodeFile(currentPath)
val path = "${Environment.DIRECTORY_PICTURES}${File.separator}PoCkDetection"
val values = createContentValues(f.name, path)
var uri: Uri? = null
try {
uri = cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)!!
val os = cr.openOutputStream(uri)
try {
val result = bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os)
if (!result) throw Exception()
} catch (e: Exception) {
e.printStackTrace()
throw e
} finally {
os?.close()
targetUri = uri
}
f.delete()
if (f.exists()) {
f.canonicalFile.delete()
if (f.exists()) return f.name
}
} catch (e: Exception) {
e.printStackTrace()
uri?.let {
cr.delete(it, null, null)
}
}
}
return ""
}
override fun getTargetUri(): Uri? = targetUri
private fun createContentValues(title: String, path: String): ContentValues =
ContentValues().apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.MediaColumns.TITLE, "$title.jpg")
put(MediaStore.MediaColumns.DISPLAY_NAME, "$title.jpg")
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpg")
put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis())
put(MediaStore.MediaColumns.DATE_TAKEN, System.currentTimeMillis())
put(MediaStore.MediaColumns.RELATIVE_PATH, path)
} else {
put(MediaStore.Images.Media.TITLE, "$title.jpg")
put(MediaStore.Images.Media.DISPLAY_NAME, "$title.jpg")
put(MediaStore.Images.Media.MIME_TYPE, "image/jpg")
put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis())
put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
}
}
override fun createTakePictureIntent(launcher: ActivityResultLauncher<Intent>) {
Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
takePictureIntent.resolveActivity(context.packageManager).also {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
val photoFile: File? = try {
createImageFile()
} catch (e: IOException) {
e.printStackTrace()
null
}
photoFile?.also {
val photoURI =
FileProvider.getUriForFile(context, "com.your.package.name", it)
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
launcher.launch(takePictureIntent)
}
} else {
val timestamp =
SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val path = "${Environment.DIRECTORY_PICTURES}${File.separator}PoCkDetection"
val values = createContentValues(timestamp, path)
val photoURI = context.contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values
)
targetUri = photoURI
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
launcher.launch(takePictureIntent)
}
}
}
}
}
For SDK < 29 I follow this code from Google Developer
this is how my manifest look after following the code:
<application ...>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.your.package.name"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/camera_paths" />
</provider>
</application>
make new res folder called xml, then make new xml file make sure the name same like you place on <meta-data>
in <provider>
and inside that file:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-files-path
name="camera_take"
path="Pictures" />
</paths>