Files
SmartPlateCabinet/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt
T
mazengfei d53ea8f798 feat(api): 新增V2版本接口支持并集成至网络请求客户端
- 新增 ApiServiceV2 接口,支持人脸缓存、增量数据、设备配置等接口
- ApiClient 中新增 apiServiceV2 对象,复用 Retrofit 实例,调整超时时间至60秒
- DeviceInitActivity 改用 netViewModelV2 进行人脸缓存全量拉取及设备配置读取
- BaseActivity 新增对人脸增量同步接口调用,调用成功后刷新本地识别缓存
- BaseActivity 新增清空本地人脸库功能,异步清理数据库并刷新识别缓存
- FaceApi 增加清空本地人脸库接口,重写数据库操作逻辑
- 升级 lib_face 模块 Room 数据库版本,新增字段以支持多标识人脸实体扩展
- FaceEntity 实体扩展会员编号、用户ID、人脸ID、会员标识、更新时间等字段
- FaceDao 新增按人脸ID查询和删除接口,支持多重人脸数据操作
- 优化网络层 OkHttpClient 配置,简化拦截器写法及日志级别判断
- 升级 lib_face 模块支持 arm64-v8a 架构,增强兼容性
- 规范模块间依赖关系,统一版本管理及包路径声明
- 完善 CLAUDE.md 文档,补充项目架构、模块划分与开发流程说明
2026-08-18 17:14:06 +08:00

