81

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.

Willi Mentzel
  • 27,862
  • 20
  • 113
  • 121
Android Admirer
  • 2,310
  • 2
  • 17
  • 38

6 Answers6

81

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
    }
}
PerracoLabs
  • 16,449
  • 15
  • 74
  • 127
  • Thank you very much, that really helped, but there are 3 more points. Please check my edited question – Android Admirer Jul 18 '19 at 16:58
  • 1
    Is this problem happening only in Android Q, or all Android versions? Take into account that the code in my answer is meant for Android Q. For older versions of Android you will need to create the path + file as usual. – PerracoLabs Jul 20 '19 at 17:21
  • 1
    Oh, this was tested on Android Pie not Q. So what I should do instead is check for the android version, and if it is Q do the code above and if it is lower use the usual deprecated code? – Android Admirer Jul 20 '19 at 17:36
  • 1
    Yes, for older than Q you need to use your original code. Your code is deprecated only Q, but valid in older versions. – PerracoLabs Jul 20 '19 at 20:34
  • You have to use `File.separator`, not `pathSeparator`. I submitted an edit but the OP strangely didn't understand and rejected it. See https://stackoverflow.com/questions/5971964/file-separator-or-file-pathseparator – devrocca Sep 19 '19 at 08:58
  • How can i get that image and display in imageview? bcz Environment.getExternalStorageDirectory() is deprecated – Sagar gujarati Feb 19 '20 at 07:19
  • Can anyone help with https://stackoverflow.com/questions/63543414/rename-file-of-the-external-storage-which-is-created-by-app-in-android-10-worki – jazzbpn Aug 24 '20 at 03:41
  • why resolver uri return null? i put png and change png mimetype only – Fortran Dec 17 '20 at 14:33
  • @PerracoLabs Do we have to add like this while we download file via DownloadManager or does the download manager do this on its own? – Nitin Vats Feb 02 '21 at 15:05
  • @PerracoLabs, Perfect master, thank you very much! – Will V Apr 28 '21 at 20:16
  • 1
    How to check if the file already exists? If it does then just show the bitmap. – Aman Verma May 30 '21 at 02:35
  • how to save image in DCIM folder of external SDCARD? – Milan Tejani May 31 '21 at 06:34
  • @PerracoLabs my friend published a question see this link https://stackoverflow.com/questions/67773780/how-to-save-image-in-dcim-folder-of-external-sdcard-in-android-11 – Milan Tejani Jun 01 '21 at 03:37
  • how to check file already exist or not It's saved filename.jpg then filename(1).jpg and go on so I want to check file exist or not. – Bhavin Patel Jul 05 '21 at 07:24
  • How to save document like csv file in DCIM folder? – Gulab Sagevadiya May 25 '22 at 07:59
54
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

Rachit Vohera
  • 707
  • 7
  • 10
  • 1
    But how to check image is already saved or not! Nobody talking about it! – Jayesh Rathod Feb 26 '20 at 07:25
  • 2
    You can check exist file using this code `if(isFilePresent()){ // call save function here }else{ // Toast file exist // } private boolean isFilePresent() { File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), name + ".jpg");return file.exists(); }` – Rachit Vohera Feb 28 '20 at 16:59
  • 7
    @RachitVohera Your answer is correct, but your previous comment is not - `getExternalStoragePublicDirectory ` is deprecated in Android 10. That is why you used `if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {` in your answer. – HB. May 08 '20 at 05:20
  • Hey and what about if I wanna open this image file with a gallery using an intent. I passed the content URI but still I got file not found – Faraz Ahmed May 09 '20 at 13:59
  • run media scan for add new media – Rachit Vohera Jul 02 '20 at 16:20
  • @RachitVohera can you provide for storing video , i tried but my app getting crashed. – iamkdblue Jul 23 '20 at 17:49
  • @HB. Do you have any suggestions on "How to check the image exists or not"? Android Q – Aman Verma May 30 '21 at 02:41
  • @AmanVerma You can try and open a `InputStream`, if it throws a `FileNotFoundException`, then the image is not available. – HB. Jun 07 '21 at 04:02
  • @RachitVohera i tried to use your suggestion(i wan't to specify file extension) `DISPLAY_NAME, name + ".jpg"`, but because we use `MIME_TYPE, "image/jpg"` it was saving file with name - `name.jpg.jpg`, because `MIME_TYPE, "image/jpg"` will automatically takes file extension according to specified `mime_type`.. – Vivek Thummar Jun 23 '21 at 11:17
  • @VivekThummar what name ur passing as parameter? – Rachit Vohera Jun 23 '21 at 12:01
  • it can be anything like `img_timeStamp`..basically it was file `name/title` – Vivek Thummar Jun 23 '21 at 12:27
  • @NikhilSolanki, @HB how to check in `if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {` condition file already exist or not It's saved filename.jpg then filename(1).jpg and go on so I want to check file exist or not. – Bhavin Patel Jul 05 '21 at 07:23
  • I have successfully saved a file using mediastore. Now I want to show user a button which will open it in gallery or photos app. But right now I have the path as Pictures/FolderName/filename.jpg But i want the full path like storage/emulated/0/Pictures/FolderName. How to get that or should i hardcode is like "storage/emulated/0" then the path to my file? – Aman Verma Dec 11 '21 at 21:23
  • How to save document file like csv file in DCIM Folder ? I have posted bountied question in my profile. https://stackoverflow.com/questions/72269368/how-to-save-document-file-in-external-storage-after-android-api-level28/72373236#72373236 – Gulab Sagevadiya May 25 '22 at 08:00
13

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();
    }

}
Ismail Osunlana
  • 434
  • 5
  • 9
  • I tried to use this approach, however, I get `IllegalArgumentException: Failed to find configured root that contains /Pictures/2020-12-17_170659_photo.jpg` when I attempt `FileProvider.getUriForFile()` immediately after saving the file on Android Q. In the manifest, the FileProvider `provider` is correctly set up. When using the MediaStore is it not possible to get the Uri after saving it (insert into table) ? – Someone Somewhere Dec 19 '20 at 01:25
  • Maybe it is necessary to save my image with getExternalFilesDir() and then call `FileProvider.getUriForFile()`? – Someone Somewhere Dec 19 '20 at 01:33
  • how to check in `if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {` condition file already exist or not It's saved filename.jpg then filename(1).jpg and go on so I want to check file exist or not. – Bhavin Patel Jul 05 '21 at 07:23
  • @SomeoneSomewhere Did u got any solution ? how to get absolute file path after saving the image ? (android 11) – Fakeeraddi Bhavi Jul 08 '21 at 09:05
