diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6d7aba8..184028e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -90,6 +90,9 @@ + diff --git a/app/src/main/java/com/nova/brain/glass/helper/OfflineCmdServiceHelper.kt b/app/src/main/java/com/nova/brain/glass/helper/OfflineCmdServiceHelper.kt index f34a6fb..9c43023 100644 --- a/app/src/main/java/com/nova/brain/glass/helper/OfflineCmdServiceHelper.kt +++ b/app/src/main/java/com/nova/brain/glass/helper/OfflineCmdServiceHelper.kt @@ -45,7 +45,14 @@ object OfflineCmdServiceHelper { ) private val CMDS_SPRAYING = listOf( OfflineCmdBean("开始", "kai shi"), - OfflineCmdBean("开始任务", "kai shi ren wu") + OfflineCmdBean("开始任务", "kai shi ren wu"), + OfflineCmdBean("拍照", "pai zhao"), + OfflineCmdBean("拍摄", "pai she"), + OfflineCmdBean("放大", "fang da"), + OfflineCmdBean("拉近", "la jin"), + OfflineCmdBean("缩小", "suo xiao"), + OfflineCmdBean("拉远", "la yuan"), + OfflineCmdBean("取消", "qu xiao") ) private val CMDS_SPRAYING_FINISH = listOf( OfflineCmdBean("补充照片", "bu chong zhao pian"), diff --git a/app/src/main/java/com/nova/brain/glass/ui/CameraPreviewPocActivity.kt b/app/src/main/java/com/nova/brain/glass/ui/CameraPreviewPocActivity.kt new file mode 100644 index 0000000..c5a3ff6 --- /dev/null +++ b/app/src/main/java/com/nova/brain/glass/ui/CameraPreviewPocActivity.kt @@ -0,0 +1,407 @@ +package com.nova.brain.glass.ui + +import android.Manifest +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.ImageFormat +import android.graphics.Rect +import android.graphics.SurfaceTexture +import android.hardware.camera2.CameraCaptureSession +import android.hardware.camera2.CameraCharacteristics +import android.hardware.camera2.CameraAccessException +import android.hardware.camera2.CameraDevice +import android.hardware.camera2.CameraManager +import android.hardware.camera2.CaptureRequest +import android.hardware.camera2.TotalCaptureResult +import android.hardware.camera2.params.StreamConfigurationMap +import android.media.ImageReader +import android.os.Bundle +import android.os.Environment +import android.os.Handler +import android.os.HandlerThread +import android.view.KeyEvent +import android.view.Surface +import android.view.TextureView +import androidx.core.app.ActivityCompat +import com.nova.brain.glass.R +import com.nova.brain.glass.databinding.ActivityCameraPreviewPocBinding +import com.nova.brain.glass.helper.OfflineCmdListener +import com.nova.brain.glass.helper.OfflineCmdServiceHelper +import com.xuqm.base.extensions.showMessage +import com.xuqm.base.ui.BaseActivity +import java.io.File +import java.io.FileOutputStream +import java.util.UUID +import kotlin.math.roundToInt + +class CameraPreviewPocActivity : BaseActivity() { + + companion object { + const val EXTRA_PHOTO_PATH = "extra_photo_path" + private const val REQUEST_CAMERA_PERMISSION = 1001 + } + + override fun getLayoutId(): Int = R.layout.activity_camera_preview_poc + override fun fullscreen(): Boolean = true + + private val cameraManager by lazy { getSystemService(CameraManager::class.java) } + private var cameraDevice: CameraDevice? = null + private var captureSession: CameraCaptureSession? = null + private var previewRequestBuilder: CaptureRequest.Builder? = null + private var imageReader: ImageReader? = null + private var backgroundThread: HandlerThread? = null + private var backgroundHandler: Handler? = null + private var activeArraySize: Rect? = null + private var maxZoom = 1f + private var zoomLevel = 1f + private var currentOutputPath: String? = null + private var isCapturing = false + + private val offlineCmdListener = object : OfflineCmdListener { + override fun onOfflineCmd(cmd: String) { + runOnUiThread { + when (cmd) { + "退出", "返回", "退回", "取消" -> finish() + "拍照", "拍摄", "开始", "开始任务" -> captureStillImage() + "放大", "拉近" -> adjustZoom(0.2f) + "缩小", "拉远" -> adjustZoom(-0.2f) + } + } + } + } + + private val textureListener = object : TextureView.SurfaceTextureListener { + override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) { + openCamera() + } + + override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) = Unit + override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean = true + override fun onSurfaceTextureUpdated(surface: SurfaceTexture) = Unit + } + + override fun initView(savedInstanceState: Bundle?) { + super.initView(savedInstanceState) + binding.previewView.surfaceTextureListener = textureListener + binding.captureButton.setOnClickListener { captureStillImage() } + binding.zoomInButton.setOnClickListener { adjustZoom(0.2f) } + binding.zoomOutButton.setOnClickListener { adjustZoom(-0.2f) } + updateZoomText() + } + + override fun onResume() { + super.onResume() + startBackgroundThread() + OfflineCmdServiceHelper.addOnLineListener(offlineCmdListener) + OfflineCmdServiceHelper.addListenerSpraying() + if (binding.previewView.isAvailable) { + openCamera() + } else { + binding.previewView.surfaceTextureListener = textureListener + } + } + + override fun onPause() { + closeCamera() + OfflineCmdServiceHelper.removeListenerSpraying() + OfflineCmdServiceHelper.removeOnLineListener(offlineCmdListener) + stopBackgroundThread() + super.onPause() + } + + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + if (event.action != KeyEvent.ACTION_DOWN) return super.dispatchKeyEvent(event) + return when (event.keyCode) { + KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_MINUS, KeyEvent.KEYCODE_VOLUME_DOWN -> { + adjustZoom(-0.2f) + true + } + KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.KEYCODE_EQUALS, KeyEvent.KEYCODE_VOLUME_UP -> { + adjustZoom(0.2f) + true + } + KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_CAMERA -> { + captureStillImage() + true + } + else -> super.dispatchKeyEvent(event) + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode != REQUEST_CAMERA_PERMISSION) return + if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) { + openCamera() + } else { + "缺少相机权限,无法验证预览".showMessage() + finish() + } + } + + private fun openCamera() { + if ( + ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED + ) { + ActivityCompat.requestPermissions( + this, + arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO), + REQUEST_CAMERA_PERMISSION + ) + return + } + try { + val selectedCameraId = selectBackCameraId() ?: run { + "未找到可用相机".showMessage() + finish() + return + } + val characteristics = cameraManager.getCameraCharacteristics(selectedCameraId) + activeArraySize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE) + maxZoom = (characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM) ?: 1f) + .coerceAtLeast(1f) + zoomLevel = zoomLevel.coerceIn(1f, maxZoom) + val streamMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) + val previewSize = choosePreviewSize(streamMap) + val captureSize = chooseCaptureSize(streamMap) + val texture = binding.previewView.surfaceTexture ?: return + texture.setDefaultBufferSize(previewSize.width, previewSize.height) + imageReader?.close() + imageReader = ImageReader.newInstance(captureSize.width, captureSize.height, ImageFormat.JPEG, 2).apply { + setOnImageAvailableListener({ reader -> + val image = reader.acquireNextImage() ?: return@setOnImageAvailableListener + val outputPath = currentOutputPath + if (outputPath == null) { + image.close() + return@setOnImageAvailableListener + } + val buffer = image.planes.firstOrNull()?.buffer + val bytes = ByteArray(buffer?.remaining() ?: 0) + buffer?.get(bytes) + image.close() + runCatching { + FileOutputStream(outputPath).use { it.write(bytes) } + }.onSuccess { + runOnUiThread { + setResult(RESULT_OK, Intent().putExtra(EXTRA_PHOTO_PATH, outputPath)) + finish() + } + }.onFailure { + isCapturing = false + runOnUiThread { + binding.statusText.text = "图片保存失败: ${it.message ?: "未知错误"}" + } + } + }, backgroundHandler) + } + binding.statusText.text = "相机预览验证中,可拉近拉远后拍照" + cameraManager.openCamera(selectedCameraId, object : CameraDevice.StateCallback() { + override fun onOpened(device: CameraDevice) { + cameraDevice = device + createPreviewSession(texture) + } + + override fun onDisconnected(device: CameraDevice) { + device.close() + cameraDevice = null + runOnUiThread { binding.statusText.text = "相机已断开" } + } + + override fun onError(device: CameraDevice, error: Int) { + device.close() + cameraDevice = null + runOnUiThread { binding.statusText.text = "相机打开失败: $error" } + } + }, backgroundHandler) + } catch (securityException: SecurityException) { + binding.statusText.text = "缺少相机权限" + "缺少相机权限,无法打开预览".showMessage() + } + } + + private fun createPreviewSession(texture: SurfaceTexture) { + val device = cameraDevice ?: return + val previewSurface = Surface(texture) + previewRequestBuilder = try { + device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply { + addTarget(previewSurface) + set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO) + set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE) + applyZoom(this) + } + } catch (e: CameraAccessException) { + handleCameraFailure("相机预览初始化失败: ${e.message}") + return + } catch (e: IllegalStateException) { + handleCameraFailure("相机设备不可用: ${e.message}") + return + } + val readerSurface = imageReader?.surface ?: return + try { + device.createCaptureSession( + listOf(previewSurface, readerSurface), + object : CameraCaptureSession.StateCallback() { + override fun onConfigured(session: CameraCaptureSession) { + captureSession = session + val request = previewRequestBuilder?.build() ?: return + try { + session.setRepeatingRequest(request, null, backgroundHandler) + } catch (e: CameraAccessException) { + handleCameraFailure("相机预览启动失败: ${e.message}") + } catch (e: IllegalStateException) { + handleCameraFailure("相机会话不可用: ${e.message}") + } + } + + override fun onConfigureFailed(session: CameraCaptureSession) { + handleCameraFailure("相机预览配置失败") + } + }, + backgroundHandler + ) + } catch (e: CameraAccessException) { + handleCameraFailure("相机会话创建失败: ${e.message}") + } catch (e: IllegalStateException) { + handleCameraFailure("相机会话不可用: ${e.message}") + } + } + + private fun captureStillImage() { + if (isCapturing) return + val device = cameraDevice ?: return + val session = captureSession ?: return + val readerSurface = imageReader?.surface ?: return + val outputPath = createOutputFile().absolutePath + currentOutputPath = outputPath + isCapturing = true + binding.statusText.text = "拍照中,请保持稳定..." + try { + val captureBuilder = device.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE).apply { + addTarget(readerSurface) + set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO) + set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE) + applyZoom(this) + set(CaptureRequest.JPEG_ORIENTATION, 0) + } + session.stopRepeating() + session.capture(captureBuilder.build(), object : CameraCaptureSession.CaptureCallback() { + override fun onCaptureCompleted( + session: CameraCaptureSession, + request: CaptureRequest, + result: TotalCaptureResult + ) { + previewRequestBuilder?.build()?.let { + runCatching { + session.setRepeatingRequest(it, null, backgroundHandler) + } + } + } + }, backgroundHandler) + } catch (e: CameraAccessException) { + isCapturing = false + handleCameraFailure("拍照失败: ${e.message}") + } catch (e: IllegalStateException) { + isCapturing = false + handleCameraFailure("相机设备不可用: ${e.message}") + } + } + + private fun adjustZoom(delta: Float) { + maxZoom = maxZoom.coerceAtLeast(1f) + zoomLevel = (zoomLevel + delta).coerceIn(1f, maxZoom) + updateZoomText() + val requestBuilder = previewRequestBuilder ?: return + val session = captureSession ?: return + applyZoom(requestBuilder) + try { + session.setRepeatingRequest(requestBuilder.build(), null, backgroundHandler) + } catch (e: CameraAccessException) { + handleCameraFailure("缩放失败: ${e.message}") + } catch (e: IllegalStateException) { + handleCameraFailure("相机会话不可用: ${e.message}") + } + } + + private fun updateZoomText() { + binding.zoomText.text = "缩放 ${String.format("%.1f", zoomLevel)}x" + } + + private fun applyZoom(builder: CaptureRequest.Builder) { + val sensorRect = activeArraySize ?: return + if (zoomLevel <= 1f) { + builder.set(CaptureRequest.SCALER_CROP_REGION, sensorRect) + return + } + val centerX = sensorRect.centerX() + val centerY = sensorRect.centerY() + val deltaX = (0.5f * sensorRect.width() / zoomLevel).roundToInt() + val deltaY = (0.5f * sensorRect.height() / zoomLevel).roundToInt() + val cropRect = Rect(centerX - deltaX, centerY - deltaY, centerX + deltaX, centerY + deltaY) + builder.set(CaptureRequest.SCALER_CROP_REGION, cropRect) + } + + private fun selectBackCameraId(): String? { + val ids = cameraManager.cameraIdList ?: return null + return ids.firstOrNull { id -> + val facing = cameraManager.getCameraCharacteristics(id) + .get(CameraCharacteristics.LENS_FACING) + facing == CameraCharacteristics.LENS_FACING_BACK + } ?: ids.firstOrNull() + } + + private fun choosePreviewSize(map: StreamConfigurationMap?): android.util.Size { + val sizes = map?.getOutputSizes(SurfaceTexture::class.java).orEmpty() + return sizes.maxByOrNull { it.width * it.height } ?: android.util.Size(1920, 1080) + } + + private fun chooseCaptureSize(map: StreamConfigurationMap?): android.util.Size { + val sizes = map?.getOutputSizes(ImageFormat.JPEG).orEmpty() + return sizes.maxByOrNull { it.width * it.height } ?: android.util.Size(1920, 1080) + } + + private fun createOutputFile(): File { + val publicPicturesDir = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + if (!publicPicturesDir.exists()) { + publicPicturesDir.mkdirs() + } + return File(publicPicturesDir, "camera_poc_${UUID.randomUUID()}.jpg") + } + private fun closeCamera() { + captureSession?.close() + captureSession = null + cameraDevice?.close() + cameraDevice = null + imageReader?.close() + imageReader = null + isCapturing = false + currentOutputPath = null + } + + private fun handleCameraFailure(message: String) { + closeCamera() + runOnUiThread { + binding.statusText.text = message + message.showMessage() + setResult(RESULT_CANCELED) + finish() + } + } + + private fun startBackgroundThread() { + if (backgroundThread != null) return + backgroundThread = HandlerThread("camera-preview-poc").also { it.start() } + backgroundHandler = Handler(backgroundThread!!.looper) + } + + private fun stopBackgroundThread() { + backgroundThread?.quitSafely() + backgroundThread?.join() + backgroundThread = null + backgroundHandler = null + } +} diff --git a/app/src/main/java/com/nova/brain/glass/ui/SprayingActivity.kt b/app/src/main/java/com/nova/brain/glass/ui/SprayingActivity.kt index 781b6c3..9d06660 100644 --- a/app/src/main/java/com/nova/brain/glass/ui/SprayingActivity.kt +++ b/app/src/main/java/com/nova/brain/glass/ui/SprayingActivity.kt @@ -1,26 +1,19 @@ package com.nova.brain.glass.ui import android.content.Intent -import android.os.Environment import androidx.recyclerview.widget.RecyclerView import com.nova.brain.glass.R import com.nova.brain.glass.databinding.ActivitySprayingBinding -import com.nova.brain.glass.helper.GlassMediaServiceHelper import com.nova.brain.glass.helper.OfflineCmdListener import com.nova.brain.glass.helper.OfflineCmdServiceHelper import com.nova.brain.glass.helper.SprayingPhotoManager import com.nova.brain.glass.model.ItemItem import com.nova.brain.glass.viewmodel.SprayingVM -import com.rokid.security.glass3.sdk.base.data.media.PhotoResolution -import com.rokid.security.system.server.media.callback.PhotoFileCallback import com.xuqm.base.adapter.BasePagedAdapter import com.xuqm.base.adapter.CommonPagedAdapter import com.xuqm.base.adapter.ViewHolder -import com.xuqm.base.common.LogHelper import com.xuqm.base.extensions.showMessage import com.xuqm.base.ui.BaseListFormLayoutNormalActivity -import java.io.File -import java.util.UUID class SprayingActivity : BaseListFormLayoutNormalActivity() { @@ -35,6 +28,10 @@ class SprayingActivity : } private var productionInfoId: String = "" + companion object { + private const val REQUEST_CAMERA_PREVIEW = 2001 + } + private val listener = object : OfflineCmdListener { override fun onOfflineCmd(cmd: String) { runOnUiThread { @@ -48,7 +45,6 @@ class SprayingActivity : binding.hint.text = "拍照中,请稍后..." } SprayingPhotoManager.clear() - isPhoto = true takePhoto() } } @@ -58,50 +54,10 @@ class SprayingActivity : } fun takePhoto() { - val fileName = "test_${System.currentTimeMillis()}.png" - val publicPicturesDir = - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) - val file = File(publicPicturesDir, fileName) - GlassMediaServiceHelper.takePhoto(PhotoResolution.RESOLUTION_1080P, file.absolutePath) - } - - private val photoCallbackId = UUID.randomUUID().toString() - - private val mPhotoFileCallback = object : PhotoFileCallback.Stub() { - override fun onTakePhoto(path: String) { - LogHelper.d("onTakePhoto-->path = $path") - } - - override fun getCallbackId(): String { - return photoCallbackId - } - - override fun onTakePhotoV2(path: String?, width: Int, height: Int) { - LogHelper.d("width:$width--height:$height") - if (path == null) { - if (isPhoto) { - isPhoto = false - takePhoto() - } else { - runOnUiThread { - binding.hint.text = "单击或语音输入“开始”,进入下一步" - } - "相机异常".showMessage() - } - } else { - SprayingPhotoManager.addPhoto(path) - startActivity(Intent(this@SprayingActivity, SprayingOCRActivity::class.java).apply { - putExtra("path", path) - putExtra("taskId", taskId) - putExtra("productionInfoId", productionInfoId) - }) - finish() -// runOnUiThread { -// binding.hint.text = "单击或语音输入“开始”,进入下一步" -// binding.iv.setImageBitmap(BitmapFactory.decodeFile(path)) -// } - } - } + startActivityForResult( + Intent(this, CameraPreviewPocActivity::class.java), + REQUEST_CAMERA_PREVIEW + ) } @@ -127,7 +83,6 @@ class SprayingActivity : override fun onResume() { super.onResume() - GlassMediaServiceHelper.addPhotoCallback(mPhotoFileCallback) OfflineCmdServiceHelper.addOnLineListener(listener) OfflineCmdServiceHelper.addListenerSpraying() } @@ -136,7 +91,28 @@ class SprayingActivity : super.onPause() OfflineCmdServiceHelper.removeListenerSpraying() OfflineCmdServiceHelper.removeOnLineListener(listener) - GlassMediaServiceHelper.removePhotoCallback(mPhotoFileCallback) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode != REQUEST_CAMERA_PREVIEW) return + if (resultCode != RESULT_OK) { + binding.hint.text = "单击或语音输入“开始”,进入下一步" + return + } + val path = data?.getStringExtra(CameraPreviewPocActivity.EXTRA_PHOTO_PATH) + if (path.isNullOrBlank()) { + binding.hint.text = "单击或语音输入“开始”,进入下一步" + "未获取到照片".showMessage() + return + } + SprayingPhotoManager.addPhoto(path) + startActivity(Intent(this, SprayingOCRActivity::class.java).apply { + putExtra("path", path) + putExtra("taskId", taskId) + putExtra("productionInfoId", productionInfoId) + }) + finish() } override fun onDestroy() { @@ -144,7 +120,6 @@ class SprayingActivity : window.clearFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } - private var isPhoto = false private val adapter = object : CommonPagedAdapter(R.layout.item_photo) { override fun convert(holder: ViewHolder, item: ItemItem, position: Int) { holder @@ -155,7 +130,6 @@ class SprayingActivity : binding.hint.text = "拍照中,请稍后..." } SprayingPhotoManager.clear() - isPhoto = true takePhoto() } } diff --git a/app/src/main/res/layout/activity_camera_preview_poc.xml b/app/src/main/res/layout/activity_camera_preview_poc.xml new file mode 100644 index 0000000..108e875 --- /dev/null +++ b/app/src/main/res/layout/activity_camera_preview_poc.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + +