21

unfortunately the solutions I've found didn't work on android 5.1.1. I have a bitmap called source. I need to save it directly to my phone's gallery. My manifest contains <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> Can you give me a working method to do this?

0

10 Answers 10

38

There were several different ways to do it before API 29 (Android Q) but all of them involved one or a few APIs that are deprecated with Q. In 2019, here's a way to do it that is both backward and forward compatible:

(And since it is 2019 so I will write in Kotlin)

    /// @param folderName can be your app's name
    private fun saveImage(bitmap: Bitmap, context: Context, folderName: String) {
        if (android.os.Build.VERSION.SDK_INT >= 29) {
            val values = contentValues()
            values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/" + folderName)
            values.put(MediaStore.Images.Media.IS_PENDING, true)
            // RELATIVE_PATH and IS_PENDING are introduced in API 29.

            val uri: Uri? = context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
            if (uri != null) {
                saveImageToStream(bitmap, context.contentResolver.openOutputStream(uri))
                values.put(MediaStore.Images.Media.IS_PENDING, false)
                context.contentResolver.update(uri, values, null, null)
            }
        } else {
            val directory = File(Environment.getExternalStorageDirectory().toString() + separator + folderName)
            // getExternalStorageDirectory is deprecated in API 29

            if (!directory.exists()) {
                directory.mkdirs()
            }
            val fileName = System.currentTimeMillis().toString() + ".png"
            val file = File(directory, fileName)
            saveImageToStream(bitmap, FileOutputStream(file))
            if (file.absolutePath != null) {
                val values = contentValues()
                values.put(MediaStore.Images.Media.DATA, file.absolutePath)
                // .DATA is deprecated in API 29
                context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
            }
        }
    }

    private fun contentValues() : ContentValues {
        val values = ContentValues()
        values.put(MediaStore.Images.Media.MIME_TYPE, "image/png")
        values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis() / 1000);
        values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis());
        return values
    }

    private fun saveImageToStream(bitmap: Bitmap, outputStream: OutputStream?) {
        if (outputStream != null) {
            try {
                bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
                outputStream.close()
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
    }

Also, before calling this, you need to have WRITE_EXTERNAL_STORAGE first.

Sign up to request clarification or add additional context in comments.

2 Comments

Hi very nice example but how is it possible to load the save image in Android Q into an ImageView ? Before it was easy with file.absolutpath . Now i have only /external/images/media/1833 for example as uri. Do you know how to get the real filepath under Android Q ?
@Frank you can get file path using Ucrop method File Utils
16

Use this one:

private void saveImage(Bitmap finalBitmap, String image_name) {

        String root = Environment.getExternalStorageDirectory().toString();
        File myDir = new File(root);
        myDir.mkdirs();
        String fname = "Image-" + image_name+ ".jpg";
        File file = new File(myDir, fname);
        if (file.exists()) file.delete();
        Log.i("LOAD", root + fname);
        try {
            FileOutputStream out = new FileOutputStream(file);
            finalBitmap.compress(Bitmap.CompressFormat.JPEG, 90, out);
            out.flush();
            out.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

2 Comments

You don't have to create the external directory, you don't have to check if file exists to delete it and please close streams in a finally block or use +JDK7 Try-With-Resources
It works. But Gallery doesn't show the saved picture.
15

Use this code this we help you to store images into a particular folder which is saved_images and that folder images show in gallery immediately.

private void SaveImage(Bitmap finalBitmap) {

String root = Environment.getExternalStoragePublicDirectory(
    Environment.DIRECTORY_PICTURES).toString();
File myDir = new File(root + "/saved_images");
myDir.mkdirs();
Random generator = new Random();

int n = 10000;
n = generator.nextInt(n);
String fname = "Image-"+ n +".jpg";
File file = new File (myDir, fname);
if (file.exists ()) file.delete ();
try {
    FileOutputStream out = new FileOutputStream(file);
    finalBitmap.compress(Bitmap.CompressFormat.JPEG, 90, out);
    // sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, 
    //     Uri.parse("file://"+ Environment.getExternalStorageDirectory())));
    out.flush();
    out.close();

} catch (Exception e) {
    e.printStackTrace();
}
// Tell the media scanner about the new file so that it is
// immediately available to the user.
MediaScannerConnection.scanFile(this, new String[]{file.toString()}, null,
    new MediaScannerConnection.OnScanCompletedListener() {
        public void onScanCompleted(String path, Uri uri) {
            Log.i("ExternalStorage", "Scanned " + path + ":");
            Log.i("ExternalStorage", "-> uri=" + uri);
        }
    });
}

1 Comment

Does this solution is using any permission? Because the folder is not creating and I'm getting int the log that uri=null
10

From Android Q there are changes in saving image to gallery.Thanks to @BaoLei, here is my answer in java if anybody needs it.

    private void saveImage(Bitmap bitmap) {
        if (android.os.Build.VERSION.SDK_INT >= 29) {
            ContentValues values = contentValues();
            values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/" + getString(R.string.app_name));
            values.put(MediaStore.Images.Media.IS_PENDING, true);

            Uri uri = this.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
            if (uri != null) {
                try {
                    saveImageToStream(bitmap, this.getContentResolver().openOutputStream(uri));
                    values.put(MediaStore.Images.Media.IS_PENDING, false);
                    this.getContentResolver().update(uri, values, null, null);
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                }

            }
        } else {
            File directory = new File(Environment.getExternalStorageDirectory().toString() + '/' + getString(R.string.app_name));

            if (!directory.exists()) {
                directory.mkdirs();
            }
            String fileName = System.currentTimeMillis() + ".png";
            File file = new File(directory, fileName);
            try {
                saveImageToStream(bitmap, new FileOutputStream(file));
                ContentValues values = new ContentValues();
                values.put(MediaStore.Images.Media.DATA, file.getAbsolutePath());
                this.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }

        }
    }

    private ContentValues contentValues() {
        ContentValues values = new ContentValues();
        values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
        values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis() / 1000);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis());
        }
        return values;
    }

    private void saveImageToStream(Bitmap bitmap, OutputStream outputStream) {
        if (outputStream != null) {
            try {
                bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
                outputStream.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

2 Comments

How can I change the file name here for >Q ?
@BhelasagarMeher String fileName = System.currentTimeMillis() + ".png";
6

Here is a fully working solution in Kotlin:

fun saveToGallery(context: Context, bitmap: Bitmap, albumName: String) {
    val filename = "${System.currentTimeMillis()}.png"
    val write: (OutputStream) -> Boolean = {
        bitmap.compress(Bitmap.CompressFormat.PNG, 100, it)
    }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        val contentValues = ContentValues().apply {
            put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
            put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
            put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_DCIM}/$albumName")
        }

        context.contentResolver.let {
            it.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)?.let { uri ->
                it.openOutputStream(uri)?.let(write)
            }
        }
    } else {
        val imagesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).toString() + File.separator + albumName
        val file = File(imagesDir)
        if (!file.exists()) {
            file.mkdir()
        }
        val image = File(imagesDir, filename)
        write(FileOutputStream(image))
    }
}