4

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.

Yaron Budowski
  • 420
  • 5
  • 15
  • This is excellent. Even today I still struggled to save to the correct location. Very good function in that lib. – Panama Jack Jan 25 '23 at 11:27
1

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>
Liong
  • 1,324
  • 12
  • 18
0
   **You can use this too**

  private fun saveFileInternal(
        sourceFile: File,
        fileName: String?,
        fileType: FolderType,
        contentResolver: ContentResolver
    ): Boolean {
        var filename: String? = fileName
        return try {
            var selectedType = fileType
            val contentValues = ContentValues()
            val extension: String? = getFileExtension(sourceFile)
            var mimeType: String? = null
            if (extension != null) {
                mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
            }
            var uriToInsert: Uri? = null
            if ((fileType == FolderType.IMAGE || fileType == FolderType.VIDEO) && mimeType != null) {
                if (mimeType.startsWith("image")) {
                    selectedType = FolderType.IMAGE
                }
                if (mimeType.startsWith("video")) {
                    selectedType = FolderType.VIDEO
                }
            }
            when (selectedType) {
                FolderType.IMAGE -> {
                    if (filename == null) {
                        filename = generateFileName(0, extension)
                    }
                    uriToInsert =
                        MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
                    val dirDest = File(Environment.DIRECTORY_PICTURES, "folder")
                    contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, "$dirDest${separator}")
                    contentValues.put(MediaStore.Images.Media.DISPLAY_NAME, filename)
                    contentValues.put(MediaStore.Images.Media.MIME_TYPE, mimeType)
                }
                FolderType.VIDEO -> {
                    if (filename == null) {
                        filename = generateFileName(1, extension)
                    }
                    val dirDest = File(Environment.DIRECTORY_MOVIES, "folder")
                    contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, "$dirDest${separator}")
                    uriToInsert =
                        MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
                    contentValues.put(MediaStore.Video.Media.DISPLAY_NAME, filename)
    
                }
                FolderType.DOWNLOAD -> {
                    if (filename == null) {
                        filename = sourceFile.name
                    }
                    val dirDest = File(Environment.DIRECTORY_DOWNLOADS, "folder")
                    contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, "$dirDest${separator}")
                    uriToInsert =
                        MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
                    contentValues.put(MediaStore.Downloads.DISPLAY_NAME, filename)
                }
                else -> {
                    if (filename == null) {
                        filename = sourceFile.name
                    }
                    val dirDest = File(Environment.DIRECTORY_MUSIC, "folder")
                    contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, "$dirDest${separator}")
                    uriToInsert =
                        MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
                    contentValues.put(MediaStore.Audio.Media.DISPLAY_NAME, filename)
                }
            }
            contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
            val dstUri: Uri = contentResolver.insert(uriToInsert!!, contentValues)!!
            val fileInputStream = FileInputStream(sourceFile)
            val outputStream: OutputStream = contentResolver.openOutputStream(dstUri)!!
    
            copyFile(fileInputStream, outputStream)
            fileInputStream.close()
            true
        } catch (e: java.lang.Exception) {
            Log.e("fileManager", e.message.toString())
            false
        }
    }
Asghar Hosseini
  • 239
  • 1
  • 5
  • 14