如何在Android Q中使用MediaStore保存图像?

81

这里是指向新版 Android Q 的Scoped Storage的链接。

根据Android Developer 最佳实践博客,在存储共享媒体文件(也是我的情况)时,应使用MediaStore API。

查看文档后,我没有找到相关函数。

这是我使用 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
}

结果是我的图像保存在这里:Android/data/com.mypackage.etc/files/Pictures/example.png,如最佳实践博客中所述的 存储应用内文件


我的问题是:

如何使用MediaStore API保存图像?Java的答案同样可接受。


编辑

但还有3个要点。

这是我的代码:

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- 保存的图片没有得到正确的名称 "Myimage.png"。已经尝试使用 "Myimage" 和 "Myimage.PNG",但都无效。

图片总是以数字命名,例如:1563468625314.jpg

这带来了第二个问题:

2- 尽管我将位图压缩为 png 格式,但图片保存为 jpg。这不是什么大问题,只是好奇为什么会这样。

3- 在 Android Q 以下的设备上,相对位置会导致异常。在“Android 版本检查”语句块中包含后,图像将直接保存在 Pictures 文件夹的根目录中。


编辑 2

更改为:

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)

这里是日志

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: <<<<<

我注意到title与名称匹配,所以我尝试添加:

put(MediaStore.Images.ImageColumns.TITLE, name)

它仍然没有起作用,这是新的日志:

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: <<<<<

我无法将 date_added 更改为名称。

MediaStore.MediaColumns.DATA 已被弃用。


1
直到我添加了 put(MediaStore.MediaColumns.DISPLAY_NAME, name + ".mp3"),我也在文件名中得到了那些随机数字。 - Marin Maršić
请查看我在此处的答案:https://dev59.com/pF0a5IYBdhLWcg3wipTk#68110559 - Amr
6个回答

82

尝试下一种方法。Android Q(及以上版本)会在必要时自动创建文件夹。此示例硬编码为输出到 DCIM 文件夹中。如果需要子文件夹,请将子文件夹名称附加为下一个:

final String relativeLocation = Environment.DIRECTORY_DCIM + File.separator + “YourSubforderName”;

考虑压缩格式应与mime-type参数相关。例如,使用JPEG压缩格式的mime-type将为"image/jpeg",依此类推。可能您还希望将压缩质量作为参数传递,在此示例中硬编码为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 变种:

@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
    }
}

1
这个问题只发生在Android Q上吗,还是所有的Android版本都有?请注意,我回答中的代码是针对Android Q设计的。对于旧版本的Android,您需要像往常一样创建路径+文件。 - PerracoLabs
1
哦,这是在 Android Pie 而不是 Q 上测试过的。因此我应该检查 Android 版本,如果是 Q 则执行上面的代码,如果是低版本则使用通常已经被弃用的代码,对吧? - Android Admirer
1
是的,对于早于Q版本的Android系统,您需要使用原始代码。您的代码仅在Q版本中已被弃用,但在旧版本中仍然有效。 - PerracoLabs
你必须使用 File.separator 而不是 pathSeparator。我提交了一篇编辑,但奇怪的是 OP 没有理解并拒绝了它。请参阅 https://dev59.com/1m025IYBdhLWcg3wfmHw。 - devrocca
1
如何检查文件是否已存在?如果是,则只需显示位图。 - Aman Verma
显示剩余10条评论

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

图片将存储在根目录下的Pictures文件夹中

在以下链接中查看实时演示:https://youtu.be/695HqaiwzQ0,我创建了教程。


2
你可以使用以下代码检查文件是否存在:if(isFilePresent()){ // 在此处调用保存函数 }else{ // 弹出文件已存在的提示信息 // } private boolean isFilePresent() { File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), name + ".jpg"); return file.exists(); } - Rachit Vohera
7
@RachitVohera 您的答案是正确的,但您之前的评论不正确 - getExternalStoragePublicDirectory 在Android 10中已被弃用。这就是为什么您在答案中使用了 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - HB.
嘿,如果我想使用意图打开这个图像文件并在画廊中查看怎么办?我传递了内容URI,但仍然显示文件未找到。 - Faraz Ahmed
@HB,你有关于“如何检查图像是否存在”的建议吗?Android Q。 - Aman Verma
显示剩余10条评论

13

这是我经常使用的。你可以试试看。

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

}

我尝试使用这种方法,但是在Android Q上保存文件后立即尝试使用FileProvider.getUriForFile()时,会出现IllegalArgumentException: Failed to find configured root that contains /Pictures/2020-12-17_170659_photo.jpg的错误。在清单文件中,FileProvider“provider”已正确设置。当使用MediaStore插入表后,无法获取Uri吗? - Someone Somewhere
也许需要使用getExternalFilesDir()保存我的图像,然后调用FileProvider.getUriForFile() - Someone Somewhere
如何检查 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 条件下文件是否已存在,如果保存的是 filename.jpg,则保存为 filename(1).jpg,依此类推,所以我想要检查文件是否存在。 - Bhavin Patel
@SomeoneSomewhere 你有解决方案吗?如何在保存图像后获取绝对文件路径?(Android 11) - Fakeeraddi Bhavi

4
如果有人想知道如何将照片保存到DCIM文件夹中,并以后在Google相册中显示的方法: (基于: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 );
    }
}

WriteFileToStream 是一个标准方法,用于将文件复制到流中。


这太棒了。即使今天我仍然在努力将文件保存到正确的位置。那个库中的功能非常好。 - Panama Jack

1
这是我为2022年版本的翻译,该版本已在模拟器SDK 27和30以及三星S22手机上进行了测试。 简述: 对于SDK < 29,您需要遵循此处的代码here,并在成功拍摄照片后添加一些代码。您可以在我的savePictureQ(...)函数下看到。
否则,如果您的SDK >= 29,只需从contentResolver.insert(...)函数中传递URI到MediaStore.EXTRA_OUTPUT extras即可。
由于`startActivityForResult(Intent)`已经过时,我的版本使用`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)
            }
        }
    }

我在另一个名为Repository.kt的文件中调用了Intent,并使用虚假的viewModel来调用Repository代码。 这是我调用viewModel代码的方式。
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)
        }
    }
    ...
}

在我的虚假viewModel中:
class MenuViewModel(private val repository: IRepository) {
    fun getTargetUri() = repository.getTargetUri()
    fun getCameraIntent(launcher: ActivityResultLauncher<Intent>) =
        repository.createTakePictureIntent(launcher)
    fun savePictureQ(): String = repository.savePictureQ()
}

在我的代码仓库中:
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)
                }
            }
        }
    }
}

对于 SDK < 29,我遵循 Google Developer 提供的代码。
在按照代码操作后,我的清单文件如下:
<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>

创建一个名为xml的新res文件夹,然后创建一个新的XML文件,确保名称与中

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
        }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接