惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Y
Y Combinator Blog
美团技术团队
H
Hacker News: Front Page
Spread Privacy
Spread Privacy
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Tenable Blog
Simon Willison's Weblog
Simon Willison's Weblog
T
The Exploit Database - CXSecurity.com
Cisco Talos Blog
Cisco Talos Blog
A
Arctic Wolf
C
CXSECURITY Database RSS Feed - CXSecurity.com
Application and Cybersecurity Blog
Application and Cybersecurity Blog
A
About on SuperTechFans
F
Fortinet All Blogs
量子位
GbyAI
GbyAI
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
The Hacker News
The Hacker News
AWS News Blog
AWS News Blog
Forbes - Security
Forbes - Security
Help Net Security
Help Net Security
I
InfoQ
有赞技术团队
有赞技术团队
W
WeLiveSecurity
Google DeepMind News
Google DeepMind News
Engineering at Meta
Engineering at Meta
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
S
Secure Thoughts
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Webroot Blog
Webroot Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园_首页
C
Check Point Blog
T
Troy Hunt's Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
Latest news
Latest news
P
Proofpoint News Feed
Jina AI
Jina AI
Last Week in AI
Last Week in AI
Martin Fowler
Martin Fowler
雷峰网
雷峰网
博客园 - Franky
L
LangChain Blog
罗磊的独立博客
Blog — PlanetScale
Blog — PlanetScale
Google DeepMind News
Google DeepMind News
D
Docker
G
GRAHAM CLULEY
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC

博客园 - 猫狼

安卓手机连接USB摄像头代码 安装Memurai 把“休眠”功能找回来,并设置成一合上盖子就生效 mathtype安装后在office无法启动,提示运行时错误53, 文件未找到:MathPage.WLL ASP.NET Web Site Project 中集成 Hangfire 并实现图片/视频压缩后台任务 SQL SERVER 数据库压缩日志步骤 Latex的一个错误, SQL 学习笔记 升级 ASP.NET 网站项目到 C# 6.0 或更高版本 C#程序集合并工具-ILRepack manim 安装 manim一个坑爸爸的问题 manim安装纪实 去掉快捷方式的小箭头 VS2022转到定义功能异常解决方案 url 传递加号,asp.net解析参数的正确处理参数 获取自然周数的下拉列表 移出Json对象的三级属性 梦记:又要去交流? 一段VBA的代码,到处是坑 多个AJAX请求,带执行进度及结果 C# 返回文件夹及子目录 解决JS跨域访问的问题 利用Jquery的map函数将json数据行转化为表格 模访京东商城jQuery省市区三级联动选择(横向DIV)
安卓连接usb camera
猫狼 · 2026-06-28 · via 博客园 - 猫狼
package net.staredu

import android.Manifest
import android.content.pm.PackageManager
import android.graphics.BitmapFactory
import android.graphics.YuvImage
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.serenegiant.usb.*
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.FileWriter
import java.io.PrintWriter
import java.nio.ByteBuffer
import java.text.SimpleDateFormat
import java.util.*

class USBCameraTestActivity : AppCompatActivity() {

    private lateinit var surfaceView: SurfaceView
    private lateinit var tvStatus: TextView
    private lateinit var btnStart: Button
    private lateinit var btnCapture: Button
    private lateinit var btnSwitchFormat: Button
    private lateinit var ivCaptured: ImageView

    private var usbMonitor: USBMonitor? = null
    private var camera: UVCCamera? = null
    private var mUsbDevice: UsbDevice? = null
    private var mCtrlBlock: USBMonitor.UsbControlBlock? = null

    private var isConnected = false
    private var isPreviewing = false
    private var bestResolution = "未找到"
    private var bestFps = 0
    private var selectedPixelFormat = -1

    private val supportedFormatList = mutableListOf<Format>()
    private var currentFormatPos = 0
    private val triedPixelFormats = mutableSetOf<Int>()

    // 常见像素格式常量(根据 serenegiant 库定义)
    private val PIXEL_FORMAT_YUYV = 0
    private val PIXEL_FORMAT_MJPEG = 1
    // 备选值
    private val PIXEL_FORMAT_YUV420 = 2
    private val PIXEL_FORMAT_NV12 = 3

