Android CameraX适配Android13的踩坑之路

news2024/11/18 13:42:41

AndroidCameraX适配Android13的踩坑之路

前言:

最近把AGP插件升级到8.1.0,新建项目的时候目标版本和编译版本都是33,发现之前的demo使用Camerax拍照和录像都失败了,于是查看了一下官网和各种资料,找到了Android13的适配方案.

行为变更:以 Android 13 或更高版本为目标平台的应用

与早期版本一样,Android 13 包含一些行为变更,这些变更可能会影响您的应用。以下行为变更仅影响以 Android 13 或更高版本为目标平台的应用。如果您的应用以 Android 13 或更高版本为目标平台,您应该修改自己的应用以适当地支持这些行为(如果适用)。

此外,请务必查看对 Android 13 上运行的所有应用都有影响的行为变更列表。

细化的媒体权限

在这里插入图片描述
该对话框的两个按钮,从上至下分别为“Allow”和“Don’t allow”
图 1. 您在请求 READ_MEDIA_AUDIO 权限时向用户显示的系统权限对话框。

如果您的应用以 Android 13 或更高版本为目标平台,并且需要访问其他应用已经创建的媒体文件,您必须请求以下一项或多项细化的媒体权限,而不是READ_EXTERNAL_STORAGE 权限:

媒体类型请求权限
图片和照片READ_MEDIA_IMAGES
视频READ_MEDIA_VIDEO
音频文件READ_MEDIA_AUDIO

如果用户之前向您的应用授予了 READ_EXTERNAL_STORAGE 权限,系统会自动向您的应用授予细化的媒体权限。否则,当应用请求上表中显示的任何权限时,系统会显示面向用户的对话框。在图 1 中,应用请求 READ_MEDIA_AUDIO 权限。

如果您同时请求 READ_MEDIA_IMAGES 权限和 READ_MEDIA_VIDEO 权限,系统只会显示一个系统权限对话框。

1.参考资料如下:

https://developer.android.google.cn/about/versions/13/features?hl=zh-cn

https://blog.csdn.net/as425017946/article/details/127530660

https://blog.csdn.net/guolin_blog/article/details/127024559

2.依赖导入:

这里的依赖都是基于AGP8.1.0,Android Studio的插件版本 Gifaffe 2022.3.1

2.1 添加统一的CameraX依赖配置:

在项目的gradle目录下新建libs.version.toml文件
在这里插入图片描述

2.2 添加CameraX依赖:

[versions]
agp = "8.1.0"
org-jetbrains-kotlin-android = "1.8.0"
core-ktx = "1.10.1"
junit = "4.13.2"
androidx-test-ext-junit = "1.1.5"
espresso-core = "3.5.1"
appcompat = "1.6.1"
material = "1.9.0"
constraintlayout = "2.1.4"
glide = "4.13.0"
glide-compiler = "4.13.0"
camerax = "1.1.0-beta03"
camerax-core = "1.1.0-beta03"
camerax-video = "1.1.0-beta03"
camerax-view = "1.1.0-beta03"
camerax-extensions = "1.1.0-beta03"
camerax-lifecycle = "1.1.0-beta03"


[libraries]
core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
appcompat = { group = "androidx.appcompat", name = "appcompat", version = "1.6.1" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
glide = {group = "com.github.bumptech.glide",name = "glide",version.ref = "glide"}
camerax = {group = "androidx.camera",name = "camera-camera2",version.ref = "camerax" }
camerax-core = {group = "androidx.camera",name = "camera-core",version.ref = "camerax-core"}
camerax-video = {group = "androidx.camera",name = "camera-video",version.ref = "camerax-video"}
camerax-view = {group = "androidx.camera",name = "camera-view",version.ref = "camerax-view"}
camerax-extensions = {group = "androidx.camera",name = "camera-extensions",version.ref = "camerax-extensions"}
camerax-lifecycle = {group = "androidx.camera",name = "camera-lifecycle",version.ref = "camerax-lifecycle"}
kotlin-stdlib = {group = "org.jetbrains.kotlin",name = "kotlin-stdlib-jdk7",version.ref = "kotlin-stdlib"}
kotlin-reflect = {group = "org.jetbrains.kotlin",name = "kotlin-reflect",version.ref = "kotlin-reflect"}
kotlinx-coroutines-core = {group = "org.jetbrains.kotlin",name = "kotlinx-coroutines-core",version.ref = "kotlinx-coroutines-core"}
kotlin-kotlinx-coroutines-android = {group = "org.jetbrains.kotlin",name = "kotlinx-coroutines-androidt",version.ref = "kotlinx-coroutines-android"}
glide-compiler = {group = "com.github.bumptech.glide",name = "compiler",version.ref = "glide-compiler"}
utilcodex = {group = "com.blankj",name = "utilcodex",version.ref = "utilcodex"}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4wxQEzPo-1692190126584)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20230816194910526.png)]

2.3 在app的build.gradle目录下引入依赖:

dependencies {

    implementation(libs.core.ktx)
    implementation(libs.appcompat)
    implementation(libs.material)
    implementation(libs.constraintlayout)
    implementation(libs.glide)
    implementation(libs.camerax)
    implementation(libs.camerax.core)
    implementation(libs.camerax.view)
    implementation(libs.camerax.extensions)
    implementation(libs.camerax.lifecycle)
    implementation(libs.camerax.video)
    implementation(libs.kotlin.stdlib)
    implementation(libs.kotlin.reflect)
    implementation(libs.utilcodex)
    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.test.ext.junit)
    androidTestImplementation(libs.espresso.core)
    annotationProcessor(libs.glide.compiler)
}

在这里插入图片描述

3.主界面布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <androidx.camera.view.PreviewView
        android:id="@+id/mPreviewView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btnCameraCapture"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_marginBottom="50dp"
        android:background="@color/colorPrimaryDark"
        android:text="拍照"
        android:textColor="@color/white"
        android:textSize="16sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@+id/btnVideo" />

    <Button
        android:id="@+id/btnVideo"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_marginStart="10dp"
        android:layout_marginBottom="50dp"
        android:background="@color/colorPrimaryDark"
        android:text="录像"
        android:textColor="@color/white"
        android:textSize="16sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toRightOf="@+id/btnCameraCapture"
        app:layout_constraintRight_toLeftOf="@+id/btnSwitch" />

    <Button
        android:id="@+id/btnSwitch"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_marginStart="10dp"
        android:layout_marginBottom="50dp"
        android:background="@color/colorPrimaryDark"
        android:gravity="center"
        android:text="切换镜头"
        android:textColor="@color/white"
        android:textSize="16sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toRightOf="@+id/btnVideo"
        app:layout_constraintRight_toRightOf="parent" />

    <Button
        android:id="@+id/btnOpenCamera"
        android:layout_width="200dp"
        android:layout_height="50dp"
        android:background="@color/colorPrimaryDark"
        android:text="进入相机拍照界面"
        android:textColor="@color/white"
        android:textSize="16sp"
        tools:ignore="MissingConstraints" />


</androidx.constraintlayout.widget.ConstraintLayout>

4.选择相机界面布局如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <Button
        android:id="@+id/btnCamera"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="打开相机"
        tools:ignore="MissingConstraints" />

    <ImageView
        android:id="@+id/iv_avatar"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_marginStart="20dp"
        android:layout_marginTop="20dp"
        android:background="@mipmap/ic_launcher"
        app:layout_constraintLeft_toRightOf="@+id/btnCamera"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

5.Android13权限适配:

Android13相较于之前的版本变化很大,细化了权限请求,具体的参考上面的官网资料,直接上代码:

5.1 Android13之前的适配:

可以看到,API 32也就是Android 12及以下系统,我们仍然声明的是READ_EXTERNAL_STORAGE权限。

在这里插入图片描述

5.2 Android13及以上的适配:

从Android 13开始,我们就会使用READ_MEDIA_IMAGES、READ_MEDIA_VIDEO、READ_MEDIA_AUDIO来替代了。

<!--存储图像或者视频权限-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
    android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
    tools:ignore="ScopedStorage" android:maxSdkVersion="32"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
    android:maxSdkVersion="32"
    tools:ignore="ScopedStorage" />
<!--录制音频权限-->
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>

6.项目中简单适配:

6.1 Android13之前权限请求如下:

@SuppressLint("RestrictedApi")
private fun initPermission() {
    if (allPermissionsGranted()) {
        // ImageCapture
        startCamera()
    } else {
        ActivityCompat.requestPermissions(
            this, REQUIRED_PERMISSIONS, Constants.REQUEST_CODE_PERMISSIONS
        )
    }
  }

 private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
    }

    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<String>, grantResults:
        IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == Constants.REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                startCamera()
            } else {
                ToastUtils.shortToast("请您打开必要权限")
            }
        }
    }

6.2 Android13及以上的版本权限请求适配如下:

private fun initPermission() {
    if (checkPermissions()) {
        // ImageCapture
        startCamera()
    } else {
        requestPermission()
    }
}

/**
 * Android13检查权限进行了细化,每个需要单独申请,这里我有拍照和录像,所以加入相机和录像权限
 *
 **/
private fun checkPermissions(): Boolean {
        when {
            Build.VERSION.SDK_INT >= 33 -> {
                val permissions = arrayOf(
                    Manifest.permission.READ_MEDIA_IMAGES,
                    Manifest.permission.READ_MEDIA_AUDIO,
                    Manifest.permission.READ_MEDIA_VIDEO,
                    Manifest.permission.CAMERA,
                    Manifest.permission.RECORD_AUDIO,
                )
                for (permission in permissions) {
                    return Environment.isExternalStorageManager()
                }
            }

            else -> {
                for (permission in REQUIRED_PERMISSIONS) {
                    if (ContextCompat.checkSelfPermission(
                            this,
                            permission
                        ) != PackageManager.PERMISSION_GRANTED
                    ) {
                        return false
                    }
                }
            }
        }
        return true
    }
	
 /**
  * 用户拒绝后请求权限需要同时申请,刚开始我是单独申请的调试后发现一直报错,所以改为一起申请
  *
  **/
    private fun requestPermission() {
        when {
            Build.VERSION.SDK_INT >= 33 -> {
                ActivityCompat.requestPermissions(
                    this,
                 arrayOf(Manifest.permission.READ_MEDIA_IMAGES,
                         Manifest.permission.READ_MEDIA_AUDIO,
                         Manifest.permission.READ_MEDIA_VIDEO,
                         Manifest.permission.CAMERA,
                         Manifest.permission.RECORD_AUDIO),
                    Constants.REQUEST_CODE_PERMISSIONS
                )
            }

            else -> {
                ActivityCompat.requestPermissions(this, 
                REQUIRED_PERMISSIONS, Constants.REQUEST_CODE_PERMISSIONS)
            }
        }
    }

 /**
  *用户请求权限后的回调,这里我是测试demo,所以用户拒绝后我会重复请求,真实项目根自己的需求来动态申请
  *
  **/
 override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<String>, grantResults:
        IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
                when (requestCode) {
                    Constants.REQUEST_CODE_PERMISSIONS -> {
                        var allPermissionsGranted = true
                        for (result in grantResults) {
                            if (result != PackageManager.PERMISSION_GRANTED) {
                                allPermissionsGranted = false
                                break
                            }
                        }
                        when {
                            allPermissionsGranted -> {
                                // 权限已授予,执行文件读写操作
                                startCamera()
                            }

                            else -> {
                                // 权限被拒绝,处理权限请求失败的情况
                                ToastUtils.shortToast("请您打开必要权限")
                                requestPermission()
                            }
                        }
                    }
                }
    }

7.拍照和录像代码:

7.1 拍照:

/**
 * 开始拍照
 */
