Reputation: 2485
I have a fun that saves bitmap as PNG or JPG (both not working), but seems like using content values not working as expected.
What am I missing ? Works on Android 10, but not working on Android 8
fun Bitmap.save(context: Context) {
val contentResolver = context.contentResolver
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, "test.png")
put(MediaStore.MediaColumns.TITLE, "test")
put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
put(MediaStore.MediaColumns.IS_PENDING, 1)
}
}
val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val uri = contentResolver.insert(contentUri, contentValues)
if (uri != null) {
try {
contentResolver.openFileDescriptor(uri, "w", null)?.use {
if (it.fileDescriptor != null) {
with(FileOutputStream(it.fileDescriptor)) {
compress(
Bitmap.CompressFormat.PNG,
DEFAULT_IMAGE_QUALITY,
this
)
flush()
close()
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
contentValues.clear()
contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0)
contentResolver.update(uri, contentValues, null, null)
}
MediaScannerConnection.scanFile(context, arrayOf(uri.toString()), null, null)
}
recycle()
}
Actual file name is 1592205828045 (some timestamp)
Actual file type is jpg with 0B - as it was not saved properly ?
Upvotes: 3
Views: 1193
Reputation: 397
You will have to maintain 2 different ways of saving images to shared storage. This post covers it quite well. Using Media Store API in older phones results in the problem you have described. Some code sample for you (tested in Android 8, 10, and 11).
Add these to your manifest
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- File save functions handles this -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"
tools:ignore="ScopedStorage" />
Add a permission check to your app (code not provided)
When you are ready with your bitmap call either of these functions (depending on the SDK version of the phone that the app is currently running on)
//TODO - bitmap needs null check
val bitmap = BitmapFactory.decodeFile(bitmapFile.canonicalPath)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
{
saveBitmapPreQ(bitmap)
} else {
saveBitmapPostQ(bitmap)
}
Finally these are the implementations of saveBitmapPreQ and saveBitmapPostQ
@Suppress("DEPRECATION") // Check is preformed on function call
private fun saveBitmapPreQ(thisBitmap: Bitmap){
Log.d("HOME_4", "in pre Q")
val pictureDirectory =
File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
"MyFolder")
if (!pictureDirectory.exists()){
pictureDirectory.mkdir()
}
val dateTimeStamp = SimpleDateFormat("yyyyMMddHHmmss").format(Date())
val name = "Image_$dateTimeStamp"
val bitmapFile = File(pictureDirectory, "$name.png")
try {
val fileOutputStream = bitmapFile.outputStream()
thisBitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream)
fileOutputStream.flush()
fileOutputStream.close()
} catch (e: Exception) {
Log.d("HOME_5", "Pre Q error $e")
}
}
private fun saveBitmapPostQ(thisBitmap: Bitmap){
Log.d("HOME_6", "in post Q")
val dateTimeStamp = SimpleDateFormat("yyyyMMddHHmmss").format(Date())
val name = "Image_$dateTimeStamp"
val relativePath = Environment.DIRECTORY_PICTURES + File.separator + "MyFolder"
val contentValues = ContentValues().apply {
put(MediaStore.Images.ImageColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
put(MediaStore.MediaColumns.TITLE, name)
put(MediaStore.Images.ImageColumns.RELATIVE_PATH, relativePath)
}
val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
var outputStream: OutputStream? = null
var uri: Uri? = null
try {
uri = contentResolver.insert(contentUri, contentValues)
if (uri == null){
throw IOException("Failed to create new MediaStore record.")
}
outputStream = contentResolver.openOutputStream(uri)
if (outputStream == null){
throw IOException("Failed to get output stream.")
}
if (!thisBitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)){
throw IOException("Failed to save bitmap.")
}
} catch (e: IOException){
if (uri != null)
{
contentResolver.delete(uri, null, null)
}
throw IOException(e)
}
finally {
outputStream?.close()
}
}
I have left log messages in there to help you understand the flow. In the saveBitmapPostQ funtions I have taken a few shortcuts. Please read this post under the headding Creating a New File on how you can improve that function further.
Upvotes: 6
Reputation: 1232
You are creating the file, but you still need to write your Bitmap
to it:
fun Bitmap.save(context: Context) {
...
val bitmap = this
val maxImageQuality = 100
val uri = contentResolver.insert(contentUri, contentValues)
if (uri != null) {
try {
contentResolver.openFileDescriptor(uri, "w", null)?.use {
if (it.fileDescriptor != null) {
with(FileOutputStream(it.fileDescriptor)) {
bitmap.compress(
Bitmap.CompressFormat.PNG,
maxImageQuality, this
)
flush()
close()
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
// release pending status of the file
contentValues.clear()
contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
contentResolver.update(uri, contentValues, null, null)
// notify media scanner there's a new picture
MediaScannerConnection.scanFile(context, arrayOf(uri.toString()), null, null)
}
// don't forget to recycle the bitmap when you don't need it any longer
bitmap.recycle()
}
Upvotes: 1