    private lateinit var logFile: File

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_usb_camera_test)

        initLogFile()

        surfaceView = findViewById(R.id.surfaceView)
        tvStatus = findViewById(R.id.tvStatus)
        btnStart = findViewById(R.id.btnStart)
        btnCapture = findViewById(R.id.btnCapture)
        btnSwitchFormat = findViewById(R.id.btnSwitchFormat)
        ivCaptured = findViewById(R.id.ivCaptured)

        surfaceView.holder.addCallback(surfaceHolderCallback)

        usbMonitor = USBMonitor(this, deviceConnectListener)

        btnStart.setOnClickListener { startCamera() }
        btnCapture.setOnClickListener { capturePhoto() }
        btnCapture.visibility = Button.GONE
        btnSwitchFormat.setOnClickListener { switchToNextFormat() }

        requestPermissionsIfNeeded()
    }

    private fun requestPermissionsIfNeeded() {
        val needPerms = mutableListOf<String>()
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
            needPerms.add(Manifest.permission.CAMERA)
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_IMAGES) != PackageManager.PERMISSION_GRANTED) {
                needPerms.add(Manifest.permission.READ_MEDIA_IMAGES)
            }
        } else {
            if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
                needPerms.add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
            }
        }
        if (needPerms.isNotEmpty()) {
            ActivityCompat.requestPermissions(this, needPerms.toTypedArray(), 100)
        }
    }

    private fun switchToNextFormat() {
        if (supportedFormatList.isEmpty()) {
            Toast.makeText(this, "没有可用的格式列表,请先启动预览", Toast.LENGTH_SHORT).show()
            return
        }
        currentFormatPos = (currentFormatPos + 1) % supportedFormatList.size
        val format = supportedFormatList[currentFormatPos]
        // 这里我们无法获取 pixelFormat,只能使用 format.index 作为索引,但实际我们使用预设常量尝试
        // 所以我们跳过,改为循环预设常量列表
        // 改为手动切换像素格式常量
        val pixelFormats = intArrayOf(PIXEL_FORMAT_YUYV, PIXEL_FORMAT_MJPEG, PIXEL_FORMAT_YUV420, PIXEL_FORMAT_NV12)
        val nextPixelFormat = pixelFormats[(currentFormatPos) % pixelFormats.size]
        Toast.makeText(this, "切换到像素格式 $nextPixelFormat", Toast.LENGTH_SHORT).show()
        log("用户切换格式 -> 像素格式 $nextPixelFormat")

        triedPixelFormats.clear()
        val ctrlBlock = mCtrlBlock
        camera?.apply {
            stopPreview()
            setFrameCallback(null, 0)
            close()
        }
        camera = null
        isPreviewing = false
        isConnected = false

        if (ctrlBlock != null) {
            surfaceView.postDelayed({
                openCameraWithCtrlBlock(ctrlBlock, nextPixelFormat)
            }, 200)
        } else {
            runOnUiThread {
                tvStatus.text = "无设备连接,请重新启动"
                Toast.makeText(this, "请先连接摄像头并点击「启动预览」", Toast.LENGTH_LONG).show()
            }
        }
    }

    private fun openCameraWithCtrlBlock(ctrlBlock: USBMonitor.UsbControlBlock, pixelFormat: Int) {
        mCtrlBlock = ctrlBlock
        log("openCameraWithCtrlBlock 被调用,pixelFormat=$pixelFormat")

        try {
            val param = UVCParam()
            camera = UVCCamera(param)
            val result = camera?.open(ctrlBlock) ?: -1
            log("UVCCamera.open 返回: $result")
            if (result != 0) {
                runOnUiThread {
                    tvStatus.text = "打开摄像头失败,错误码: $result"
                    Toast.makeText(this, "打开摄像头失败,错误码: $result", Toast.LENGTH_LONG).show()
                }
                triedPixelFormats.add(pixelFormat)
                tryNextFormatAfterFailure()
                return
            }

            isConnected = true
            log("设备已连接并打开,尝试像素格式 $pixelFormat")

            if (supportedFormatList.isEmpty()) {
                val list = camera?.getSupportedFormatList()
                if (list != null && list.isNotEmpty()) {
                    supportedFormatList.addAll(list)
                    currentFormatPos = 0
                    log("摄像头支持的格式列表:")
                    supportedFormatList.forEachIndexed { idx, f ->
                        log("  [$idx] index=${f.index}, toString=${f.toString()}")
                    }
                } else {
                    log("无法获取支持的格式列表")
                }
            }

            findBestResolutionAndFps(pixelFormat)

        } catch (e: Exception) {
            log("打开摄像头异常: ${e.message}")
            e.printStackTrace()
            runOnUiThread {
                tvStatus.text = "异常: ${e.message}"
                Toast.makeText(this, "异常: ${e.message}", Toast.LENGTH_LONG).show()
            }
            triedPixelFormats.add(pixelFormat)
            tryNextFormatAfterFailure()
        }
    }

    private fun tryNextFormatAfterFailure() {
        // 我们使用预设的像素格式列表尝试,而不是依赖 Format 对象
        val presetPixelFormats = intArrayOf(PIXEL_FORMAT_YUYV, PIXEL_FORMAT_MJPEG, PIXEL_FORMAT_YUV420, PIXEL_FORMAT_NV12)
        val untried = presetPixelFormats.filter { !triedPixelFormats.contains(it) }
        if (untried.isEmpty()) {
            log("所有预设像素格式均尝试失败,停止探测")
            runOnUiThread {
                tvStatus.text = "所有格式均失败,请检查摄像头"
                Toast.makeText(this, "所有格式均失败,请检查摄像头", Toast.LENGTH_LONG).show()
            }
            camera?.close()
            camera = null
            isConnected = false
            return
        }

        val nextPixelFormat = untried.first()
        log("自动切换到下一个未尝试格式: pixelFormat=$nextPixelFormat")
        runOnUiThread {
            tvStatus.text = "格式失败,尝试下一个..."
            Toast.makeText(this, "自动切换格式", Toast.LENGTH_SHORT).show()
        }
        camera?.close()
        camera = null
        isConnected = false
        mCtrlBlock?.let {
            openCameraWithCtrlBlock(it, nextPixelFormat)
        }
    }

    private fun findBestResolutionAndFps(pixelFormat: Int) {
        log("开始探测,当前像素格式=$pixelFormat")
        triedPixelFormats.add(pixelFormat)

        val allSizes = camera?.getSupportedSizeList() ?: emptyList()
        val sizeList = if (allSizes.isNotEmpty()) {
            log("获取到 ${allSizes.size} 个支持的尺寸: ${allSizes.joinToString { "${it.width}x${it.height}" }}")
            allSizes.map { Pair(it.width, it.height) }
        } else {
            log("未获取到支持的尺寸列表,使用预设尺寸(可能不准确)")
            listOf(
                Pair(1920, 1080), Pair(1280, 720), Pair(800, 600),
                Pair(1024, 768), Pair(640, 480), Pair(320, 240)
            )
        }
        val distinctSizes = sizeList.distinct()
        val sortedSizes = distinctSizes.sortedWith(compareByDescending { it.first * it.second })
        val fpsPriority = intArrayOf(30, 25, 20, 15, 10, 5)

        var found = false
        // 先尝试无帧率版本的 setPreviewSize (两个参数)
        for ((width, height) in sortedSizes) {
            try {
                // 尝试只设置尺寸,不指定格式和帧率(使用默认)
                camera?.setPreviewSize(width, height)
                log("✅ 成功: ${width}x${height} (无格式无帧率)")
                bestResolution = "${width}x${height}"
                bestFps = 0 // 未知
                selectedPixelFormat = pixelFormat
                found = true
                break
            } catch (e: Exception) {
                log("❌ 失败: ${width}x${height} (无格式无帧率), error: ${e.message}")
                // 继续尝试带帧率版本
                for (fps in fpsPriority) {
                    try {
                        // 尝试指定像素格式和帧率
                        camera?.setPreviewSize(width, height, fps, pixelFormat)
                        log("✅ 成功: ${width}x${height} @ ${fps}fps, pixelFormat=$pixelFormat")
                        bestResolution = "${width}x${height}"
                        bestFps = fps
                        selectedPixelFormat = pixelFormat
                        found = true
                        break
                    } catch (e2: Exception) {
                        log("❌ 失败: ${width}x${height} @ ${fps}fps, pixelFormat=$pixelFormat, error: ${e2.message}")
                        // 尝试不指定像素格式,只指定尺寸和帧率
                        try {
                            camera?.setPreviewSize(width, height, fps)
                            log("✅ 成功: ${width}x${height} @ ${fps}fps (无格式)")
                            bestResolution = "${width}x${height}"
                            bestFps = fps
                            selectedPixelFormat = -1 // 无格式
                            found = true
                            break
                        } catch (e3: Exception) {
                            log("❌ 失败: ${width}x${height} @ ${fps}fps (无格式), error: ${e3.message}")
                        }
                    }
                    if (found) break
                }
            }
            if (found) break
        }

        if (!found) {
            log("当前像素格式 $pixelFormat 无可用组合")
            tryNextFormatAfterFailure()
            return
        }

        val formatName = if (selectedPixelFormat >= 0) "像素格式 $selectedPixelFormat" else "默认格式"
        val bestText = "最佳参数: $bestResolution @ ${bestFps}fps, $formatName"
        log(bestText)
        runOnUiThread {
            tvStatus.text = bestText
            Toast.makeText(this, bestText, Toast.LENGTH_LONG).show()
        }

        if (surfaceView.holder.surface != null) {
            camera?.setPreviewDisplay(surfaceView.holder)
            camera?.startPreview()
            isPreviewing = true
            runOnUiThread {
                tvStatus.text = "预览中 ($bestResolution @ ${bestFps}fps)"
                btnStart.isEnabled = false
                btnCapture.visibility = Button.VISIBLE
            }
            log("预览已启动")
        }
    }

    // 拍照和转换函数保持不变(略,可沿用之前版本)
    private fun capturePhoto() {
        if (camera == null || !isPreviewing) {
            Toast.makeText(this, "请先启动预览", Toast.LENGTH_SHORT).show()
            return
        }
        val previewSize = camera?.getPreviewSize()
        if (previewSize == null) {
            Toast.makeText(this, "无法获取预览尺寸", Toast.LENGTH_SHORT).show()
            return
        }
        val width = previewSize.width
        val height = previewSize.height
        log("拍照尺寸: ${width}x${height}")

        camera?.setFrameCallback(object : IFrameCallback {
            override fun onFrame(frame: ByteBuffer) {
                try {
                    val remaining = frame.remaining()
                    val bytes = ByteArray(remaining)
                    frame.get(bytes)

                    val jpegData: ByteArray? = when (selectedPixelFormat) {
                        PIXEL_FORMAT_MJPEG -> bytes
                        else -> convertYUY2toJPEG(bytes, width, height)
                    }

                    if (jpegData == null || jpegData.isEmpty()) {
                        runOnUiThread {
                            Toast.makeText(this@USBCameraTestActivity, "图片转换失败", Toast.LENGTH_SHORT).show()
                        }
                        return
                    }

                    val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
                    val fileName = "UVC_${timeStamp}.jpg"
                    val storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
                    val file = File(storageDir, fileName)
                    FileOutputStream(file).use { fos -> fos.write(jpegData) }

                    runOnUiThread {
                        val bitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.size)
                        ivCaptured.setImageBitmap(bitmap)
                        Toast.makeText(this@USBCameraTestActivity, "拍照成功: ${file.absolutePath}", Toast.LENGTH_LONG).show()
                        tvStatus.text = "拍照成功"
                    }
                    log("拍照成功: ${file.absolutePath}")
                } catch (e: Exception) {
                    log("拍照异常: ${e.message}")
                    e.printStackTrace()
                    runOnUiThread {
                        Toast.makeText(this@USBCameraTestActivity, "拍照失败: ${e.message}", Toast.LENGTH_SHORT).show()
                        tvStatus.text = "拍照失败"
                    }
                } finally {
                    camera?.setFrameCallback(null, 0)
                }
            }
        }, if (selectedPixelFormat >= 0) selectedPixelFormat else 0)
    }

    private fun convertYUY2toJPEG(data: ByteArray, width: Int, height: Int): ByteArray? {
        try {
            val nv21 = ByteArray(width * height * 3 / 2)
            var yIndex = 0
            var uvIndex = width * height
            var i = 0
            while (i + 3 < data.size) {
                val y1 = data[i].toInt() and 0xFF
                val u  = data[i + 1].toInt() and 0xFF
                val y2 = data[i + 2].toInt() and 0xFF
                val v  = data[i + 3].toInt() and 0xFF

                nv21[yIndex++] = y1.toByte()
                nv21[yIndex++] = y2.toByte()

                val pairIndex = i / 4
                val pixelIndex = pairIndex * 2
                val row = pixelIndex / width

                if (row % 2 == 0) {
                    nv21[uvIndex++] = v.toByte()
                    nv21[uvIndex++] = u.toByte()
                }
                i += 4
            }

            val yuvImage = YuvImage(nv21, android.graphics.ImageFormat.NV21, width, height, null)
            val out = ByteArrayOutputStream()
            yuvImage.compressToJpeg(android.graphics.Rect(0, 0, width, height), 80, out)
            return out.toByteArray()
        } catch (e: Exception) {
            log("YUY2转换失败: ${e.message}")
            e.printStackTrace()
            return null
        }
    }

    private val deviceConnectListener = object : USBMonitor.OnDeviceConnectListener {
        override fun onAttach(device: UsbDevice?) {
            runOnUiThread {
                tvStatus.text = "设备已连接: ${device?.deviceName}"
                Toast.makeText(this@USBCameraTestActivity, "设备已连接", Toast.LENGTH_SHORT).show()
                log("设备已连接: ${device?.deviceName}")
                device?.let { usbMonitor?.requestPermission(it) }
            }
        }

        override fun onDeviceOpen(device: UsbDevice?, ctrlBlock: USBMonitor.UsbControlBlock?, createNew: Boolean) {
            if (ctrlBlock == null) {
                log("onDeviceOpen: ctrlBlock is null")
                return
            }
            mCtrlBlock = ctrlBlock
            runOnUiThread { tvStatus.text = "设备已打开,正在探测最佳参数..." }
            triedPixelFormats.clear()
            // 从预设常量开始尝试,不使用 Format 中的 pixelFormat
            val firstPixelFormat = PIXEL_FORMAT_YUYV
            openCameraWithCtrlBlock(ctrlBlock, firstPixelFormat)
        }

        override fun onDeviceClose(device: UsbDevice?, ctrlBlock: USBMonitor.UsbControlBlock?) {
            camera?.close()
            camera = null
            isConnected = false
            isPreviewing = false
            mCtrlBlock = null
            runOnUiThread {
                tvStatus.text = "设备已关闭"
                btnStart.isEnabled = true
                btnCapture.visibility = Button.GONE
            }
            log("设备已关闭")
        }

        override fun onDetach(device: UsbDevice?) {
            camera?.close()
            camera = null
            isConnected = false
            isPreviewing = false
            mCtrlBlock = null
            runOnUiThread {
                tvStatus.text = "设备已拔出"
                Toast.makeText(this@USBCameraTestActivity, "设备已拔出", Toast.LENGTH_SHORT).show()
                btnStart.isEnabled = true
                btnCapture.visibility = Button.GONE
            }
            log("设备已拔出")
        }

        override fun onCancel(device: UsbDevice?) {
            runOnUiThread {
                tvStatus.text = "USB 权限被拒绝"
                Toast.makeText(this@USBCameraTestActivity, "USB 权限被拒绝", Toast.LENGTH_LONG).show()
            }
            log("USB 权限被拒绝")
        }
    }

    private val surfaceHolderCallback = object : SurfaceHolder.Callback {
        override fun surfaceCreated(holder: SurfaceHolder) {
            if (camera != null && isPreviewing) {
                camera?.setPreviewDisplay(holder)
                camera?.startPreview()
                runOnUiThread { tvStatus.text = "预览中 ($bestResolution @ ${bestFps}fps)" }
                log("Surface 创建,预览恢复")
            }
        }
        override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}
        override fun surfaceDestroyed(holder: SurfaceHolder) {
            camera?.stopPreview()
            runOnUiThread { tvStatus.text = "预览已停止" }
            log("Surface 销毁,预览停止")
        }
    }

    private fun startCamera() {
        if (isConnected) {
            Toast.makeText(this, "摄像头已连接,无需重复启动", Toast.LENGTH_SHORT).show()
            return
        }
        log("启动摄像头")
        usbMonitor?.register()

        val usbManager = getSystemService(USB_SERVICE) as UsbManager
        val deviceList = usbManager.deviceList
        log("检测到 USB 设备数量: ${deviceList.size}")
        if (deviceList.isEmpty()) {
            runOnUiThread {
                Toast.makeText(this, "未检测到 USB 设备,请连接摄像头", Toast.LENGTH_SHORT).show()
                tvStatus.text = "未检测到设备"
            }
            log("未检测到 USB 设备")
            return
        }

        val device = deviceList.values.firstOrNull()
        if (device == null) {
            runOnUiThread {
                Toast.makeText(this, "未找到 USB 摄像头", Toast.LENGTH_SHORT).show()
                tvStatus.text = "未找到摄像头"
            }
            log("USB 设备列表为空")
            return
        }

        mUsbDevice = device
        log("请求权限设备: ${device.deviceName}, VID: ${device.vendorId}, PID: ${device.productId}")
        runOnUiThread { tvStatus.text = "正在请求 USB 权限..." }
        usbMonitor?.requestPermission(device)
    }

    private fun initLogFile() {
        try {
            val dir = getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) ?: filesDir
            val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
            logFile = File(dir, "uvc_log_$timestamp.txt")
            log("=== UVC 测试日志开始 ===")
            log("设备信息: ${Build.MANUFACTURER} ${Build.MODEL}, Android ${Build.VERSION.RELEASE}")
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    private fun log(message: String) {
        try {
            FileWriter(logFile, true).use { writer ->
                PrintWriter(writer).use { pw ->
                    val timestamp = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()).format(Date())
                    pw.println("[$timestamp] $message")
                    pw.flush()
                }
            }
        } catch (e: Exception) {
        }
    }

    override fun onResume() {
        super.onResume()
        usbMonitor?.register()
        log("onResume")
    }

    override fun onPause() {
        super.onPause()
        usbMonitor?.unregister()
        if (isPreviewing) {
            camera?.stopPreview()
        }
        log("onPause")
    }

    override fun onDestroy() {
        super.onDestroy()
        camera?.stopPreview()
        camera?.close()
        camera = null
        usbMonitor?.destroy()
        log("onDestroy")
        log("=== 日志结束 ===")
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == 100) {
            var allGranted = true
            grantResults.forEach {
                if (it != PackageManager.PERMISSION_GRANTED) allGranted = false
            }
            if (allGranted) {
                log("全部权限已授予")
            } else {
                log("权限被拒绝,部分功能无法使用")
                Toast.makeText(this, "缺少必要存储/相机权限", Toast.LENGTH_LONG).show()
            }
        }
    }
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#000000"
    android:padding="10dp">

    <!-- 状态显示 -->
    <TextView
        android:id="@+id/tvStatus"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="等待设备..."
        android:textColor="#FFFFFF"
        android:textSize="16sp"
        android:gravity="center"
        android:padding="8dp"/>

    <!-- 预览区域(SurfaceView) -->
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:background="#000000">
        <SurfaceView
            android:id="@+id/surfaceView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_gravity="center"/>
    </FrameLayout>

    <!-- 操作按钮 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginTop="8dp">

        <Button
            android:id="@+id/btnStart"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="启动预览"
            android:backgroundTint="#2196F3"
            android:textColor="#FFFFFF"/>

        <Button
            android:id="@+id/btnCapture"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="拍照"
            android:backgroundTint="#4CAF50"
            android:textColor="#FFFFFF"
            android:visibility="gone"/>

        <Button
            android:id="@+id/btnSwitchFormat"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="切换格式"
            android:backgroundTint="#FF9800"
            android:textColor="#FFFFFF"/>
    </LinearLayout>

    <!-- 显示最近拍摄的照片 -->
    <ImageView
        android:id="@+id/ivCaptured"
        android:layout_width="match_parent"
        android:layout_height="120dp"
        android:layout_marginTop="8dp"
        android:background="#333333"
        android:scaleType="centerInside"/>
</LinearLayout>