private fun takePhoto() {
    val imageCapture = imageCamera ?: return
    val photoFile = createFile(outputDirectory, DATE_FORMAT, PHOTO_EXTENSION)
    val metadata = ImageCapture.Metadata().apply {
        // Mirror image when using the front camera
        isReversedHorizontal = lensFacing == CameraSelector.LENS_FACING_FRONT
    }
    val outputOptions =
        ImageCapture.OutputFileOptions.Builder(photoFile).setMetadata(metadata).build()
    imageCapture.takePicture(outputOptions, ContextCompat.getMainExecutor(this),
        object : ImageCapture.OnImageSavedCallback {
            override fun onError(exc: ImageCaptureException) {
                LogUtils.e(TAG, "Photo capture failed: ${exc.message}", exc)
                ToastUtils.shortToast(" 拍照失败 ${exc.message}")
            }

            override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                val savedUri = output.savedUri ?: Uri.fromFile(photoFile)
                ToastUtils.shortToast(" 拍照成功 $savedUri")
                LogUtils.e(TAG, savedUri.path.toString())
                val mimeType = MimeTypeMap.getSingleton()
                    .getMimeTypeFromExtension(savedUri.toFile().extension)
                MediaScannerConnection.scanFile(
                    this@MainActivity,
                    arrayOf(savedUri.toFile().absolutePath),
                    arrayOf(mimeType)
                ) { _, uri ->
                    LogUtils.d(
                        TAG,
                        "Image capture scanned into media store: ${uri.path.toString()}"
                    )
                }
            }
        })
}

7.2 录像:

之前的版本很老还在测试阶段,所以官方api发生了一些改变:

旧的api示例如下:

/**
 * 开始录像
 */
@SuppressLint("RestrictedApi", "ClickableViewAccessibility")
private fun takeVideo() {
    isRecordVideo = true
    val mFileDateFormat = SimpleDateFormat(DATE_FORMAT, Locale.US)
    //视频保存路径
    val file = File(FileManager.getCameraVideoPath(), mFileDateFormat.format(Date()) + ".mp4")
    //开始录像
    videoCapture?.startRecording(
        file,
        Executors.newSingleThreadExecutor(),
        object : OnVideoSavedCallback {
            override fun onVideoSaved(@NonNull file: File) {
                isRecordVideo = false
                LogUtils.d(TAG,"===视频保存的地址为=== ${file.absolutePath}")
                //保存视频成功回调,会在停止录制时被调用
                ToastUtils.shortToast(" 录像成功 $file")
            }

            override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {
                //保存失败的回调,可能在开始或结束录制时被调用
                isRecordVideo = false
                LogUtils.e(TAG, "onError: $message")
                ToastUtils.shortToast(" 录像失败 $message")
            }
        })
}

新的api示例如下:

startRecording方法参数发生了一些变化:第一个参数是传入一个文件输出信息类,之前是直接传入文件,其实影响不大

我们通过val outputOptions = OutputFileOptions.Builder(file)这个类构建一个对象,然后在开始录像时传入即可.

/**
 * 开始录像
 */
@SuppressLint("RestrictedApi", "ClickableViewAccessibility", "MissingPermission")
private fun takeVideo() {
    //开始录像
    try {
        isRecordVideo = true
        val mFileDateFormat = SimpleDateFormat(DATE_FORMAT, Locale.US)
        //视频保存路径
        val file =
            File(FileManager.getCameraVideoPath(), mFileDateFormat.format(Date()) + ".mp4")
        val outputOptions = OutputFileOptions.Builder(file)
        videoCapture?.startRecording(
            outputOptions.build(),
            Executors.newSingleThreadExecutor(),
            object : OnVideoSavedCallback {
                override fun onVideoSaved(outputFileResults: VideoCapture.OutputFileResults) {
                    isRecordVideo = false
                    LogUtils.d(TAG, "===视频保存的地址为=== ${file.absolutePath}")
                    //保存视频成功回调,会在停止录制时被调用
                    ToastUtils.shortToast(" 录像成功 $file")
                }

                override fun onError(
                    videoCaptureError: Int,
                    message: String,
                    cause: Throwable?
                ) {
                    //保存失败的回调,可能在开始或结束录制时被调用
                    isRecordVideo = false
                    LogUtils.e(TAG, "onError: $message")
                    ToastUtils.shortToast(" 录像失败 $message")
                }
            })
    } catch (e: Exception) {
        e.printStackTrace()
        LogUtils.e(TAG, "===录像出错===${e.message}")
    }
}

7.3 停止录像:

这里通过isRecordVideo是否正在录像进程录像和停止录像的操作

btnVideo.setOnClickListener {
    if (!isRecordVideo) {
        takeVideo()
        isRecordVideo = true
        btnVideo.text = "停止录像"
    } else {
        isRecordVideo = false
        videoCapture?.stopRecording()//停止录制
        //preview?.clear()//清除预览
        btnVideo.text = "开始录像"
    }
}

8.常量工具类:

object Constants {
    const val REQUEST_CODE_PERMISSIONS = 101
    const val REQUEST_CODE_CAMERA = 102
    const val REQUEST_CODE_CROP = 103

    const val DATE_FORMAT = "yyyy-MM-dd HH-mm-ss"
    const val PHOTO_EXTENSION = ".jpg"

    val REQUIRED_PERMISSIONS = arrayOf(
        Manifest.permission.CAMERA,
        Manifest.permission.WRITE_EXTERNAL_STORAGE,
        Manifest.permission.READ_EXTERNAL_STORAGE,
        Manifest.permission.RECORD_AUDIO
    )
}

9.ToastUtils:

package com.example.cameraxdemo.utils

import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.Log
import android.view.Gravity
import android.widget.Toast
import androidx.annotation.StringRes
import com.example.cameraxdemo.app.CameraApp
import java.lang.reflect.Field

/**
 *@author: njb
 *@date:   2023/8/15 17:13
 *@desc:
 */
object ToastUtils {
    private const val TAG = "ToastUtil"
    private var mToast: Toast? = null
    private var sField_TN: Field? = null
    private var sField_TN_Handler: Field? = null
    private var sIsHookFieldInit = false
    private const val FIELD_NAME_TN = "mTN"
    private const val FIELD_NAME_HANDLER = "mHandler"
    private fun showToast(
        context: Context, text: CharSequence,
        duration: Int, isShowCenterFlag: Boolean
    ) {
        val toastRunnable = ToastRunnable(context, text, duration, isShowCenterFlag)
        if (context is Activity) {
            if (!context.isFinishing) {
                context.runOnUiThread(toastRunnable)
            }
        } else {
            val handler = Handler(context.mainLooper)
            handler.post(toastRunnable)
        }
    }

    fun shortToast(context: Context, text: CharSequence) {
        showToast(context, text, Toast.LENGTH_SHORT, false)
    }

    fun longToast(context: Context, text: CharSequence) {
        showToast(context, text, Toast.LENGTH_LONG, false)
    }

    fun shortToast(msg: String) {
        showToast(CameraApp.mInstance, msg, Toast.LENGTH_SHORT, false)
    }

    fun shortToast(@StringRes resId: Int) {
        showToast(
            CameraApp.mInstance, CameraApp.mInstance.getText(resId),
            Toast.LENGTH_SHORT, false
        )
    }

    fun centerShortToast(msg: String) {
        showToast(CameraApp.mInstance, msg, Toast.LENGTH_SHORT, true)
    }

    fun centerShortToast(@StringRes resId: Int) {
        showToast(
            CameraApp.mInstance, CameraApp.mInstance.getText(resId),
            Toast.LENGTH_SHORT, true
        )
    }

    fun cancelToast() {
        val looper = Looper.getMainLooper()
        if (looper.thread === Thread.currentThread()) {
            mToast!!.cancel()
        } else {
            Handler(looper).post { mToast!!.cancel() }
        }
    }

    @SuppressLint("SoonBlockedPrivateApi")
    private fun hookToast(toast: Toast?) {
        try {
            if (!sIsHookFieldInit) {
                sField_TN = Toast::class.java.getDeclaredField(FIELD_NAME_TN)
                sField_TN?.run {
                    isAccessible = true
                    sField_TN_Handler = type.getDeclaredField(FIELD_NAME_HANDLER)
                }
                sField_TN_Handler?.isAccessible = true
                sIsHookFieldInit = true
            }
            val tn = sField_TN!![toast]
            val originHandler = sField_TN_Handler!![tn] as Handler
            sField_TN_Handler!![tn] = SafelyHandlerWrapper(originHandler)
        } catch (e: Exception) {
            Log.e(TAG, "Hook toast exception=$e")
        }
    }

    private class ToastRunnable(
        private val context: Context,
        private val text: CharSequence,
        private val duration: Int,
        private val isShowCenter: Boolean
    ) : Runnable {
        @SuppressLint("ShowToast")
        override fun run() {
            if (mToast == null) {
                mToast = Toast.makeText(context, text, duration)
            } else {
                mToast!!.setText(text)
                if (isShowCenter) {
                    mToast!!.setGravity(Gravity.CENTER, 0, 0)
                }
                mToast!!.duration = duration
            }
            hookToast(mToast)
            mToast!!.show()
        }
    }

    private class SafelyHandlerWrapper(private val originHandler: Handler?) : Handler() {
        override fun dispatchMessage(msg: Message) {
            try {
                super.dispatchMessage(msg)
            } catch (e: Exception) {
                Log.e(TAG, "Catch system toast exception:$e")
            }
        }

        override fun handleMessage(msg: Message) {
            originHandler?.handleMessage(msg)
        }
    }
}

10.FileManager:

package com.example.cameraxdemo.utils;

import android.app.Activity;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.util.Log;

import androidx.annotation.NonNull;

import com.example.cameraxdemo.app.CameraApp;

import java.io.File;

/**
 * @author: njb
 * @date: 2023/8/15 17:13
 * @desc:
 */
public class FileManager {
    // 媒体模块根目录
    private static final String SAVE_MEDIA_ROOT_DIR = Environment.DIRECTORY_DCIM;
    // 媒体模块存储路径
    private static final String SAVE_MEDIA_DIR = SAVE_MEDIA_ROOT_DIR + "/CameraXApp";
    private static final String AVATAR_DIR = "/avatar";
    private static final String SAVE_MEDIA_VIDEO_DIR = SAVE_MEDIA_DIR + "/video";
    private static final String SAVE_MEDIA_PHOTO_DIR = SAVE_MEDIA_DIR + "/photo";
    // JPG后缀
    public static final String JPG_SUFFIX = ".jpg";
    // PNG后缀
    public static final String PNG_SUFFIX = ".png";
    // MP4后缀
    public static final String MP4_SUFFIX = ".mp4";
    // YUV后缀
    public static final String YUV_SUFFIX = ".yuv";
    // h264后缀
    public static final String H264_SUFFIX = ".h264";


    /**
     * 保存图片到系统相册
     *
     * @param context
     * @param file
     */
    public static String saveImage(Context context, File file) {
        ContentResolver localContentResolver = context.getContentResolver();
        ContentValues localContentValues = getImageContentValues(context, file, System.currentTimeMillis());
        localContentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, localContentValues);