520 lines
19 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.sw.platecabinet.activity
import android.Manifest
import android.content.Intent
import android.graphics.Point
import android.hardware.Camera
import android.os.Build
import android.os.CountDownTimer
import android.util.DisplayMetrics
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.lifecycle.Observer
import com.arcsoft.face.ErrorInfo
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.ConfigUtil
import com.sw.plate.utils.arcface.ErrorCodeUtil
import com.sw.plate.utils.arcface.FaceRectTransformer
import com.sw.plate.utils.arcface.FaceRectView
import com.sw.plate.utils.arcface.FaceRectView.DrawInfo
import com.sw.plate.utils.arcface.PreviewConfig
import com.sw.plate.utils.arcface.camera.CameraListener
import com.sw.plate.utils.arcface.camera.DualCameraHelper
import com.sw.plate.utils.arcface.face.constants.LivenessType
import com.sw.plate.utils.arcface.face.model.CompareResult
import com.sw.plate.utils.arcface.face.model.FacePreviewInfo
import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration
import com.sw.plate.utils.arcface.faceserver.FaceServer
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import com.sw.platecabinet.utils.Debouncer
import com.sw.platecabinet.member.R
import com.sw.platecabinet.member.databinding.ActivityLoginFaceBinding
import com.sw.platecabinet.member.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.network.task.TaskManager
import com.sw.platecabinet.utils.PermissionHelper
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import timber.log.Timber
/**
* 人脸识别
*/
class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
ViewTreeObserver.OnGlobalLayoutListener {
private var countDownTimer: CountDownTimer? = null
private val CAMERA_PERMISSION_REQUEST_CODE = 100
private val REQUIRED_PERMISSIONS: Array<String> = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
override fun inflateViewBinding(): ActivityLoginFaceBinding {
return ActivityLoginFaceBinding.inflate(layoutInflater)
}
override fun inflateTitleBinding(): ItemTitleTimeBinding {
return binding.includeHeader
}
companion object {
private var instance: LoginByFaceActivity? = null
fun goInitActivity() {
instance?.goInitActivity()
}
}
override fun initialize() {
instance = this
// V2 引擎激活(appId/sdkKey/activeKey 来自 V2 设备配置)
netViewModelV2.activeEngine()
initCountTime()
initArcViewModel()
initArcView()
openRectInfoDraw = true
recognizeViewModel.setDrawRectInfoTextValue(true)
//开启人脸增量数据定时任务(V2
startFaceTask()
binding.llToPwd.setOnClickListener {
val intent = Intent(this, LoginByPwdActivity::class.java)
startActivity(intent)
}
// TODO: 测试请求
TaskManager.startTask()
EventBus.getDefault().register(this)
}
private fun checkCameraPermission() {
Timber.i("checkCameraPermission")
val permissionHelper =
PermissionHelper.with(this, REQUIRED_PERMISSIONS, CAMERA_PERMISSION_REQUEST_CODE)
.onGranted {
Timber.d("checkCameraPermission 权限已允许")
// 权限已授予
recognizeViewModel.init()
initRgbCamera()
resumeCamera()
}
.build()
registerPermissionHelper(permissionHelper)
permissionHelper.checkAndRequest()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun getFaceEntity(insertEntity: FaceEntity) {
Timber.tag("performSync").e("-time=%s", insertEntity.registerTime)
recognizeViewModel.addFace(insertEntity)
Timber.tag("performSync over").e("-time=%s", insertEntity.registerTime)
}
override fun onLeftDoubleClick() {
finish()
}
private var isRecognition = false
private var rgbCameraHelper: DualCameraHelper? = null
private var rgbFaceRectTransformer: FaceRectTransformer? = null
private val livenessType = LivenessType.RGB
private var openRectInfoDraw = false
private fun initArcViewModel() {
recognizeViewModel.setLiveType(livenessType)
recognizeViewModel.ftInitCode.observe(this, Observer { ftInitCode: Int? ->
if (ftInitCode != ErrorInfo.MOK) {
val error: String? = getString(
R.string.specific_engine_init_failed, "ftEngine",
ftInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(ftInitCode!!)
)
Timber.e("ftInitCode observe = $error")
ToastUtils.showToast(error)
}
})
recognizeViewModel.frInitCode.observe(this, Observer { frInitCode: Int? ->
if (frInitCode != ErrorInfo.MOK) {
val error: String? = getString(
R.string.specific_engine_init_failed, "frEngine",
frInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(frInitCode!!)
)
Timber.e("frInitCode observe = $error")
ToastUtils.showToast(error)
}
})
recognizeViewModel.flInitCode.observe(this, Observer { flInitCode: Int? ->
if (flInitCode != ErrorInfo.MOK) {
val error: String? = getString(
R.string.specific_engine_init_failed, "flEngine",
flInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(flInitCode!!)
)
Timber.e("flInitCode observe = $error")
ToastUtils.showToast(error)
}
})
recognizeViewModel.recognizeConfiguration
.observe(this, Observer { recognizeConfiguration: RecognizeConfiguration? ->
Timber.i("recognizeConfiguration: observe = ${recognizeConfiguration.toString()}")
})
recognizeViewModel.recognizeNotice.observe(this, Observer { notice: String? ->
Timber.i("recognizeNotice observe notice = $notice")
})
recognizeViewModel.recognizeUserId.observe(this, Observer { result: CompareResult? ->
Timber.i(
"recognizeUserId observe userId=${result?.faceEntity?.userName}," +
"similar=${result?.similar},userType=${result?.faceEntity?.userType}"
)
loadFaceRecognizeResult(result)
})
recognizeViewModel.drawRectInfoText.observe(this, Observer { info ->
Timber.i("drawRectInfoText observe info = $info")
})
}
private fun initArcView() {
//在布局结束后才做初始化操作
binding.dualCameraTexturePreviewRgb.getViewTreeObserver().addOnGlobalLayoutListener(this)
recognizeViewModel.getCompareResultList().getValue()
}
/**
* 识别阈值,默认值为0.8
*/
private val faceSuccessThreshold by lazy { ConfigUtil.getRecognizeThreshold(this) }
/**
* 识别成功后开柜(查询用户信息)防抖,防止重复触发
*/
private val loginDebouncer = Debouncer(3000)
/**
* 处理识别结果:达到阈值走登录流程,未达阈值仅记录(本版本无采集模式)
*/
private fun loadFaceRecognizeResult(result: CompareResult?) {
val similar = result?.similar ?: return
Timber.i(
"loadFaceRecognizeResult 阈值=$faceSuccessThreshold, similar=$similar"
)
if (similar >= faceSuccessThreshold) {
val userId = result.faceEntity?.userName
recognizeSuccess(userId)
return
}
// 未达阈值:暂不处理(无采集模式),等待下一次识别
}
/**
* 识别成功:防抖后查询用户信息,走开柜门业务流程
*/
private fun recognizeSuccess(userId: String?) {
if (userId.isNullOrBlank()) {
Timber.i("recognizeSuccess 识别成功但 userId 为空,忽略")
return
}
Timber.i("recognizeSuccess userId=$userId")
loginDebouncer.debounce {
viewModel.getUserInfoById(memberId = userId)
}
}
override fun onDestroy() {
Timber.d("onDestroy")
instance = null
if (rgbCameraHelper != null) {
rgbCameraHelper!!.release()
rgbCameraHelper = null
}
recognizeViewModel.destroy()
EventBus.getDefault().unregister(this)
super.onDestroy()
}
/**
* 调整View的宽高,使预览显示正常且采集框固定为380x380
*
* @param rgbPreview RGB预览View
* @param previewView 显示预览数据的view
* @param faceRectView 画框的view (380x380)
* @param previewSize 预览大小
* @param displayOrientation 相机旋转角度
* @param scale 缩放比例
* @return 调整后的LayoutParams
*/
private fun adjustPreviewViewSize(
rgbPreview: View,
previewView: View,
faceRectView: FaceRectView,
previewSize: Camera.Size,
displayOrientation: Int,
scale: Float
): ViewGroup.LayoutParams {
val layoutParams = previewView.layoutParams
val measuredWidth = previewView.measuredWidth
val measuredHeight = previewView.measuredHeight
var ratio = (previewSize.height.toFloat()) / previewSize.width.toFloat()
if (ratio > 1) {
ratio = 1 / ratio
}
if (displayOrientation % 180 == 0) {
layoutParams.width = measuredWidth
layoutParams.height = (measuredWidth * ratio).toInt()
} else {
layoutParams.height = measuredHeight
layoutParams.width = (measuredHeight * ratio).toInt()
}
if (scale < 1f) {
val rgbParam = rgbPreview.getLayoutParams()
layoutParams.width = (rgbParam.width * scale).toInt()
layoutParams.height = (rgbParam.height * scale).toInt()
} else {
layoutParams.width = (layoutParams.width * scale).toInt()
layoutParams.height = (layoutParams.height * scale).toInt()
}
val metrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(metrics)
if (layoutParams.width >= metrics.widthPixels) {
val viewRatio = layoutParams.width / (metrics.widthPixels.toFloat())
layoutParams.width = (layoutParams.width / viewRatio).toInt()
layoutParams.height = (layoutParams.height / viewRatio).toInt()
}
if (layoutParams.height >= metrics.heightPixels) {
val viewRatio = layoutParams.height / (metrics.heightPixels.toFloat())
layoutParams.width = (layoutParams.width / viewRatio).toInt()
layoutParams.height = (layoutParams.height / viewRatio).toInt()
}
previewView.setLayoutParams(layoutParams)
faceRectView.setLayoutParams(layoutParams)
return layoutParams
}
private fun initRgbCamera() {
val cameraListener: CameraListener = object : CameraListener {
override fun onCameraOpened(
camera: Camera,
cameraId: Int,
displayOrientation: Int,
isMirror: Boolean
) {
Timber.d("initRgbCamera Rgb onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation")
runOnUiThread({
val previewSizeRgb = camera.getParameters().getPreviewSize()
val layoutParams = adjustPreviewViewSize(
binding.dualCameraTexturePreviewRgb,
binding.dualCameraTexturePreviewRgb, binding.dualCameraFaceRectView,
previewSizeRgb, displayOrientation, 0.7f
)
Timber.d("initRgbCamera previewSizeRgb = ${previewSizeRgb.width}-${previewSizeRgb.height}")
Timber.d("initRgbCamera layoutParams = ${layoutParams.width}-${layoutParams.height}")
Timber.d(
"initRgbCamera isMirror = ${isMirror}, isDrawRgbRectHorizontalMirror = ${
ConfigUtil.isDrawRgbRectHorizontalMirror(
context
)
}, isDrawRgbRectVerticalMirror = ${
ConfigUtil.isDrawRgbRectVerticalMirror(
context
)
}"
)
rgbFaceRectTransformer = FaceRectTransformer(
previewSizeRgb.width,
previewSizeRgb.height,
layoutParams.width,
layoutParams.height,
90,
cameraId,
isMirror,
true,
true
)
recognizeViewModel.onRgbCameraOpened(camera)
recognizeViewModel.setRgbFaceRectTransformer(rgbFaceRectTransformer)
})
}
@RequiresApi(api = Build.VERSION_CODES.Q)
override fun onPreview(nv21: ByteArray?, camera: Camera?) {
if (!isRecognition) {
return
}
binding.dualCameraFaceRectView.clearFaceInfo()
val facePreviewInfoList: MutableList<FacePreviewInfo?>? =
recognizeViewModel.onPreviewFrame(nv21, true)
if (facePreviewInfoList != null && rgbFaceRectTransformer != null) {
drawPreviewInfo(facePreviewInfoList)
}
recognizeViewModel.clearLeftFace(facePreviewInfoList)
}
override fun onCameraClosed() {
Timber.i("initRgbCamera onCameraClosed: ")
}
override fun onCameraError(e: java.lang.Exception) {
Timber.i("initRgbCamera onCameraError: %s", e.message)
e.printStackTrace()
}
override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) {
Timber.i("initRgbCamera onCameraConfigurationChanged: threadName = ${Thread.currentThread().name}")
if (rgbFaceRectTransformer != null) {
rgbFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation
}
Timber.i("initRgbCamera onCameraConfigurationChanged: $cameraID $displayOrientation")
}
}
val previewConfig: PreviewConfig = recognizeViewModel.previewConfig
rgbCameraHelper = DualCameraHelper.Builder()
.previewViewSize(
Point(
binding.dualCameraTexturePreviewRgb.measuredWidth,
binding.dualCameraTexturePreviewRgb.measuredHeight
)
)
.rotation(windowManager.defaultDisplay.rotation)
.additionalRotation(0) // 角度
.previewSize(recognizeViewModel.loadPreviewSize())
.specificCameraId(previewConfig.rgbCameraId)
.isMirror(true)
.previewOn(binding.dualCameraTexturePreviewRgb)
.cameraListener(cameraListener)
.build()
rgbCameraHelper!!.init()
rgbCameraHelper!!.start()
}
/**
* 绘制RGB、IR画面的实时人脸信息
*
* @param facePreviewInfoList RGB画面的实时人脸信息
*/
private fun drawPreviewInfo(facePreviewInfoList: MutableList<FacePreviewInfo?>) {
if (rgbFaceRectTransformer != null) {
val rgbDrawInfoList: MutableList<DrawInfo?>? = recognizeViewModel.getDrawInfo(
facePreviewInfoList,
LivenessType.RGB,
openRectInfoDraw
)
// 识别成功
binding.dualCameraFaceRectView.drawRealtimeFaceInfo(rgbDrawInfoList)
}
}
fun openRectInfoDraw(view: View?) {
openRectInfoDraw = !openRectInfoDraw
recognizeViewModel.setDrawRectInfoTextValue(openRectInfoDraw)
}
override fun onGlobalLayout() {
Timber.d("onGlobalLayout")
binding.dualCameraTexturePreviewRgb.getViewTreeObserver().removeOnGlobalLayoutListener(this)
// 请求权限
checkCameraPermission()
}
private fun resumeCamera() {
val helper = rgbCameraHelper
if (helper != null && helper.isStopped) {
isRecognition = true
helper.start()
} else {
// 相机未停止时,发送黑帧清除 ViewModel 内部识别缓存,防止恢复后使用旧数据
recognizeViewModel.onPreviewFrame(emptyFrame, true)
binding.dualCameraFaceRectView.clearFaceInfo()
binding.root.postDelayed({
isRecognition = true
}, 500)
}
}
override fun onResume() {
super.onResume()
// 清空上次识别结果和 FaceHelper 内部状态,防止短时间内再次识别无法触发
recognizeViewModel.resetFaceState()
resumeCamera()
viewModel.resetUserInfo()
countDownTimer?.let {
it.cancel()
it.start()
}
}
protected override fun onPause() {
pauseCamera()
super.onPause()
countDownTimer?.cancel()
}
private fun pauseCamera() {
isRecognition = false
recognizeViewModel.onPreviewFrame(emptyFrame, true)
}
/**
* 构造符合分辨率的 NV21 格式全黑帧
* @param width 图像宽度
* @param height 图像高度
* @return 合法的全黑帧字节数组
*/
private fun createBlackNV21Frame(width: Int, height: Int): ByteArray {
val frameSize = width * height
val nv21Data = ByteArray(frameSize * 3 / 2) // NV21 格式长度 = 宽×高×1.5
// Y 分量(亮度)设为 0(全黑),UV 分量设为 128(默认值)
nv21Data.fill(0, 0, frameSize)
nv21Data.fill(128.toByte(), frameSize, nv21Data.size)
return nv21Data
}
private val emptyFrame = createBlackNV21Frame(1280, 720)
override fun handleLoginSuccess(equipmentUserInfo: EquipmentUserInfo, isAdmin: Boolean) {
super.handleLoginSuccess(equipmentUserInfo, isAdmin)
// goInitActivity()
}
fun initCountTime() {
val totalTimeInMillis = 30L * 1000
countDownTimer = object : CountDownTimer(totalTimeInMillis, 1000) {
override fun onTick(millisUntilFinished: Long) {
Timber.d("initCountTime onTick = ${(millisUntilFinished / 1000).toInt()}")
binding.tvToInit.text = "${(millisUntilFinished / 1000).toInt() + 1}"
}
override fun onFinish() {
goInitActivity()
}
}.start()
}
fun goInitActivity() {
val intent = Intent(context, InitActivity::class.java)
startActivity(intent)
}
}