1 Comment

It works very well for me only for version > Q, for minor I had to use the following code: val imagesDir = *image path* val file = File(imagesDir) if (!file.exists()) { file.mkdir() } val image = File(imagesDir, filename) write(FileOutputStream(image)) MediaScannerConnection.scanFile( context, arrayOf(image.absolutePath), arrayOf("image/png"), null ) bmpUri = Uri.fromFile(image)
5

Do it in One Line MediaStore.Images.Media.insertImage(applicationContext.getContentResolver(), IMAGE ,"nameofimage" , "description");

2 Comments

I tried this and it sort of works (it doesn't break), but it isn't sending the bitmap to the photo gallery. Perchance, do you know what might be wrong in my approach? I have the permission set and pictures taken from camera app on the emulator appears inside the gallery normally. Thanks in advance!
Since insertImage is deprecated, is there any other alternative?
4

Now we have android10 and android11, so here is an updated version, that will work in all android devices.

Make sure you have the WRITE_EXTERNAL_STORAGE permission before calling this function.

    private fun saveMediaToStorage(bitmap: Bitmap) {
        val filename = "${System.currentTimeMillis()}.jpg"
        var fos: OutputStream? = null
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            contentResolver?.also { resolver ->
                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)
                }
                val imageUri: Uri? =
                    resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
                fos = imageUri?.let { resolver.openOutputStream(it) }
            }
        } else {
            val imagesDir =
                Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
            val image = File(imagesDir, filename)
            fos = FileOutputStream(image)
        }
        fos?.use {
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it)
            toast("Saved to Photos")
        }
    }

Its kotlin, and I think very straight forward and self explaining code. But still if you have a problem, comment below and I will explain.

Reference: Android Save Bitmap to Gallery Tutorial.

3 Comments

simple and best.
this is saving pictures with black background, even though it is white photo
Does it require permission for accessing media files to save the bitmap?
2