        Intent localIntent = new Intent("android.intent.action.MEDIA_SCANNER_SCAN_FILE");
        final Uri localUri = Uri.fromFile(file);
        localIntent.setData(localUri);
        context.sendBroadcast(localIntent);
        return file.getAbsolutePath();
    }

    public static ContentValues getImageContentValues(Context paramContext, File paramFile, long paramLong) {
        ContentValues localContentValues = new ContentValues();
        localContentValues.put("title", paramFile.getName());
        localContentValues.put("_display_name", paramFile.getName());
        localContentValues.put("mime_type", "image/jpeg");
        localContentValues.put("datetaken", Long.valueOf(paramLong));
        localContentValues.put("date_modified", Long.valueOf(paramLong));
        localContentValues.put("date_added", Long.valueOf(paramLong));
        localContentValues.put("orientation", Integer.valueOf(0));
        localContentValues.put("_data", paramFile.getAbsolutePath());
        localContentValues.put("_size", Long.valueOf(paramFile.length()));
        return localContentValues;
    }

    /**
     * 获取App存储根目录
     */
    public static String getAppRootDir() {
        String path = getStorageRootDir();
        FileUtil.createOrExistsDir(path);
        return path;
    }

    /**
     * 获取文件存储根目录
     */
    public static String getStorageRootDir() {
        File filePath = CameraApp.Companion.getMInstance().getExternalFilesDir("");
        String path;
        if (filePath != null) {
            path = filePath.getAbsolutePath();
        } else {
            path = CameraApp.Companion.getMInstance().getFilesDir().getAbsolutePath();
        }
        return path;
    }

    /**
     * 图片地址
     */
    public static String getCameraPhotoPath() {
        return getFolderDirPath(SAVE_MEDIA_PHOTO_DIR);
    }

    /**
     * 获取拍照普通图片文件
     */
    public static File getSavedPictureFile(long timeStamp) {
        String fileName = "image"+ "_"+ + timeStamp + JPG_SUFFIX;
        return new File(getCameraPhotoPath(), fileName);
    }

    /**
     * 头像地址
     */
    public static String getAvatarPath(String fileName) {
        String path;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            path = getFolderDirPath(SAVE_MEDIA_DIR + AVATAR_DIR);
        } else {
            path = getSaveDir(AVATAR_DIR);
        }
        return path + File.separator + fileName;
    }

    /**
     * 视频地址
     */
    public static String getCameraVideoPath() {
        return getFolderDirPath(SAVE_MEDIA_VIDEO_DIR);
    }

    public static String getFolderDirPath(String dstDirPathToCreate) {
        File dstFileDir = new File(Environment.getExternalStorageDirectory(), dstDirPathToCreate);
        if (!dstFileDir.exists() && !dstFileDir.mkdirs()) {
            Log.e("Failed to create file", dstDirPathToCreate);
            return null;
        }
        return dstFileDir.getAbsolutePath();
    }

    /**
     * 获取具体模块存储目录
     */
    public static String getSaveDir(@NonNull String directory) {
        String path = "";
        if (TextUtils.isEmpty(directory) || "/".equals(directory)) {
            path = "";
        } else if (directory.startsWith("/")) {
            path = directory;
        } else {
            path = "/" + directory;
        }
        path = getAppRootDir() + path;
        FileUtil.createOrExistsDir(path);
        return path;
    }

    /**
     * 通过媒体文件Uri获取文件-Android 11兼容
     *
     * @param fileUri 文件Uri
     */
    public static File getMediaUri2File(Uri fileUri) {
        String[] projection = {MediaStore.Images.Media.DATA};
        Cursor cursor = CameraApp.Companion.getMInstance().getContentResolver().query(fileUri, projection,
                null, null, null);
        if (cursor != null) {
            if (cursor.moveToFirst()) {
                int columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
                String path = cursor.getString(columnIndex);
                cursor.close();
                return new File(path);
            }
        }
        return null;
    }

    /**
     * 根据Uri获取图片绝对路径,解决Android4.4以上版本Uri转换
     *
     * @param context  上下文
     * @param imageUri 图片地址
     */
    public static String getImageAbsolutePath(Activity context, Uri imageUri) {
        if (context == null || imageUri == null)
            return null;
        if (DocumentsContract.isDocumentUri(context, imageUri)) {
            if (isExternalStorageDocument(imageUri)) {
                String docId = DocumentsContract.getDocumentId(imageUri);
                String[] split = docId.split(":");
                String type = split[0];
                if ("primary".equalsIgnoreCase(type)) {
                    return Environment.getExternalStorageDirectory() + "/" + split[1];
                }
            } else if (isDownloadsDocument(imageUri)) {
                String id = DocumentsContract.getDocumentId(imageUri);
                Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.parseLong(id));
                return getDataColumn(context, contentUri, null, null);
            } else if (isMediaDocument(imageUri)) {
                String docId = DocumentsContract.getDocumentId(imageUri);
                String[] split = docId.split(":");
                String type = split[0];
                Uri contentUri = null;
                if ("image".equals(type)) {
                    contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
                } else if ("video".equals(type)) {
                    contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
                } else if ("audio".equals(type)) {
                    contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
                }
                String selection = MediaStore.Images.Media._ID + "=?";
                String[] selectionArgs = new String[]{split[1]};
                return getDataColumn(context, contentUri, selection, selectionArgs);
            }
        } // MediaStore (and general)
        else if ("content".equalsIgnoreCase(imageUri.getScheme())) {
            // Return the remote address
            if (isGooglePhotosUri(imageUri))
                return imageUri.getLastPathSegment();
            return getDataColumn(context, imageUri, null, null);
        }
        // File
        else if ("file".equalsIgnoreCase(imageUri.getScheme())) {
            return imageUri.getPath();
        }
        return null;
    }

    /**
     * @param uri The Uri to checkRemote.
     * @return Whether the Uri authority is ExternalStorageProvider.
     */
    public static boolean isExternalStorageDocument(Uri uri) {
        return "com.android.externalstorage.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to checkRemote.
     * @return Whether the Uri authority is DownloadsProvider.
     */
    public static boolean isDownloadsDocument(Uri uri) {
        return "com.android.providers.downloads.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to checkRemote.
     * @return Whether the Uri authority is MediaProvider.
     */
    public static boolean isMediaDocument(Uri uri) {
        return "com.android.providers.media.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to checkRemote.
     * @return Whether the Uri authority is Google Photos.
     */
    public static boolean isGooglePhotosUri(Uri uri) {
        return "com.google.android.apps.photos.content".equals(uri.getAuthority());
    }

    public static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) {
        String column = MediaStore.Images.Media.DATA;
        String[] projection = {column};
        try (Cursor cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null)) {
            if (cursor != null && cursor.moveToFirst()) {
                int index = cursor.getColumnIndexOrThrow(column);
                return cursor.getString(index);
            }
        }
        return null;
    }
}

11.FileUtil

package com.example.cameraxdemo.utils;
import android.app.Activity;
import android.app.Application;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.util.Log;

import androidx.core.content.FileProvider;


import com.example.cameraxdemo.app.CameraApp;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

/**
 * @author: njb
 * @date: 2023/8/15 17:15
 * @desc:
 */
public class FileUtil {
    private static final String LINE_SEP = System.getProperty("line.separator");


    private FileUtil() {
        throw new UnsupportedOperationException("u can't instantiate me...");
    }

    /**
     * Return the file by path.
     *
     * @param filePath The path of file.
     * @return the file
     */
    public static File getFileByPath(final String filePath) {
        return isSpace(filePath) ? null : new File(filePath);
    }

    /**
     * Return whether the file exists.
     *
     * @param filePath The path of file.
     * @return {@code true}: yes<br>{@code false}: no
     */
    public static boolean isFileExists(final String filePath) {
        return isFileExists(getFileByPath(filePath));
    }

    /**
     * Return whether the file exists.
     *
     * @param file The file.
     * @return {@code true}: yes<br>{@code false}: no
     */
    public static boolean isFileExists(final File file) {
        return file != null && file.exists();
    }

    /**
     * Rename the file.
     *
     * @param filePath The path of file.
     * @param newName  The new name of file.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean rename(final String filePath, final String newName) {
        return rename(getFileByPath(filePath), newName);
    }

    /**
     * Rename the file.
     *
     * @param file    The file.
     * @param newName The new name of file.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean rename(final File file, final String newName) {
        // file is null then return false
        if (file == null) return false;
        // file doesn't exist then return false
        if (!file.exists()) return false;
        // the new name is space then return false
        if (isSpace(newName)) return false;
        // the new name equals old name then return true
        if (newName.equals(file.getName())) return true;
        File newFile = new File(file.getParent() + File.separator + newName);
        // the new name of file exists then return false
        return !newFile.exists()
                && file.renameTo(newFile);
    }

    /**
     * Return whether it is a directory.
     *
     * @param dirPath The path of directory.
     * @return {@code true}: yes<br>{@code false}: no
     */
    public static boolean isDir(final String dirPath) {
        return isDir(getFileByPath(dirPath));
    }

    /**
     * Return whether it is a directory.
     *
     * @param file The file.
     * @return {@code true}: yes<br>{@code false}: no
     */
    public static boolean isDir(final File file) {
        return file != null && file.exists() && file.isDirectory();
    }

    /**
     * Return whether it is a file.
     *
     * @param filePath The path of file.
     * @return {@code true}: yes<br>{@code false}: no
     */
    public static boolean isFile(final String filePath) {
        return isFile(getFileByPath(filePath));
    }

    /**
     * Return whether it is a file.
     *
     * @param file The file.
     * @return {@code true}: yes<br>{@code false}: no
     */
    public static boolean isFile(final File file) {
        return file != null && file.exists() && file.isFile();
    }

    /**
     * Create a directory if it doesn't exist, otherwise do nothing.
     *
     * @param dirPath The path of directory.
     * @return {@code true}: exists or creates successfully<br>{@code false}: otherwise
     */
    public static boolean createOrExistsDir(final String dirPath) {
        return createOrExistsDir(getFileByPath(dirPath));
    }

    /**
     * Create a directory if it doesn't exist, otherwise do nothing.
     *
     * @param file The file.
     * @return {@code true}: exists or creates successfully<br>{@code false}: otherwise
     */
    public static boolean createOrExistsDir(final File file) {
        boolean isDirectory = file.isDirectory();
        return file != null && (file.exists() ? file.isDirectory() : file.mkdirs());
    }

    /**
     * Create a file if it doesn't exist, otherwise do nothing.
     *
     * @param filePath The path of file.
     * @return {@code true}: exists or creates successfully<br>{@code false}: otherwise
     */
    public static boolean createOrExistsFile(final String filePath) {
        return createOrExistsFile(getFileByPath(filePath));
    }

    /**
     * Create a file if it doesn't exist, otherwise do nothing.
     *
     * @param file The file.
     * @return {@code true}: exists or creates successfully<br>{@code false}: otherwise
     */
    public static boolean createOrExistsFile(final File file) {
        if (file == null) return false;
        if (file.exists()) return file.isFile();
        if (!createOrExistsDir(file.getParentFile())) return false;
        try {
            return file.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * Create a file if it doesn't exist, otherwise delete old file before creating.
     *
     * @param filePath The path of file.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean createFileByDeleteOldFile(final String filePath) {
        return createFileByDeleteOldFile(getFileByPath(filePath));
    }

    /**
     * Create a file if it doesn't exist, otherwise delete old file before creating.
     *
     * @param file The file.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean createFileByDeleteOldFile(final File file) {
        if (file == null) return false;
        // file exists and unsuccessfully delete then return false
        if (file.exists() && !file.delete()) return false;
        if (!createOrExistsDir(file.getParentFile())) return false;
        try {
            return file.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * Copy the directory.
     *
     * @param srcDirPath  The path of source directory.
     * @param destDirPath The path of destination directory.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean copyDir(final String srcDirPath,
                                  final String destDirPath) {
        return copyDir(getFileByPath(srcDirPath), getFileByPath(destDirPath));
    }

    /**
     * Copy the directory.
     *
     * @param srcDirPath  The path of source directory.
     * @param destDirPath The path of destination directory.
     * @param listener    The replace listener.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean copyDir(final String srcDirPath,
                                  final String destDirPath,
                                  final OnReplaceListener listener) {
        return copyDir(getFileByPath(srcDirPath), getFileByPath(destDirPath), listener);
    }

    /**
     * Copy the directory.
     *
     * @param srcDir  The source directory.
     * @param destDir The destination directory.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean copyDir(final File srcDir,
                                  final File destDir) {
        return copyOrMoveDir(srcDir, destDir, false);
    }

    /**
     * Copy the directory.
     *
     * @param srcDir   The source directory.
     * @param destDir  The destination directory.
     * @param listener The replace listener.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean copyDir(final File srcDir,
                                  final File destDir,
                                  final OnReplaceListener listener) {
        return copyOrMoveDir(srcDir, destDir, listener, false);
    }

    /**
     * Copy the file.
     *
     * @param srcFilePath  The path of source file.
     * @param destFilePath The path of destination file.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean copyFile(final String srcFilePath,
                                   final String destFilePath) {
        return copyFile(getFileByPath(srcFilePath), getFileByPath(destFilePath));
    }

    /**
     * Copy the file.
     *
     * @param srcFilePath  The path of source file.
     * @param destFilePath The path of destination file.
     * @param listener     The replace listener.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean copyFile(final String srcFilePath,
                                   final String destFilePath,
                                   final OnReplaceListener listener) {
        return copyFile(getFileByPath(srcFilePath), getFileByPath(destFilePath), listener);
    }

    /**
     * Copy the file.
     *
     * @param srcFile  The source file.
     * @param destFile The destination file.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean copyFile(final File srcFile,
                                   final File destFile) {
        return copyOrMoveFile(srcFile, destFile, false);
    }

    /**
     * Copy the file.
     *
     * @param srcFile  The source file.
     * @param destFile The destination file.
     * @param listener The replace listener.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean copyFile(final File srcFile,
                                   final File destFile,
                                   final OnReplaceListener listener) {
        return copyOrMoveFile(srcFile, destFile, listener, false);
    }

    /**
     * Move the directory.
     *
     * @param srcDirPath  The path of source directory.
     * @param destDirPath The path of destination directory.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean moveDir(final String srcDirPath,
                                  final String destDirPath) {
        return moveDir(getFileByPath(srcDirPath), getFileByPath(destDirPath));
    }

    /**
     * Move the directory.
     *
     * @param srcDirPath  The path of source directory.
     * @param destDirPath The path of destination directory.
     * @param listener    The replace listener.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean moveDir(final String srcDirPath,
                                  final String destDirPath,
                                  final OnReplaceListener listener) {
        return moveDir(getFileByPath(srcDirPath), getFileByPath(destDirPath), listener);
    }

    /**
     * Move the directory.
     *
     * @param srcDir  The source directory.
     * @param destDir The destination directory.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean moveDir(final File srcDir,
                                  final File destDir) {
        return copyOrMoveDir(srcDir, destDir, true);
    }

    /**
     * Move the directory.
     *
     * @param srcDir   The source directory.
     * @param destDir  The destination directory.
     * @param listener The replace listener.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean moveDir(final File srcDir,
                                  final File destDir,
                                  final OnReplaceListener listener) {
        return copyOrMoveDir(srcDir, destDir, listener, true);
    }

    /**
     * Move the file.
     *
     * @param srcFilePath  The path of source file.
     * @param destFilePath The path of destination file.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean moveFile(final String srcFilePath,
                                   final String destFilePath) {
        return moveFile(getFileByPath(srcFilePath), getFileByPath(destFilePath));
    }

    /**
     * Move the file.
     *
     * @param srcFilePath  The path of source file.
     * @param destFilePath The path of destination file.
     * @param listener     The replace listener.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean moveFile(final String srcFilePath,
                                   final String destFilePath,
                                   final OnReplaceListener listener) {
        return moveFile(getFileByPath(srcFilePath), getFileByPath(destFilePath), listener);
    }

    /**
     * Move the file.
     *
     * @param srcFile  The source file.
     * @param destFile The destination file.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean moveFile(final File srcFile,
                                   final File destFile) {
        return copyOrMoveFile(srcFile, destFile, true);
    }

    /**
     * Move the file.
     *
     * @param srcFile  The source file.
     * @param destFile The destination file.
     * @param listener The replace listener.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean moveFile(final File srcFile,
                                   final File destFile,
                                   final OnReplaceListener listener) {
        return copyOrMoveFile(srcFile, destFile, listener, true);
    }

    private static boolean copyOrMoveDir(final File srcDir,
                                         final File destDir,
                                         final boolean isMove) {
        return copyOrMoveDir(srcDir, destDir, new OnReplaceListener() {
            @Override
            public boolean onReplace() {
                return true;
            }
        }, isMove);
    }

    private static boolean copyOrMoveDir(final File srcDir,
                                         final File destDir,
                                         final OnReplaceListener listener,
                                         final boolean isMove) {
        if (srcDir == null || destDir == null) return false;
        // destDir's path locate in srcDir's path then return false
        String srcPath = srcDir.getPath() + File.separator;
        String destPath = destDir.getPath() + File.separator;
        if (destPath.contains(srcPath)) return false;
        if (!srcDir.exists() || !srcDir.isDirectory()) return false;
        if (destDir.exists()) {
            if (listener == null || listener.onReplace()) {// require delete the old directory
                if (!deleteAllInDir(destDir)) {// unsuccessfully delete then return false
                    return false;
                }
            } else {
                return true;
            }
        }
        if (!createOrExistsDir(destDir)) return false;
        File[] files = srcDir.listFiles();
        for (File file : files) {
            File oneDestFile = new File(destPath + file.getName());
            if (file.isFile()) {
                if (!copyOrMoveFile(file, oneDestFile, listener, isMove)) return false;
            } else if (file.isDirectory()) {
                if (!copyOrMoveDir(file, oneDestFile, listener, isMove)) return false;
            }
        }
        return !isMove || deleteDir(srcDir);
    }

    private static boolean copyOrMoveFile(final File srcFile,
                                          final File destFile,
                                          final boolean isMove) {
        return copyOrMoveFile(srcFile, destFile, new OnReplaceListener() {
            @Override
            public boolean onReplace() {
                return true;
            }
        }, isMove);
    }

    private static boolean copyOrMoveFile(final File srcFile,
                                          final File destFile,
                                          final OnReplaceListener listener,
                                          final boolean isMove) {
        if (srcFile == null || destFile == null) return false;
        // srcFile equals destFile then return false
        if (srcFile.equals(destFile)) return false;
        // srcFile doesn't exist or isn't a file then return false
        if (!srcFile.exists() || !srcFile.isFile()) return false;
        if (destFile.exists()) {
            if (listener == null || listener.onReplace()) {// require delete the old file
                if (!destFile.delete()) {// unsuccessfully delete then return false
                    return false;
                }
            } else {
                return true;
            }
        }
        if (!createOrExistsDir(destFile.getParentFile())) return false;
        try {
            return writeFileFromIS(destFile, new FileInputStream(srcFile))
                    && !(isMove && !deleteFile(srcFile));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * Delete the directory.
     *
     * @param filePath The path of file.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean delete(final String filePath) {
        return delete(getFileByPath(filePath));
    }

    /**
     * Delete the directory.
     *
     * @param file The file.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean delete(final File file) {
        if (file == null) return false;
        if (file.isDirectory()) {
            return deleteDir(file);
        }
        return deleteFile(file);
    }

    /**
     * Delete the directory.
     *
     * @param dirPath The path of directory.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean deleteDir(final String dirPath) {
        return deleteDir(getFileByPath(dirPath));
    }

    /**
     * Delete the directory.
     *
     * @param dir The directory.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean deleteDir(final File dir) {
        if (dir == null) return false;
        // dir doesn't exist then return true
        if (!dir.exists()) return true;
        // dir isn't a directory then return false
        if (!dir.isDirectory()) return false;
        File[] files = dir.listFiles();
        if (files != null && files.length != 0) {
            for (File file : files) {
                if (file.isFile()) {
                    if (!file.delete()) return false;
                } else if (file.isDirectory()) {
                    if (!deleteDir(file)) return false;
                }
            }
        }
        return dir.delete();
    }


    public static boolean createDir(String path) {
        File file = new File(path);
        if (!file.exists()) {
            if (!file.mkdirs()) {
                //  FL.d(TAG, "创建失败,请检查路径和是否配置文件权限!");
                return false;
            }
            return true;
        }
        return false;
    }

    /**
     * Delete the file.
     *
     * @param srcFilePath The path of source file.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean deleteFile(final String srcFilePath) {
        return deleteFile(getFileByPath(srcFilePath));
    }

    /**
     * Delete the file.
     *
     * @param file The file.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean deleteFile(final File file) {
        return file != null && (!file.exists() || file.isFile() && file.delete());
    }

    /**
     * Delete the all in directory.
     *
     * @param dirPath The path of directory.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean deleteAllInDir(final String dirPath) {
        return deleteAllInDir(getFileByPath(dirPath));
    }

    /**
     * Delete the all in directory.
     *
     * @param dir The directory.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean deleteAllInDir(final File dir) {
        return deleteFilesInDirWithFilter(dir, new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                return true;
            }
        });
    }

    /**
     * Delete all files in directory.
     *
     * @param dirPath The path of directory.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean deleteFilesInDir(final String dirPath) {
        return deleteFilesInDir(getFileByPath(dirPath));
    }

    /**
     * Delete all files in directory.
     *
     * @param dir The directory.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean deleteFilesInDir(final File dir) {
        return deleteFilesInDirWithFilter(dir, new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                return pathname.isFile();
            }
        });
    }

    /**
     * Delete all files that satisfy the filter in directory.
     *
     * @param dirPath The path of directory.
     * @param filter  The filter.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean deleteFilesInDirWithFilter(final String dirPath,
                                                     final FileFilter filter) {
        return deleteFilesInDirWithFilter(getFileByPath(dirPath), filter);
    }

    /**
     * Delete all files that satisfy the filter in directory.
     *
     * @param dir    The directory.
     * @param filter The filter.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean deleteFilesInDirWithFilter(final File dir, final FileFilter filter) {
        if (dir == null) return false;
        // dir doesn't exist then return true
        if (!dir.exists()) return true;
        // dir isn't a directory then return false
        if (!dir.isDirectory()) return false;
        File[] files = dir.listFiles();
        if (files != null && files.length != 0) {
            for (File file : files) {
                if (filter.accept(file)) {
                    if (file.isFile()) {
                        if (!file.delete()) return false;
                    } else if (file.isDirectory()) {
                        if (!deleteDir(file)) return false;
                    }
                }
            }
        }
        return true;
    }

    /**
     * Return the files in directory.
     * <p>Doesn't traverse subdirectories</p>
     *
     * @param dirPath The path of directory.
     * @return the files in directory
     */
    public static List<File> listFilesInDir(final String dirPath) {
        return listFilesInDir(dirPath, false);
    }

    /**
     * Return the files in directory.
     * <p>Doesn't traverse subdirectories</p>
     *
     * @param dir The directory.
     * @return the files in directory
     */
    public static List<File> listFilesInDir(final File dir) {
        return listFilesInDir(dir, false);
    }

    /**
     * Return the files in directory.
     *
     * @param dirPath     The path of directory.
     * @param isRecursive True to traverse subdirectories, false otherwise.
     * @return the files in directory
     */
    public static List<File> listFilesInDir(final String dirPath, final boolean isRecursive) {
        return listFilesInDir(getFileByPath(dirPath), isRecursive);
    }

    /**
     * Return the files in directory.
     *
     * @param dir         The directory.
     * @param isRecursive True to traverse subdirectories, false otherwise.
     * @return the files in directory
     */
    public static List<File> listFilesInDir(final File dir, final boolean isRecursive) {
        return listFilesInDirWithFilter(dir, new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                return true;
            }
        }, isRecursive);
    }

    /**
     * Return the files that satisfy the filter in directory.
     * <p>Doesn't traverse subdirectories</p>
     *
     * @param dirPath The path of directory.
     * @param filter  The filter.
     * @return the files that satisfy the filter in directory
     */
    public static List<File> listFilesInDirWithFilter(final String dirPath,
                                                      final FileFilter filter) {
        return listFilesInDirWithFilter(getFileByPath(dirPath), filter, false);
    }

    /**
     * Return the files that satisfy the filter in directory.
     * <p>Doesn't traverse subdirectories</p>
     *
     * @param dir    The directory.
     * @param filter The filter.
     * @return the files that satisfy the filter in directory
     */
    public static List<File> listFilesInDirWithFilter(final File dir,
                                                      final FileFilter filter) {
        return listFilesInDirWithFilter(dir, filter, false);
    }

    /**
     * Return the files that satisfy the filter in directory.
     *
     * @param dirPath     The path of directory.
     * @param filter      The filter.
     * @param isRecursive True to traverse subdirectories, false otherwise.
     * @return the files that satisfy the filter in directory
     */
    public static List<File> listFilesInDirWithFilter(final String dirPath,
                                                      final FileFilter filter,
                                                      final boolean isRecursive) {
        return listFilesInDirWithFilter(getFileByPath(dirPath), filter, isRecursive);
    }