I'd like to add Java code based on @Bao Lei 's answer that I used in my app.

    private void saveImage(Bitmap bitmap, Context context, String folderName) throws FileNotFoundException {
        if (android.os.Build.VERSION.SDK_INT >= 29) {
            ContentValues values = new ContentValues();
            values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/" + folderName);
            values.put(MediaStore.Images.Media.IS_PENDING, true);
            // RELATIVE_PATH and IS_PENDING are introduced in API 29.

            Uri uri = context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
            if (uri != null) {
                saveImageToStream(bitmap, context.getContentResolver().openOutputStream(uri));
                values.put(MediaStore.Images.Media.IS_PENDING, false);
                context.getContentResolver().update(uri, values, null, null);
            }
        } else {
            dir = new File(getApplicationContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES),"");
            // getExternalStorageDirectory is deprecated in API 29

            if (!dir.exists()) {
                dir.mkdirs();
            }

            java.util.Date date = new java.util.Date();
            imageFile = new File(dir.getAbsolutePath()
                    + File.separator
                    + new Timestamp(date.getTime()).toString()
                    + "Image.jpg");

            imageFile = new File(dir.getAbsolutePath()
                    + File.separator
                    + new Timestamp(date.getTime()).toString()
                    + "Image.jpg");
            saveImageToStream(bitmap, new FileOutputStream(imageFile));
            if (imageFile.getAbsolutePath() != null) {
                ContentValues values = new ContentValues();
                values.put(MediaStore.Images.Media.DATA, imageFile.getAbsolutePath());
                // .DATA is deprecated in API 29
                context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
            }
        }
    }

    private ContentValues contentValues() {
        ContentValues values = new ContentValues();
        values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
        values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis() / 1000);
        values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis());
        return values;
    }

    private void saveImageToStream(Bitmap bitmap, OutputStream outputStream) {
        if (outputStream != null) {
            try {
                bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
                outputStream.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

This one worked fine in my app.

Comments

1

Since it 2024, I'll try update the answer with Kotlin. Right now I also use this code in my Jetpack Compose app.

Since all the answer have not 100% similarity with mine, and my answer is no need to use any file providers or generate any File object, I'll show it here.

Environment I used on my app is Pictures (Environment.DIRECTORY_PICTURES).

First of all, because my app min SDK is 25, I need to add Manifest uses-permission for read and write.

<manifest ...

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="28" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
        android:maxSdkVersion="28" />

    <application
        ...

</manifest>

Second, let's say you already have your own bitmap, so I'll just skip to the MediaStore, ContentValues, and ContentResolver configuration. I'm using Kotlin Coroutines with Dispatchers.IO as the dispatcher. This second stage is occur in my ViewModel file.

To create ContentValues with MediaStore efficiently, I put a function to create it.

// This code, is in my ViewModel file

private fun createContentValues(title: String, subfolder: String): ContentValues =
    ContentValues().apply {
        put(MediaStore.Images.Media.TITLE, "$title.png")
        put(MediaStore.Images.Media.DISPLAY_NAME, "$title.png")
        put(MediaStore.Images.Media.MIME_TYPE, "image/png")
        put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis())
        put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            put(
                MediaStore.MediaColumns.RELATIVE_PATH,
                "${Environment.DIRECTORY_PICTURES}/$subfolder"
            )
        }
    }

Then we back on configure the ContentResolver

// This code, is in my ViewModel file

suspend fun saveShareBitmap(applicationContext: Context, bitmap: Bitmap, subfolder: String, filename: String): List<Any> =
    withContext(Dispatchers.IO) {
        val contentValues = createContentValues(filename, subfolder)

        var uri: Uri? = null

        try {
            uri = applicationContext.contentResolver.insert(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues
            ) ?: throw IOException("Failed to create new MediaStore record")

            val stream = applicationContext.contentResolver.openOutputStream(uri)
                ?: throw IOException("Failed to open output stream")

            bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
            stream.flush()
            stream.close()
        } catch (e: IOException) {
            if (uri != null) {
                applicationContext.contentResolver.delete(uri, null, null)
            }
        }

        return@withContext listOf(uri ?: Uri.parse(""), filename)
    }

Third, we back to our layout code file. We need to check if build version code is <= SDK 28, we need to grant permission for read-write file. Inside the CoroutineScope that I already initialized before, it is show how to share the image using current created URI from my previous ViewModel function.

if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
    if (!(writePermissionCheckResult && readPermissionCheckResult)) {
        launcherPermission.launch(
            arrayOf(
                Manifest.permission.READ_EXTERNAL_STORAGE,
                Manifest.permission.WRITE_EXTERNAL_STORAGE
            )
        )
    } else {
        isWriteReadPermission = true
    }
} else {  // SDK after 28 no need check
    isWriteReadPermission = true
}

if (!isWriteReadPermission && !isRationaleWriteReadPermission) {
    context.showToast(context.getString(R.string.accept_on_settings))

    Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
        data = Uri.fromParts("package", applicationContext.packageName, null)
        context.startActivity(this)
    }
} else if (isRationaleWriteReadPermission) {
    context.showToast(
        "${context.getString(R.string.accept_permissions)}\n" +
                context.getString(R.string.please_press_again)
    )
} else {
    // code if permission accepted
    // Coroutine Scope already initialized as scope
    // ViewModel already initialized as 
    scope.launch{
        withContext(Dispatchers.Main){
            // in this scope I make share bitmap by URI
            val (uri, filename) = viewModel.saveShareBitmap(applicationContext, bitmap, subfolder, filename)

            if (uri is Uri && filename is String) {
                val chooser = Intent.createChooser(
                    Intent(Intent.ACTION_SEND).apply {
                        clipData =
                            ClipData.newRawUri(filename, uri)  // to fix Security Exception
                        putExtra(Intent.EXTRA_STREAM, uri)
                        addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
                        type = "image/png"
                    }, applicationContext.getString(R.string.share_where)
                )
                context.startActivity(chooser)
            }
        }
    }
}

That's all from me, thank you and feel free to correct my answer by comment.

Comments

0

For Media Scanning, you can simply do


  val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
  intent.data = Uri.fromFile(path) // path must be of File type
  context.sendBroadcast(intent)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.