    /**
     * Return the files that satisfy the filter in directory.
     *
     * @param dir         The directory.
     * @param filter      The filter.
     * @param isRecursive True to traverse subdirectories, false otherwise.
     * @return the files that satisfy the filter in directory
     */
    public static List<File> listFilesInDirWithFilter(final File dir,
                                                      final FileFilter filter,
                                                      final boolean isRecursive) {
        if (!isDir(dir)) return null;
        List<File> list = new ArrayList<>();
        File[] files = dir.listFiles();
        if (files != null && files.length != 0) {
            for (File file : files) {
                if (filter.accept(file)) {
                    list.add(file);
                }
                if (isRecursive && file.isDirectory()) {
                    //noinspection ConstantConditions
                    list.addAll(listFilesInDirWithFilter(file, filter, true));
                }
            }
        }
        return list;
    }

    /**
     * Return the time that the file was last modified.
     *
     * @param filePath The path of file.
     * @return the time that the file was last modified
     */

    public static long getFileLastModified(final String filePath) {
        return getFileLastModified(getFileByPath(filePath));
    }

    /**
     * Return the time that the file was last modified.
     *
     * @param file The file.
     * @return the time that the file was last modified
     */
    public static long getFileLastModified(final File file) {
        if (file == null) return -1;
        return file.lastModified();
    }

    /**
     * Return the charset of file simply.
     *
     * @param filePath The path of file.
     * @return the charset of file simply
     */
    public static String getFileCharsetSimple(final String filePath) {
        return getFileCharsetSimple(getFileByPath(filePath));
    }

    /**
     * Return the charset of file simply.
     *
     * @param file The file.
     * @return the charset of file simply
     */
    public static String getFileCharsetSimple(final File file) {
        int p = 0;
        InputStream is = null;
        try {
            is = new BufferedInputStream(new FileInputStream(file));
            p = (is.read() << 8) + is.read();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        switch (p) {
            case 0xefbb:
                return "UTF-8";
            case 0xfffe:
                return "Unicode";
            case 0xfeff:
                return "UTF-16BE";
            default:
                return "GBK";
        }
    }

    /**
     * Return the number of lines of file.
     *
     * @param filePath The path of file.
     * @return the number of lines of file
     */
    public static int getFileLines(final String filePath) {
        return getFileLines(getFileByPath(filePath));
    }

    /**
     * Return the number of lines of file.
     *
     * @param file The file.
     * @return the number of lines of file
     */
    public static int getFileLines(final File file) {
        int count = 1;
        InputStream is = null;
        try {
            is = new BufferedInputStream(new FileInputStream(file));
            byte[] buffer = new byte[1024];
            int readChars;
            if (LINE_SEP.endsWith("\n")) {
                while ((readChars = is.read(buffer, 0, 1024)) != -1) {
                    for (int i = 0; i < readChars; ++i) {
                        if (buffer[i] == '\n') ++count;
                    }
                }
            } else {
                while ((readChars = is.read(buffer, 0, 1024)) != -1) {
                    for (int i = 0; i < readChars; ++i) {
                        if (buffer[i] == '\r') ++count;
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return count;
    }

    /**
     * Return the size of directory.
     *
     * @param dirPath The path of directory.
     * @return the size of directory
     */
    public static String getDirSize(final String dirPath) {
        return getDirSize(getFileByPath(dirPath));
    }

    /**
     * Return the size of directory.
     *
     * @param dir The directory.
     * @return the size of directory
     */
    public static String getDirSize(final File dir) {
        long len = getDirLength(dir);
        return len == -1 ? "" : byte2FitMemorySize(len);
    }

    /**
     * Return the length of file.
     *
     * @param filePath The path of file.
     * @return the length of file
     */
    public static String getFileSize(final String filePath) {
        long len = getFileLength(filePath);
        return len == -1 ? "" : byte2FitMemorySize(len);
    }

    /**
     * Return the length of file.
     *
     * @param file The file.
     * @return the length of file
     */
    public static String getFileSize(final File file) {
        long len = getFileLength(file);
        return len == -1 ? "" : byte2FitMemorySize(len);
    }

    /**
     * Return the length of directory.
     *
     * @param dirPath The path of directory.
     * @return the length of directory
     */
    public static long getDirLength(final String dirPath) {
        return getDirLength(getFileByPath(dirPath));
    }

    /**
     * Return the length of directory.
     *
     * @param dir The directory.
     * @return the length of directory
     */
    public static long getDirLength(final File dir) {
        if (!isDir(dir)) return -1;
        long len = 0;
        File[] files = dir.listFiles();
        if (files != null && files.length != 0) {
            for (File file : files) {
                if (file.isDirectory()) {
                    len += getDirLength(file);
                } else {
                    len += file.length();
                }
            }
        }
        return len;
    }

    /**
     * Return the length of file.
     *
     * @param filePath The path of file.
     * @return the length of file
     */
    public static long getFileLength(final String filePath) {
        boolean isURL = filePath.matches("[a-zA-z]+://[^\\s]*");
        if (isURL) {
            try {
                HttpURLConnection conn = (HttpURLConnection) new URL(filePath).openConnection();
                conn.setRequestProperty("Accept-Encoding", "identity");
                conn.connect();
                if (conn.getResponseCode() == 200) {
                    return conn.getContentLength();
                }
                return -1;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return getFileLength(getFileByPath(filePath));
    }

    /**
     * Return the length of file.
     *
     * @param file The file.
     * @return the length of file
     */
    public static long getFileLength(final File file) {
        if (!isFile(file)) return -1;
        return file.length();
    }

    /**
     * Return the MD5 of file.
     *
     * @param filePath The path of file.
     * @return the md5 of file
     */
    public static String getFileMD5ToString(final String filePath) {
        File file = isSpace(filePath) ? null : new File(filePath);
        return getFileMD5ToString(file);
    }

    /**
     * Return the MD5 of file.
     *
     * @param file The file.
     * @return the md5 of file
     */
    public static String getFileMD5ToString(final File file) {
        return bytes2HexString(getFileMD5(file));
    }

    /**
     * Return the MD5 of file.
     *
     * @param filePath The path of file.
     * @return the md5 of file
     */
    public static byte[] getFileMD5(final String filePath) {
        return getFileMD5(getFileByPath(filePath));
    }

    /**
     * Return the MD5 of file.
     *
     * @param file The file.
     * @return the md5 of file
     */
    public static byte[] getFileMD5(final File file) {
        if (file == null) return null;
        DigestInputStream dis = null;
        try {
            FileInputStream fis = new FileInputStream(file);
            MessageDigest md = MessageDigest.getInstance("MD5");
            dis = new DigestInputStream(fis, md);
            byte[] buffer = new byte[1024 * 256];
            while (true) {
                if (!(dis.read(buffer) > 0)) break;
            }
            md = dis.getMessageDigest();
            return md.digest();
        } catch (NoSuchAlgorithmException | IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (dis != null) {
                    dis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * Return the file's path of directory.
     *
     * @param file The file.
     * @return the file's path of directory
     */
    public static String getDirName(final File file) {
        if (file == null) return "";
        return getDirName(file.getAbsolutePath());
    }

    /**
     * Return the file's path of directory.
     *
     * @param filePath The path of file.
     * @return the file's path of directory
     */
    public static String getDirName(final String filePath) {
        if (isSpace(filePath)) return "";
        int lastSep = filePath.lastIndexOf(File.separator);
        return lastSep == -1 ? "" : filePath.substring(0, lastSep + 1);
    }

    /**
     * Return the name of file.
     *
     * @param file The file.
     * @return the name of file
     */
    public static String getFileName(final File file) {
        if (file == null) return "";
        return getFileName(file.getAbsolutePath());
    }

    /**
     * Return the name of file.
     *
     * @param filePath The path of file.
     * @return the name of file
     */
    public static String getFileName(final String filePath) {
        if (isSpace(filePath)) return "";
        int lastSep = filePath.lastIndexOf(File.separator);
        return lastSep == -1 ? filePath : filePath.substring(lastSep + 1);
    }

    /**
     * Return the name of file without extension.
     *
     * @param file The file.
     * @return the name of file without extension
     */
    public static String getFileNameNoExtension(final File file) {
        if (file == null) return "";
        return getFileNameNoExtension(file.getPath());
    }

    /**
     * Return the name of file without extension.
     *
     * @param filePath The path of file.
     * @return the name of file without extension
     */
    public static String getFileNameNoExtension(final String filePath) {
        if (isSpace(filePath)) return "";
        int lastPoi = filePath.lastIndexOf('.');
        int lastSep = filePath.lastIndexOf(File.separator);
        if (lastSep == -1) {
            return (lastPoi == -1 ? filePath : filePath.substring(0, lastPoi));
        }
        if (lastPoi == -1 || lastSep > lastPoi) {
            return filePath.substring(lastSep + 1);
        }
        return filePath.substring(lastSep + 1, lastPoi);
    }

    /**
     * Return the extension of file.
     *
     * @param file The file.
     * @return the extension of file
     */
    public static String getFileExtension(final File file) {
        if (file == null) return "";
        return getFileExtension(file.getPath());
    }

    /**
     * Return the extension of file.
     *
     * @param filePath The path of file.
     * @return the extension of file
     */
    public static String getFileExtension(final String filePath) {
        if (isSpace(filePath)) return "";
        int lastPoi = filePath.lastIndexOf('.');
        int lastSep = filePath.lastIndexOf(File.separator);
        if (lastPoi == -1 || lastSep >= lastPoi) return "";
        return filePath.substring(lastPoi + 1);
    }

    public interface OnReplaceListener {
        boolean onReplace();
    }

    private static final char HEX_DIGITS[] =
            {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};

    private static String bytes2HexString(final byte[] bytes) {
        if (bytes == null) return "";
        int len = bytes.length;
        if (len <= 0) return "";
        char[] ret = new char[len << 1];
        for (int i = 0, j = 0; i < len; i++) {
            ret[j++] = HEX_DIGITS[bytes[i] >> 4 & 0x0f];
            ret[j++] = HEX_DIGITS[bytes[i] & 0x0f];
        }
        return new String(ret);
    }

    private static String byte2FitMemorySize(final long byteNum) {
        if (byteNum < 0) {
            return "shouldn't be less than zero!";
        } else if (byteNum < 1024) {
            return String.format(Locale.getDefault(), "%.1fB", (double) byteNum);
        } else if (byteNum < 1048576) {
            return String.format(Locale.getDefault(), "%.1fKB", (double) byteNum / 1024);
        } else if (byteNum < 1073741824) {
            return String.format(Locale.getDefault(), "%.1fMB", (double) byteNum / 1048576);
        } else {
            return String.format(Locale.getDefault(), "%.1fGB", (double) byteNum / 1073741824);
        }
    }

    private static boolean isSpace(final String s) {
        if (s == null) return true;
        for (int i = 0, len = s.length(); i < len; ++i) {
            if (!Character.isWhitespace(s.charAt(i))) {
                return false;
            }
        }
        return true;
    }

    private static boolean writeFileFromIS(final File file,
                                           final InputStream is) {
        OutputStream os = null;
        try {
            os = new BufferedOutputStream(new FileOutputStream(file));
            byte data[] = new byte[8192];
            int len;
            while ((len = is.read(data, 0, 8192)) != -1) {
                os.write(data, 0, len);
            }
            return true;
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        } finally {
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (os != null) {
                    os.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 写入文件
     */
    public static void writeFrame(String fileName, byte[] data) {
        try {
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(fileName));
            bos.write(data);
            bos.flush();
            bos.close();
            // Log.e(TAG, "" + data.length + " bytes have been written to " + filesDir + fileName + ".jpg");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 拷贝文件
     * 使用文件通道,提高文件复制效率
     *
     * @param fromFile 来源文件地址
     * @param toFile   复制后文件地址
     * @return 是否复制成功
     */
    public static boolean copyFileWithChannel(String fromFile, String toFile) {
        FileInputStream fsIn = null;
        FileOutputStream fsOut = null;
        FileChannel fcIn = null;
        FileChannel fcOut = null;
        try {
            fsIn = new FileInputStream(fromFile);
            fsOut = new FileOutputStream(toFile);
            // 得到对应文件通道
            fcIn = fsIn.getChannel();
            fcOut = fsOut.getChannel();
            // 连接两个通道,并且从in通道读取,然后写入out通道
            fcIn.transferTo(0, fcIn.size(), fcOut);
            return true;
        } catch (Exception ex) {
            ex.printStackTrace();
            return false;
        } finally {
            try {
                if (fsIn != null) {
                    fsIn.close();
                }
                if (fsOut != null) {
                    fsOut.close();
                }
                if (fcIn != null) {
                    fcIn.close();
                }
                if (fcOut != null) {
                    fcOut.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void saveRgbaDataToFile(File file, byte[] rgba, int width, int height) {
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        ByteBuffer buf = ByteBuffer.wrap(rgba);
        bitmap.copyPixelsFromBuffer(buf);
        try {
            FileOutputStream fos = new FileOutputStream(file);
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
            fos.flush();
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void saveRgbaDataToFile(File file, ByteBuffer rgba, int width, int height) {
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        rgba.rewind();
        bitmap.copyPixelsFromBuffer(rgba);
        try {
            FileOutputStream fos = new FileOutputStream(file);
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
            fos.flush();
            fos.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * Copy the file from assets.
     *
     * @param assetsFilePath The path of file in assets.
     * @param destFilePath   The path of destination file.
     * @return {@code true}: success<br>{@code false}: fail
     */
    public static boolean copyFileFromAssets(final String assetsFilePath, final String destFilePath) {
        boolean res = true;
        try {
            String[] assets = CameraApp.Companion.getMInstance().getAssets().list(assetsFilePath);
            if (assets.length > 0) {
                for (String asset : assets) {
                    res &= copyFileFromAssets(assetsFilePath + "/" + asset, destFilePath + "/" + asset);
                }
            } else {
                res = writeFileFromIS(
                        new File(destFilePath),
                        CameraApp.Companion.getMInstance().getAssets().open(assetsFilePath)
                );
            }
        } catch (IOException e) {
            e.printStackTrace();
            res = false;
        }
        return res;
    }

    /**
     * 获取jpg图片输出路径
     *
     * @return 输出路径
     */
    public static Object getHeadJpgFile() {
        String fileName = System.currentTimeMillis() + FileManager.JPG_SUFFIX;
        String headPath = FileManager.getAvatarPath(fileName);
        File imgFile;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            imgFile = new File(headPath);
            // 通过 MediaStore API 插入file 为了拿到系统裁剪要保存到的uri(因为App没有权限不能访问公共存储空间,需要通过 MediaStore API来操作)
            ContentValues values = new ContentValues();
            values.put(MediaStore.Images.Media.DATA, imgFile.getAbsolutePath());
            values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName);
            values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
            return CameraApp.Companion.getMInstance().getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
        } else {
            imgFile = new File(headPath);
        }
        return imgFile;
    }

    public static void saveBitmap(Bitmap bitmap, String path) {
        File filePic;
        try {
            filePic = new File(path);
            if (!filePic.exists()) {
                filePic.getParentFile().mkdirs();
                filePic.createNewFile();
            }
            FileOutputStream fos = new FileOutputStream(filePic);
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
            fos.flush();
            fos.close();
        } catch (IOException e) {
            Log.e("tag", "saveBitmap: " + e.getMessage());
            return;
        }
        Log.i("tag", "saveBitmap success: " + filePic.getAbsolutePath());
    }

    /**
     * 从File获取Uri
     * 此方式获取视频Uri原生视频分享报导入失败,解决方式参考 getVideoUri()
     *
     * @param file 文件
     * @return uri
     */
    public static Uri getUriFromFile(File file, Application application) {
        Uri mUri;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            Log.w("lzq", "name:" + application.getPackageName());
            mUri = FileProvider.getUriForFile(application,
                    application.getPackageName() + ".fileprovider", file);
        } else {
            mUri = Uri.fromFile(file);
        }
        return mUri;
    }


    /**
     * 从文件路径获取uri
     *
     * @param path 文件路径
     * @return uri
     */
    public static Uri getUriFromPath(String path, Application application) {
        File file = new File(path);
        if (file.exists()) {
            return getUriFromFile(file, application);
        } else {
            Log.e("File is not exist","");
            return null;
        }
    }

    /**
     * 保存rgba数据为jpg文件
     *
     * @param bytes  rgba数据
     * @param width  宽度
     * @param height 高度
     * @param file   保存文件
     */

    public static void saveRgbaDataToFile(byte[] bytes, int width, int height, File file) {
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        ByteBuffer buf = ByteBuffer.wrap(bytes);
        bitmap.copyPixelsFromBuffer(buf);

        try {
            FileOutputStream fos = new FileOutputStream(file);
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
            fos.flush();
            fos.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static File createFile(String path) {
        File file = new File(path);
        if (!file.getParentFile().exists()) {
            // FL.d(TAG, "目标文件所在路径不存在,准备创建……");
            if (!createDir(file.getParent())) {
                // FL.d(TAG, "创建目录文件所在的目录失败!文件路径【" + path + "】");
            }
        }
        // 创建目标文件
        try {
            if (!file.exists()) {
                if (file.createNewFile()) {
                    //  FL.d(TAG, "创建文件成功:" + file.getAbsolutePath());
                }
                return file;
            } else {
                return file;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 图片压缩并且存储
     *
     * @param targetFile   保存目标文件
     * @param originalPath 图片地址
     * @param scale        图片缩放值
     */
    public static void compressAndSaveImage(File targetFile, String originalPath, int scale) {
        Bitmap bitmap = null;
        BufferedInputStream bufferedInputStream = null;
        FileOutputStream fileOutputStream = null;
        try {
            // 1、得到图片的宽、高
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            bufferedInputStream = new BufferedInputStream(new FileInputStream(originalPath));
            bitmap = BitmapFactory.decodeStream(bufferedInputStream, null, options);
            if (bitmap != null) {
                bitmap.recycle();
            }
            bufferedInputStream.close();

            int originalImageWidth = options.outWidth;
            int originalImageHeight = options.outHeight;

            // 2、获取图片方向
            int orientation = getImageOrientation(originalPath);
            int rotate = 0;
            switch (orientation) { // 判断是否需要旋转
                case ExifInterface.ORIENTATION_ROTATE_270:
                    rotate = -90;
                    break;
                case ExifInterface.ORIENTATION_ROTATE_180:
                    rotate = 180;
                    break;
                case ExifInterface.ORIENTATION_ROTATE_90:
                    rotate = 90;
                    break;
            }

            // 3、计算图片压缩inSampleSize
            int maxValue = Math.max(originalImageWidth, originalImageHeight);
            if (maxValue > 3000) {
                options.inSampleSize = scale * 3;
            } else if (maxValue > 2000) {
                options.inSampleSize = scale * 2;
            } else if (maxValue > 1500) {
                options.inSampleSize = (int) (scale * 1.5);
            } else if (maxValue > 1000) {
                options.inSampleSize = (int) (scale * 1.1);
            } else {
                options.inSampleSize = scale;
            }
            options.inJustDecodeBounds = false;

            // 4、图片方向纠正和压缩(生成缩略图)
            bufferedInputStream = new BufferedInputStream(new FileInputStream(originalPath));
            bitmap = BitmapFactory.decodeStream(bufferedInputStream, null, options);
            bufferedInputStream.close();

            if (bitmap == null) {
                return;
            }

            targetFile.getParentFile().mkdirs();

            fileOutputStream = new FileOutputStream(targetFile);
            if (rotate != 0) {
                Matrix matrix = new Matrix();
                matrix.setRotate(rotate);
                Bitmap bitmapOld = bitmap;
                bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
                        bitmap.getHeight(), matrix, false);
                bitmapOld.recycle();
            }
            // 5、保存图片
            bitmap.compress(Bitmap.CompressFormat.JPEG, 70, fileOutputStream);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (bufferedInputStream != null) {
                    bufferedInputStream.close();
                }
                if (fileOutputStream != null) {
                    fileOutputStream.flush();
                }
                if (fileOutputStream != null) {
                    fileOutputStream.close();
                }
            } catch (IOException io) {
                io.printStackTrace();
            }
            if (bitmap != null && bitmap.isRecycled()) {
                bitmap.recycle();
            }
        }
    }

    /**
     * 获取一张图片在手机上的方向值
     */
    public static int getImageOrientation(String uri) throws IOException {
        if (!new File(uri).exists()) {
            return 0;
        }
        return new ExifInterface(uri).getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
    }

    /**
     * 根据Uri获取图片绝对路径,解决Android4.4以上版本Uri转换
     *
     * @param context  上下文
     * @param imageUri 图片地址
     */
    public static String getImageAbsolutePath(Activity context, Uri imageUri) {
        if (context == null || imageUri == null)
            return null;
        if (DocumentsContract.isDocumentUri(context, imageUri)) {
            if (isExternalStorageDocument(imageUri)) {
                String docId = DocumentsContract.getDocumentId(imageUri);
                String[] split = docId.split(":");
                String type = split[0];
                if ("primary".equalsIgnoreCase(type)) {
                    return Environment.getExternalStorageDirectory() + "/" + split[1];
                }
            } else if (isDownloadsDocument(imageUri)) {
                String id = DocumentsContract.getDocumentId(imageUri);
                Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.parseLong(id));
                return getDataColumn(context, contentUri, null, null);
            } else if (isMediaDocument(imageUri)) {
                String docId = DocumentsContract.getDocumentId(imageUri);
                String[] split = docId.split(":");
                String type = split[0];
                Uri contentUri = null;
                if ("image".equals(type)) {
                    contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
                } else if ("video".equals(type)) {
                    contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
                } else if ("audio".equals(type)) {
                    contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
                }
                String selection = MediaStore.Images.Media._ID + "=?";
                String[] selectionArgs = new String[]{split[1]};
                return getDataColumn(context, contentUri, selection, selectionArgs);
            }
        } // MediaStore (and general)
        else if ("content".equalsIgnoreCase(imageUri.getScheme())) {
            // Return the remote address
            if (isGooglePhotosUri(imageUri))
                return imageUri.getLastPathSegment();
            return getDataColumn(context, imageUri, null, null);
        }
        // File
        else if ("file".equalsIgnoreCase(imageUri.getScheme())) {
            return imageUri.getPath();
        }
        return null;
    }

    /**
     * @param uri The Uri to checkRemote.
     * @return Whether the Uri authority is ExternalStorageProvider.
     */
    public static boolean isExternalStorageDocument(Uri uri) {
        return "com.android.externalstorage.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to checkRemote.
     * @return Whether the Uri authority is DownloadsProvider.
     */
    public static boolean isDownloadsDocument(Uri uri) {
        return "com.android.providers.downloads.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to checkRemote.
     * @return Whether the Uri authority is MediaProvider.
     */
    public static boolean isMediaDocument(Uri uri) {
        return "com.android.providers.media.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to checkRemote.
     * @return Whether the Uri authority is Google Photos.
     */
    public static boolean isGooglePhotosUri(Uri uri) {
        return "com.google.android.apps.photos.content".equals(uri.getAuthority());
    }

    public static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) {
        String column = MediaStore.Images.Media.DATA;
        String[] projection = {column};
        try (Cursor cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null)) {
            if (cursor != null && cursor.moveToFirst()) {
                int index = cursor.getColumnIndexOrThrow(column);
                return cursor.getString(index);
            }
        }
        return null;
    }
}

12.VideoFileUtils

package com.example.cameraxdemo.utils

import android.content.Context
import android.os.Environment
import com.example.cameraxdemo.R
import java.io.File
import java.text.SimpleDateFormat
import java.util.Locale

/**
 *@author: njb
 *@date:   2023/8/15 17:13
 *@desc:
 */
object VideoFileUtils {
    /**
     * 获取视频文件路径
     */
    fun getVideoName(): String {
        val videoPath = Environment.getExternalStorageDirectory().toString() + "/CameraXApp"
        val dir = File(videoPath)
        if (!dir.exists() && !dir.mkdirs()) {
            ToastUtils.shortToast("文件不存在")
        }
        return videoPath
    }

    /**
     * 获取图片文件路径
     */
    fun getImageFileName(): String {
        val imagePath = Environment.getExternalStorageDirectory().toString() + "/images"
        val dir = File(imagePath)
        if (!dir.exists() && !dir.mkdirs()) {
            ToastUtils.shortToast("文件不存在")
        }
        return imagePath
    }

    /**
     * 拍照文件保存路径
     * @param context
     * @return
     */
    fun getPhotoDir(context: Context?): String? {
        return FileManager.getFolderDirPath(
            "DCIM/Camera/CameraXApp/photo"
        )
    }

    /**
     * 视频文件保存路径
     * @param context
     * @return
     */
    fun getVideoDir(): String? {
        return FileManager.getFolderDirPath(
            "DCIM/Camera/CameraXApp/video"
        )
    }

    /** Use external media if it is available, our app's file directory otherwise */
    fun getOutputDirectory(context: Context): File {
        val appContext = context.applicationContext
        val mediaDir = context.externalMediaDirs.firstOrNull()?.let {
            File(it, appContext.resources.getString(R.string.app_name)).apply { mkdirs() }
        }
        return if (mediaDir != null && mediaDir.exists())
            mediaDir else appContext.filesDir
    }

    fun createFile(baseFolder: File, format: String, extension: String) =
        File(
            baseFolder, SimpleDateFormat(format, Locale.US)
                .format(System.currentTimeMillis()) + extension
        )
}

13.CameraApp:

package com.example.cameraxdemo.app

import android.app.Application

/**
 *@author: njb
 *@date:   2023/8/15 17:07
 *@desc:
 */
class CameraApp : Application() {

    override fun onCreate() {
        super.onCreate()
        mInstance = this
    }

    companion object {
        lateinit var mInstance: CameraApp
            private set
    }
}

14.完整的示例代码如下:

package com.example.cameraxdemo

import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.webkit.MimeTypeMap
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.AspectRatio
import androidx.camera.core.Camera
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.core.VideoCapture
import androidx.camera.core.VideoCapture.OnVideoSavedCallback
import androidx.camera.core.VideoCapture.OutputFileOptions
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.net.toFile
import com.blankj.utilcode.util.LogUtils
import com.example.cameraxdemo.activity.CameraActivity
import com.example.cameraxdemo.utils.Constants
import com.example.cameraxdemo.utils.Constants.Companion.DATE_FORMAT
import com.example.cameraxdemo.utils.Constants.Companion.PHOTO_EXTENSION
import com.example.cameraxdemo.utils.Constants.Companion.REQUIRED_PERMISSIONS
import com.example.cameraxdemo.utils.FileManager
import com.example.cameraxdemo.utils.ToastUtils
import com.example.cameraxdemo.utils.VideoFileUtils.createFile
import com.example.cameraxdemo.utils.VideoFileUtils.getOutputDirectory
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

class MainActivity : AppCompatActivity() {
    private var imageCamera: ImageCapture? = null
    private lateinit var cameraExecutor: ExecutorService
    private var videoCapture: VideoCapture? = null
    private var cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA//当前相机
    private var preview: Preview? = null//预览对象
    private var cameraProvider: ProcessCameraProvider? = null//相机信息
    private lateinit var camera: Camera //相机对象
    private var isRecordVideo: Boolean = false
    private val TAG = "CameraXApp"
    private lateinit var outputDirectory: File
    private var lensFacing: Int = CameraSelector.LENS_FACING_BACK
    private val btnCameraCapture: Button by lazy { findViewById(R.id.btnCameraCapture) }
    private val btnVideo: Button by lazy { findViewById(R.id.btnVideo) }
    private val btnSwitch: Button by lazy { findViewById(R.id.btnSwitch) }
    private val btnOpenCamera: Button by lazy { findViewById(R.id.btnOpenCamera) }
    private val viewFinder: PreviewView by lazy { findViewById(R.id.mPreviewView) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initPermission()
        initView()
    }

    private fun initView() {
        outputDirectory = getOutputDirectory(this)
    }

    @SuppressLint("RestrictedApi")
    private fun initListener() {
        btnCameraCapture.setOnClickListener {
            takePhoto()
        }
        btnVideo.setOnClickListener {
            if (!isRecordVideo) {
                takeVideo()
                isRecordVideo = true
                btnVideo.text = "停止录像"
            } else {
                isRecordVideo = false
                videoCapture?.stopRecording()//停止录制
                //preview?.clear()//清除预览
                btnVideo.text = "开始录像"
            }
        }
        btnSwitch.setOnClickListener {
            cameraSelector = if (cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA) {
                CameraSelector.DEFAULT_FRONT_CAMERA
            } else {
                CameraSelector.DEFAULT_BACK_CAMERA
            }
            if (!isRecordVideo) {
                startCamera()
            }
        }
        btnOpenCamera.setOnClickListener {
            val intent = Intent(this, CameraActivity::class.java)
            startActivity(intent)
        }
    }


    private fun initPermission() {
        if (checkPermissions()) {
            // ImageCapture
            startCamera()
        } else {
            requestPermission()
        }
    }

    private fun requestPermission() {
        when {
            Build.VERSION.SDK_INT >= 33 -> {
                ActivityCompat.requestPermissions(
                    this,
                    arrayOf(Manifest.permission.READ_MEDIA_IMAGES,Manifest.permission.READ_MEDIA_AUDIO,Manifest.permission.READ_MEDIA_VIDEO,Manifest.permission.CAMERA,Manifest.permission.RECORD_AUDIO),
                    Constants.REQUEST_CODE_PERMISSIONS
                )
            }

            else -> {
                ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, Constants.REQUEST_CODE_PERMISSIONS)
            }
        }
    }


    /**
     * 开始拍照
     */
    private fun takePhoto() {
        val imageCapture = imageCamera ?: return
        val photoFile = createFile(outputDirectory, DATE_FORMAT, PHOTO_EXTENSION)
        val metadata = ImageCapture.Metadata().apply {
            // Mirror image when using the front camera
            isReversedHorizontal = lensFacing == CameraSelector.LENS_FACING_FRONT
        }
        val outputOptions =
            ImageCapture.OutputFileOptions.Builder(photoFile).setMetadata(metadata).build()
        imageCapture.takePicture(outputOptions, ContextCompat.getMainExecutor(this),
            object : ImageCapture.OnImageSavedCallback {
                override fun onError(exc: ImageCaptureException) {
                    LogUtils.e(TAG, "Photo capture failed: ${exc.message}", exc)
                    ToastUtils.shortToast(" 拍照失败 ${exc.message}")
                }

                override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                    val savedUri = output.savedUri ?: Uri.fromFile(photoFile)
                    ToastUtils.shortToast(" 拍照成功 $savedUri")
                    LogUtils.e(TAG, savedUri.path.toString())
                    val mimeType = MimeTypeMap.getSingleton()
                        .getMimeTypeFromExtension(savedUri.toFile().extension)
                    MediaScannerConnection.scanFile(
                        this@MainActivity,
                        arrayOf(savedUri.toFile().absolutePath),
                        arrayOf(mimeType)
                    ) { _, uri ->
                        LogUtils.d(
                            TAG,
                            "Image capture scanned into media store: ${uri.path.toString()}"
                        )
                    }
                }
            })
    }


    /**
     * 开始录像
     */
    @SuppressLint("RestrictedApi", "ClickableViewAccessibility", "MissingPermission")
    private fun takeVideo() {
        //开始录像
        try {
            isRecordVideo = true
            val mFileDateFormat = SimpleDateFormat(DATE_FORMAT, Locale.US)
            //视频保存路径
            val file =
                File(FileManager.getCameraVideoPath(), mFileDateFormat.format(Date()) + ".mp4")
            val outputOptions = OutputFileOptions.Builder(file)
            videoCapture?.startRecording(
                outputOptions.build(),
                Executors.newSingleThreadExecutor(),
                object : OnVideoSavedCallback {
                    override fun onVideoSaved(outputFileResults: VideoCapture.OutputFileResults) {
                        isRecordVideo = false
                        LogUtils.d(TAG, "===视频保存的地址为=== ${file.absolutePath}")
                        //保存视频成功回调,会在停止录制时被调用
                        ToastUtils.shortToast(" 录像成功 $file")
                    }

                    override fun onError(
                        videoCaptureError: Int,
                        message: String,
                        cause: Throwable?
                    ) {
                        //保存失败的回调,可能在开始或结束录制时被调用
                        isRecordVideo = false
                        LogUtils.e(TAG, "onError: $message")
                        ToastUtils.shortToast(" 录像失败 $message")
                    }
                })
        } catch (e: Exception) {
            e.printStackTrace()
            LogUtils.e(TAG, "===录像出错===${e.message}")
        }
    }

    /**
     * 开始相机预览
     */
    @SuppressLint("RestrictedApi")
    private fun startCamera() {
        cameraExecutor = Executors.newSingleThreadExecutor()
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener(Runnable {
            cameraProvider = cameraProviderFuture.get()//获取相机信息

            //预览配置
            preview = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(viewFinder.surfaceProvider)
                }

            imageCamera = ImageCapture.Builder()
                .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
                .build()

            videoCapture = VideoCapture.Builder()//录像用例配置
                .setTargetAspectRatio(AspectRatio.RATIO_16_9) //设置高宽比
                //.setTargetRotation(viewFinder.display!!.rotation)//设置旋转角度
                .build()
            try {
                cameraProvider?.unbindAll()//先解绑所有用例
                camera = cameraProvider?.bindToLifecycle(
                    this,
                    cameraSelector,
                    preview,
                    imageCamera,
                    videoCapture
                )!!//绑定用例
            } catch (e: Exception) {
                LogUtils.e(TAG, "Use case binding failed", e.message)
            }

        }, ContextCompat.getMainExecutor(this))
        initListener()
    }

    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<String>, grantResults:
        IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
                when (requestCode) {
                    Constants.REQUEST_CODE_PERMISSIONS -> {
                        var allPermissionsGranted = true
                        for (result in grantResults) {
                            if (result != PackageManager.PERMISSION_GRANTED) {
                                allPermissionsGranted = false
                                break
                            }
                        }
                        when {
                            allPermissionsGranted -> {
                                // 权限已授予,执行文件读写操作
                                startCamera()
                            }

                            else -> {
                                // 权限被拒绝,处理权限请求失败的情况
                                ToastUtils.shortToast("请您打开必要权限")
                                requestPermission()
                            }
                        }
                    }
                }
    }

    private fun checkPermissions(): Boolean {
        when {
            Build.VERSION.SDK_INT >= 33 -> {
                val permissions = arrayOf(
                    Manifest.permission.READ_MEDIA_IMAGES,
                    Manifest.permission.READ_MEDIA_AUDIO,
                    Manifest.permission.READ_MEDIA_VIDEO,
                    Manifest.permission.CAMERA,
                    Manifest.permission.RECORD_AUDIO,
                )
                for (permission in permissions) {
                    return Environment.isExternalStorageManager()
                }
            }

            else -> {
                for (permission in REQUIRED_PERMISSIONS) {
                    if (ContextCompat.checkSelfPermission(
                            this,
                            permission
                        ) != PackageManager.PERMISSION_GRANTED
                    ) {
                        return false
                    }
                }
            }
        }
        return true
    }

    override fun onDestroy() {
        super.onDestroy()
        cameraExecutor.shutdown()
    }
}

15.实现的效果如下:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

16.日志打印:

模拟器日志如下:

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
真机日志打印:
在这里插入图片描述
在这里插入图片描述

17.选择相册的代码如下:

package com.example.cameraxdemo.activity

import androidx.appcompat.app.AppCompatActivity
import android.content.ContentValues
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.widget.Button
import android.widget.ImageView
import com.blankj.utilcode.util.LogUtils
import com.bumptech.glide.Glide
import com.example.cameraxdemo.R
import com.example.cameraxdemo.utils.Constants.Companion.REQUEST_CODE_CAMERA
import com.example.cameraxdemo.utils.Constants.Companion.REQUEST_CODE_CROP
import com.example.cameraxdemo.utils.FileManager
import com.example.cameraxdemo.utils.FileUtil
import java.io.File

/**
 *@author: njb
 *@date:   2023/8/15 17:20
 *@desc:
 */
class CameraActivity :AppCompatActivity(){
    private var mUploadImageUri: Uri? = null
    private var mUploadImageFile: File? = null
    private var photoUri: Uri? = null
    private val btnCamera:Button by lazy { findViewById(R.id.btnCamera) }
    private val ivAvatar:ImageView by lazy { findViewById(R.id.iv_avatar) }
    private val TAG = CameraActivity::class.java.name
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_camera)
        initView()
    }

    private fun initView() {
        btnCamera.setOnClickListener {
            startSystemCamera()
        }
    }

    /**
     * 调起系统相机拍照
     */
    private fun startSystemCamera() {
        val takeIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        val values = ContentValues()
        //根据uri查询图片地址
        photoUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
        LogUtils.d(TAG, "photoUri:" + photoUri?.authority + ",photoUri:" + photoUri?.path)
        //放入拍照后的地址
        takeIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
        //调起拍照
        startActivityForResult(
            takeIntent,
            REQUEST_CODE_CAMERA
        )
    }


    /**
     * 设置用户头像
     */
    private fun setAvatar() {
        val file: File? = if (mUploadImageUri != null) {
            FileManager.getMediaUri2File(mUploadImageUri)
        } else {
            mUploadImageFile
        }
        Glide.with(this).load(file).into(ivAvatar)
        LogUtils.d(TAG,"filepath"+ file!!.absolutePath)
    }

    /**
     * 系统裁剪方法
     */
    private fun workCropFun(imgPathUri: Uri?) {
        mUploadImageUri = null
        mUploadImageFile = null
        if (imgPathUri != null) {
            val imageObject: Any = FileUtil.getHeadJpgFile()
            if (imageObject is Uri) {
                mUploadImageUri = imageObject
            }
            if (imageObject is File) {
                mUploadImageFile = imageObject
            }
            val intent = Intent("com.android.camera.action.CROP")
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
            }
            intent.run {
                setDataAndType(imgPathUri, "image/*")// 图片资源
                putExtra("crop", "true") // 裁剪
                putExtra("aspectX", 1) // 宽度比
                putExtra("aspectY", 1) // 高度比
                putExtra("outputX", 150) // 裁剪框宽度
                putExtra("outputY", 150) // 裁剪框高度
                putExtra("scale", true) // 缩放
                putExtra("return-data", false) // true-返回缩略图-data,false-不返回-需要通过Uri
                putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()) // 保存的图片格式
                putExtra("noFaceDetection", true) // 取消人脸识别
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                    putExtra(MediaStore.EXTRA_OUTPUT, mUploadImageUri)
                } else {
                    val imgCropUri = Uri.fromFile(mUploadImageFile)
                    putExtra(MediaStore.EXTRA_OUTPUT, imgCropUri)
                }
            }
            startActivityForResult(
                intent, REQUEST_CODE_CROP
            )
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == RESULT_OK) {
            if (requestCode == REQUEST_CODE_CAMERA) {//拍照回调
                workCropFun(photoUri)
            } else if (requestCode == REQUEST_CODE_CROP) {//裁剪回调
                setAvatar()
            }
        }
    }
}

18.选择相册后的效果如下:

在这里插入图片描述
在这里插入图片描述

19.总结:

今天虽然遇到了不少问题,但是出现问题后调试的过程很安逸,基本找出原因和解决花费了将近3个小时才把完整的demo整理出来,花费这么久的时间有三点原因:

1.Android13适配的规则没有搞清楚就直接升级更换了新版本,导致请求和拒绝后一直出错。

2.CameraX新的api和录像权限没看完整导致这里来回折腾了很久,最后找到了官网api才解决。

3.对于AGP8.1.0使用不熟悉,导致刚开始配置依赖也浪费了一点时间。

路漫漫而修远兮,吾将上下而求索,后面会把整理出来的完整适配Android13的例子也放出来,还有关于AGP8.1.0配置依赖方式变更的也会整理,遇到问题进行求索的过程比结果更重要,只要有一颗战胜困难的心,相信问题最终都会解决,如果感兴趣的可以尝试一下,若有其他问题可以提出来一起讨论解决,共同学习,一起成长.

20.demo源码地址如下:

https://gitee.com/jackning_admin/camera-xdemo

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/886692.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

中远麒麟堡垒机 SQL注入漏洞复现

0x01 产品简介 中远麒麟依托自身强大的研发能力,丰富的行业经验&#xff0c;自主研发了新一代软硬件一体化统一安全运维平台一-iAudit 统一安全运维平台。该产品支持对企业运维人员在运维过程中进行统一身份认证、统一授权、统一审计、统一监控&#xff0c;消除了传统运维过程中…

十七、地物识别

描述了使用2D卷积神经网络图像识别的全过程。下载和安装标注工具,对图像进行标注,生成标注后的图像。然后对数据进行增强,划分训练集和测试集。最后通过神经网络建立分类模型,对现有图片进行分类应用。 1、Labelme工具 Labelme工具是语义分割标注工具,在地物类型或建…

《论文阅读14》FAST-LIO

一、论文 研究领域&#xff1a;激光雷达惯性测距框架论文&#xff1a;FAST-LIO: A Fast, Robust LiDAR-inertial Odometry Package by Tightly-Coupled Iterated Kalman Filter IEEE Robotics and Automation Letters, 2021 香港大学火星实验室 论文链接论文github 二、论文概…

管理类联考——逻辑——真题篇——按知识分类——汇总篇——一、形式逻辑——性质——第三节——真假推理

文章目录 第三节 性质—真假推理题—性质—两命题间的关系—①先找矛盾&#xff0c;后找包含—矛盾&#xff1a;“所有A都是B”矛盾“有的A不是B”&#xff1b;“所有A都不是B”矛盾“有的A是B”。—包含&#xff1a;“所有A都是B”→“有的A是B”&#xff1b;“所有A都不是B”…

【k8s】基于Prometheus监控Kubernetes集群安装部署

目录 基于Prometheus监控Kubernetes集群安装部署 一、环境准备 二、部署kubernetes集群 三、部署Prometheus监控平台 四、部署Grafana服务 五、grafana web操作 基于Prometheus监控Kubernetes集群安装部署 一、环境准备 IP地址 主机名 组件 192.168.100.131 k8s-ma…

信息与通信工程面试准备——数学知识|正态分布|中心极限定理

目录 正态分布 正态分布的参数 正态分布的第一个参数是均值 正态分布的第二个参数是标准差SD 所有正态分布的共同特征 标准正态分布&#xff1a;正态分布的特例 中心极限定理 理解定义 示例# 1 示例# 2 知道样本均值总是正态分布的实际含义是什么&#xff1f; 正态分…

Python发送QQ邮件

使用Python的smtplib可以发送QQ邮件&#xff0c;代码如下 #!/usr/bin/python3 import smtplib from email.mime.text import MIMEText from email.header import Headersender 111qq.com # 发送邮箱 receivers [222qq.com] # 接收邮箱 auth_code "abc" # 授权…

流媒体服务-传输延时(SEI插帧)

什么是延时 很多小伙伴认为&#xff0c;当推流端和拉流端显示的时间不一致&#xff0c;即为延时。 其实这种看法是比较片面的&#xff0c;不同的播放器&#xff0c;对同一路流进行测试&#xff0c;可能会得到不同的结果。 一般来说&#xff0c;延时为以下几个部分的累加组成 …

最近抖音很火的情侣飞行棋

最近抖音很火的情侣飞行棋 最近抖音很火的情侣飞行棋&#xff0c;这款情侣飞行棋提供了丰富的游戏玩法&#xff0c;可以为情侣、朋友或家人带来欢乐的游戏体验。扫码进行体验识别 无论是在家中&#xff0c;还是在聚会、旅行等场合&#xff0c;都可以轻松启动该网站&#xff0c…

为何千万别学网络安全专业(网络安全小白避坑的建议解析)

前言&#xff1a; 近年来&#xff0c;随着国家对网络安全的战略关注和新基建的持续投入&#xff0c;网络安全专业成为一个热门话题。然而&#xff0c;好专业不一定就能找到好工作&#xff0c;对于想从事网络安全专业的小白们&#xff0c;需要持谨慎态度&#xff0c;避免走一些…

案例:用户登录/注册

文章目录 技术框架说明登录案例1.需求分析2.环境准备2.1 前端页面2.2 创建数据表及对应的实体类2.3 导入mybatis坐标&#xff0c;MySQL坐标2.4 配置文件及接口 3. 用户名密码校验4. 前端配置5.Servlet编写 注册案例1.需求分析2.配置用户接口3. 测试添加用户4. 前端配置5. servl…

【概念理解】STM32中的sprintf()函数

sprintf()函数 这个函数在 stdio.h中&#xff1b;可以将格式化的数据写入到一个字符串缓冲区中。 int sprintf(char *str, const char *format, ...);str&#xff1a;指向字符数组的指针&#xff0c;即用于存储格式化后字符串的缓冲区。format&#xff1a;格式化字符串&#…

通过nvm切换nodejs版本

下载&#xff1a; 1.下载nvm地址&#xff1a; https://github.com/coreybutler/nvm-windows/releases 下载该安装包&#xff0c;下载后无需配置就可以使用&#xff0c;十分方便。 简单说明一些包&#xff1a; nvm - noinstall.zip &#xff1a; 这个是绿色免安装版本&#…

c++ std::shared_ptr的线程安全问题(race condition)

有 3 个 shared_ptr 对象 x、g、n; 两个工作线程&#xff1a; void main(){shared_ptr g(new Foo); // 线程之间共享的 shared_ptr shared_ptr x; // 线程 A 的局部变量 shared_ptr n(new Foo); // 线程 B 的局部变量std::thread([&]{x g; }).detach();std::thread([&…

分布式 - 消息队列Kafka:Kafka 消费者的消费位移

文章目录 01. Kafka 分区位移02. Kafka 消费位移03. kafka 消费位移的作用04. Kafka 消费位移的提交05. kafka 消费位移的存储位置06. Kafka 消费位移与消费者提交的位移07. kafka 消费位移的提交时机08. Kafka 维护消费状态跟踪的方法 01. Kafka 分区位移 对于Kafka中的分区而…

每日一题——移动零

移动零 题目链接 思路——双指针 如果可以开辟额外的空间&#xff0c;那这题十分好做。我们开辟和nums同样大小的空间&#xff0c;将遍历数组&#xff0c;将非零元素从头放置&#xff0c;将零从后往前放置&#xff0c;这样就可以将所有的零放到后面&#xff0c;同时保证非零元…

安全狗获批成为算网融合产业及标准推进委员会伙伴单位

近日&#xff0c;安全狗获批成为中国通信标准化协会算网融合产业及标准推进委员会伙伴单位。 据悉&#xff0c;中国通信标准化协会算网融合产业及标准推进委员会&#xff0c;致力于算网融合、数字化转型、SDN/NFV、SD-WAN、新基建、信息安全、边缘计算、高性能计算领域及典型应…

品牌营销|所有产品都值得用 AI 再做一遍

微软 CEO Satya Nadella 曾经说过&#xff1a;“所有的产品都值得用 AI 重做一遍。” AI 大模型的出现&#xff0c;开启了一个全新的智能化时代&#xff0c;重新定义了人机交互。这让生成式 AI 技术变得「触手可得」&#xff0c;也让各行业看到 AGI 驱动商业增长的更大可能性。…

基于注册中心如何实现全链路灰度

1. 为什么需要服务发现? 2. 微服务注册中心 3. 基于注册中心如何实现全链路灰度 4. GRPC 如何结合注册中心 GRPC服务发现与全链路灰度 为什么需要服务发现? 服务拆分 配置调用 如果有很多服务怎么办&#xff1f; 服务注册 服务发现 注册中心的架构 配置与使用 常见的…

西瓜书之神经网络

一&#xff0c;神经元模型 所谓神经网络&#xff0c; 目前用得最广泛的一个定义是“神经网络是由具有适应性的简单单元组成的广泛并行互连的网络&#xff0c;它的组织能够模拟生物神经系统对真实世界物体所做出的交互反应”。 M-P神经元 M-P神经元&#xff1a;接收n个输入(…