Эх сурвалжийг харах

feat(人脸识别):
- 新增HLK-223人脸识别模组作为可选后端,与虹软(ArcSoft)引擎并行支持。
- `FaceUtil` (原 `ArcSoftUtil`) 新增后端切换机制,可通过 `enableHlkBackend` 切换至HLK模组。
- 在HLK模式下,人脸检测、活体识别和1:N验证完全由硬件模组处理,App仅负责预览和回调。
- 引入ML Kit进行人脸框的实时绘制,并配合HLK的NOTE上报进行状态同步。
- `Hlk223Client`: 重构了与模组的通信逻辑,实现了`startVerifyWithNotes`方法,用于持续监听和解析模组的`NOTE`(人脸状态、活体)和`REPLY`(识别结果)帧,并增加了对粘包、半包、丢帧的健壮性处理。
- `Hlk223Frames`: 新增 `Framer` 类,用于增量式地从数据流中解析出完整的HLK协议帧。

refactor(人脸识别):
- `ArcSoftUtil.kt` 重命名为 `FaceUtil.kt`,并统一所有模块对其的引用。
- 优化了登录 (`checkCamera`) 和注册 (`initCamera`) 时的人脸识别流程:
- **HLK模式**: 调用 `hlkClient.startVerifyWithNotes` 启动硬件识别,通过回调获取`userId`。
- **ArcSoft模式**: 维持原有的本地SDK识别逻辑。
- 在HLK模式下,图片注册(`registerFace`)逻辑调整为调用 `enrollWithPhoto`,将压缩后的JPG图片下发给模组进行注册。
- `Hlk223Client`: 增加了`stopVerify`方法,以可靠地中止模组的识别流程。

feat(用户):
- 在`SetJobCardFragment`中,进入/退出设置作业卡时,通过发送`InRFIDScanModeEvent`事件来全局控制RFID扫描模式的开关。

refactor(启动):
- `SplashActivity`: 优化应用启动流程,改为先快速跳转至目标页面(`LoginActivity`/`InitActivity`),再通过`ProcessLifecycleOwner`在后台协程中执行耗时任务(如数据库预热、硬件连接、数据预置),避免启动页卡顿,提升了用户体验。
- 在应用启动时,增加打印上次异常退出原因的日志,便于问题排查。

fix(指纹):
- 在指纹录入流程中,当连续失败次数达到上限或录入成功时,确保加载提示(`Loading`)被及时关闭,防止UI卡死。

fix(UI):
- `JobExecuteFragment`: 将共锁人、待共锁人列表的布局管理器从`GridLayoutManager`改为`FlexboxLayoutManager`,解决了在部分设备上item显示不全或重叠的问题。
- 作业票步骤列表: 移除步骤布局的背景着色(`bg_tint`),解决换肤后背景色不正确的问题。
- `item_locker_group.xml`: 调整布局高度为`wrap_content`,修复了部分场景下分组显示不全的问题。
- `BaseActivity`: 修复了通过快捷入口跳转时,若目标页面不在当前`NavGraph`中,无法正确跳转的问题。

chore(依赖):
- 升级通信库 `sik-comm` 版本从 `1.0.17` 至 `1.0.18`。
- 新增 `com.google.mlkit:face-detection` 依赖,用于在HLK模式下绘制人脸框。

周文健 2 долоо хоног өмнө
parent
commit
a36e4a4eb2
52 өөрчлөгдсөн 1414 нэмэгдсэн , 972 устгасан
  1. 2 2
      data/src/main/java/com/grkj/data/hardware/BiometricVerifier.kt
  2. 0 579
      data/src/main/java/com/grkj/data/hardware/face/ArcSoftUtil.kt
  3. 627 0
      data/src/main/java/com/grkj/data/hardware/face/FaceUtil.kt
  4. 237 115
      data/src/main/java/com/grkj/data/hardware/face/hlk/Hlk223Client.kt
  5. 1 0
      data/src/main/java/com/grkj/data/hardware/face/hlk/Hlk223Config.kt
  6. 51 4
      data/src/main/java/com/grkj/data/hardware/face/hlk/Hlk223Frames.kt
  7. 1 1
      data/src/main/java/com/grkj/data/hardware/face/hlk/Hlk223PhotoEnroll.kt
  8. 1 1
      gradle/libs.versions.toml
  9. 4 4
      iscs_lock/src/main/AndroidManifest.xml
  10. 5 5
      iscs_lock/src/main/assets/preset/CN/preset_workflow_step.json
  11. 40 7
      iscs_lock/src/main/java/com/grkj/iscs/ISCSApplication.kt
  12. 8 11
      iscs_lock/src/main/java/com/grkj/iscs/features/login/dialog/LoginDialog.kt
  13. 2 2
      iscs_lock/src/main/java/com/grkj/iscs/features/login/viewmodel/LoginViewModel.kt
  14. 23 26
      iscs_lock/src/main/java/com/grkj/iscs/features/main/dialog/CheckFaceDialog.kt
  15. 9 10
      iscs_lock/src/main/java/com/grkj/iscs/features/main/dialog/data_manage/RegisterFaceDialog.kt
  16. 2 2
      iscs_lock/src/main/java/com/grkj/iscs/features/main/entity/QuickEntranceMenuItemEntity.kt
  17. 4 0
      iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/data_manage/UserManageFragment.kt
  18. 15 17
      iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/hardware_manage/LockManageFragment.kt
  19. 1 1
      iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/home/HomeFragment.kt
  20. 2 0
      iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/CreateJobFragment.kt
  21. 2 0
      iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/CreateSopFragment.kt
  22. 2 0
      iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/CreateSopJobFragment.kt
  23. 2 0
      iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/EditJobFragment.kt
  24. 2 0
      iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/EditSopFragment.kt
  25. 2 0
      iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/EditSopJobFragment.kt
  26. 32 3
      iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/JobExecuteFragment.kt
  27. 9 9
      iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/user_info/SetFaceFragment.kt
  28. 4 0
      iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/user_info/SetFingerprintFragment.kt
  29. 3 0
      iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/user_info/SetJobCardFragment.kt
  30. 9 11
      iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/user_info/UserInfoFragment.kt
  31. 2 2
      iscs_lock/src/main/java/com/grkj/iscs/features/main/viewmodel/user_info/UserInfoViewModel.kt
  32. 85 93
      iscs_lock/src/main/java/com/grkj/iscs/features/splash/activity/SplashActivity.kt
  33. 26 1
      iscs_lock/src/main/res/drawable/icon_add.xml
  34. 1 1
      iscs_lock/src/main/res/layout/item_home_text_drop_down.xml
  35. 2 3
      iscs_lock/src/main/res/layout/item_locker_group.xml
  36. 1 0
      iscs_lock/src/main/res/layout/item_point_group.xml
  37. 10 7
      iscs_mc/src/main/java/com/grkj/iscs_mc/ISCSMCApplication.kt
  38. 8 9
      iscs_mc/src/main/java/com/grkj/iscs_mc/features/login/dialog/LoginDialog.kt
  39. 7 9
      iscs_mc/src/main/java/com/grkj/iscs_mc/features/login/fragment/LoginFragment.kt
  40. 2 2
      iscs_mc/src/main/java/com/grkj/iscs_mc/features/login/viewmodel/LoginViewModel.kt
  41. 9 10
      iscs_mc/src/main/java/com/grkj/iscs_mc/features/main/dialog/data_manage/RegisterFaceDialog.kt
  42. 4 0
      iscs_mc/src/main/java/com/grkj/iscs_mc/features/main/fragment/data_manage/UserManageFragment.kt
  43. 9 9
      iscs_mc/src/main/java/com/grkj/iscs_mc/features/main/fragment/user_info/SetFaceFragment.kt
  44. 4 0
      iscs_mc/src/main/java/com/grkj/iscs_mc/features/main/fragment/user_info/SetFingerprintFragment.kt
  45. 9 11
      iscs_mc/src/main/java/com/grkj/iscs_mc/features/main/fragment/user_info/UserInfoFragment.kt
  46. 2 2
      iscs_mc/src/main/java/com/grkj/iscs_mc/features/main/viewmodel/user_info/UserInfoViewModel.kt
  47. 1 0
      shared/build.gradle.kts
  48. 22 0
      shared/src/main/java/com/grkj/shared/utils/Crc32Utils.kt
  49. 96 0
      shared/src/main/java/com/grkj/shared/utils/ImageCompress.kt
  50. 4 0
      shared/src/main/java/com/grkj/shared/widget/FaceOverlayView.kt
  51. 6 1
      ui-base/src/main/java/com/grkj/ui_base/base/BaseActivity.kt
  52. 2 2
      ui-base/src/main/java/com/grkj/ui_base/utils/event/InRFIDScanModeEvent.kt

+ 2 - 2
data/src/main/java/com/grkj/data/hardware/BiometricVerifier.kt

@@ -1,7 +1,7 @@
 package com.grkj.data.hardware
 
 import android.util.Base64
-import com.grkj.data.hardware.face.ArcSoftUtil
+import com.grkj.data.hardware.face.FaceUtil
 import com.machinezoo.sourceafis.FingerprintMatcher
 import com.machinezoo.sourceafis.FingerprintTemplate
 import kotlinx.coroutines.Dispatchers
@@ -97,6 +97,6 @@ object BiometricVerifier {
         threshold: Float = 0.7f
     ): Boolean {
         // compareResult.score 在 [0,1],越大越像
-        return ArcSoftUtil.verifyFaceArcSoft(b64a, b64b, threshold)
+        return FaceUtil.verifyFaceArcSoft(b64a, b64b, threshold)
     }
 }

+ 0 - 579
data/src/main/java/com/grkj/data/hardware/face/ArcSoftUtil.kt

@@ -1,579 +0,0 @@
-package com.grkj.data.hardware.face
-
-// 新增:协程(你文件里用到了 CoroutineScope/Dispatchers/SupervisorJob)
-import android.Manifest
-import android.content.Context
-import android.graphics.Bitmap
-import android.graphics.BitmapFactory
-import android.graphics.Point
-import android.hardware.Camera
-import android.util.Base64
-import android.util.DisplayMetrics
-import android.view.View
-import android.view.WindowManager
-import com.arcsoft.face.ActiveFileInfo
-import com.arcsoft.face.ErrorInfo
-import com.arcsoft.face.FaceEngine
-import com.arcsoft.face.FaceFeature
-import com.arcsoft.face.FaceFeatureInfo
-import com.arcsoft.face.FaceInfo
-import com.arcsoft.face.FaceSimilar
-import com.arcsoft.face.LivenessInfo
-import com.arcsoft.face.enums.DetectFaceOrientPriority
-import com.arcsoft.face.enums.DetectMode
-import com.arcsoft.face.enums.ExtractType
-import com.grkj.data.hardware.face.hlk.Hlk223Client
-import com.grkj.shared.config.Constants
-import com.grkj.shared.utils.DisplayUtils
-import com.grkj.shared.utils.extension.isInCenterArea
-import com.grkj.shared.utils.face.arcsoft.CameraHelper
-import com.grkj.shared.utils.face.arcsoft.CameraListener
-import com.grkj.shared.widget.FaceOverlayView
-import com.sik.sikimage.ImageConvertUtils
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.SupervisorJob
-import org.json.JSONObject
-import org.slf4j.Logger
-import org.slf4j.LoggerFactory
-import java.io.File
-
-/**
- * ArcSoft 人脸:老工具类(兼容新实现)
- * - 保留所有旧方法签名
- * - 新增:后端切换(HLK 模组)但不改外部 API;HLK 不支持“图对图”比对时降级到本地算分
- */
-object ArcSoftUtil {
-    private val logger: Logger = LoggerFactory.getLogger(ArcSoftUtil::class.java)
-
-    // == 新增:后端切换(默认 ArcSoft) ==
-    private enum class FaceBackend { ARC, HLK }
-
-    @Volatile
-    private var backend: FaceBackend = FaceBackend.ARC
-    @Volatile
-    private var hlkClient: Hlk223Client? = null
-
-    /** 开启 HLK 后端(不改对外接口,仅改变内部实现策略) */
-    @JvmStatic
-    fun enableHlkBackend(client: Hlk223Client) {
-        hlkClient = client
-        backend = FaceBackend.HLK
-        logger.info("ArcSoftUtil backend -> HLK")
-    }
-
-    /** 关闭 HLK 后端,回到 ArcSoft */
-    @JvmStatic
-    fun disableHlkBackend() {
-        backend = FaceBackend.ARC
-        hlkClient = null
-        logger.info("ArcSoftUtil backend -> ARC")
-    }
-
-    // ---- 兼容新实现:本地配置 ----
-    private const val CONFIG_DIR = "arcsoft_config"
-    private const val CONFIG_FILE = "ArcSoftConfig.json"
-    private const val OFFLINE_DAT = "OfflineActive.dat"
-
-    // JSON key 与 iscs_mars 的 ArcSoftLicenseConfig 对齐
-    private data class ArcSoftLicenseConfig(
-        val appId: String,
-        val sdkKey: String,
-        val activeKey: String,
-        val activeOnline: Boolean,
-        val activeOfflineFilePath: String
-    )
-
-    private fun readOrInitConfig(context: Context): ArcSoftLicenseConfig {
-        val dir = File("/sdcard/iscs/", CONFIG_DIR).apply { if (!exists()) mkdirs() }
-        val cfg = File(dir, CONFIG_FILE)
-        val offlineDat = File(dir, OFFLINE_DAT)
-        if (!offlineDat.exists()) runCatching { offlineDat.createNewFile() }
-        if (!cfg.exists()) {
-            val def = ArcSoftLicenseConfig(
-                appId = Constants.APP_ID,
-                sdkKey = Constants.SDK_KEY,
-                activeKey = Constants.ACTIVE_KEY,
-                activeOnline = false,
-                activeOfflineFilePath = offlineDat.absolutePath
-            )
-            cfg.writeText(
-                JSONObject().apply {
-                    put("appId", def.appId)
-                    put("sdkKey", def.sdkKey)
-                    put("activeKey", def.activeKey)
-                    put("activeOnline", def.activeOnline)
-                    put("activeOfflineFilePath", def.activeOfflineFilePath)
-                }.toString()
-            )
-            return def
-        }
-        val json = runCatching { JSONObject(cfg.readText()) }.getOrElse { JSONObject() }
-        fun j(key: String, def: String) = json.optString(key, def).ifBlank { def }
-        return ArcSoftLicenseConfig(
-            appId = j("appId", Constants.APP_ID),
-            sdkKey = j("sdkKey", Constants.SDK_KEY),
-            activeKey = j("activeKey", Constants.ACTIVE_KEY),
-            activeOnline = json.optBoolean("activeOnline", false),
-            activeOfflineFilePath = j("activeOfflineFilePath", offlineDat.absolutePath)
-        )
-    }
-
-    // ---- 旧有字段(保持) ----
-    private var cameraHelper: CameraHelper? = null
-    private var previewSize: Camera.Size? = null
-    private var rgbCameraId: Int? = null
-    private var faceEngine: FaceEngine? = null
-    private val cameraWidth: Int = 640
-    private val cameraHeight: Int = 480
-    private var afCode = -1
-    private val processMask: Int = FaceEngine.ASF_MASK_DETECT or FaceEngine.ASF_LIVENESS
-    private val registerFaceFeatureJob get() = CoroutineScope(Dispatchers.IO + SupervisorJob())
-
-    private const val ACTION_REQUEST_PERMISSIONS: Int = 0x001
-    var isActivated = false
-
-    @Volatile
-    var inDetecting = false
-
-    // 新增:未检出人脸提示节流
-    private var startDetectFaceTime = 0L
-    private fun maybeLogNoFaceTip() {
-        val now = System.currentTimeMillis()
-        if (now - startDetectFaceTime > 3000) {
-            logger.info("未检测到人脸,请靠近/居中/保证光照")
-            startDetectFaceTime = now
-        }
-    }
-
-    private fun findCameraIdByFacing(facing: Int): Int? {
-        val count = Camera.getNumberOfCameras()
-        val info = Camera.CameraInfo()
-        for (id in 0 until count) {
-            Camera.getCameraInfo(id, info)
-            if (info.facing == facing) return id
-        }
-        return null
-    }
-
-    /**
-     * 所需的所有权限信息
-     */
-    private val NEEDED_PERMISSIONS: Array<String?> = arrayOf(
-        Manifest.permission.CAMERA, Manifest.permission.READ_PHONE_STATE
-    )
-
-    fun checkActiveStatus(context: Context) {
-        val cfg = readOrInitConfig(context)
-        val activeCode = try {
-            if (cfg.activeOnline) {
-                FaceEngine.activeOnline(context, cfg.activeKey, cfg.appId, cfg.sdkKey)
-            } else {
-                FaceEngine.activeOffline(context, cfg.activeOfflineFilePath)
-            }
-        } catch (e: Throwable) {
-            logger.error("激活异常: ${e.message}", e)
-            ErrorInfo.MERR_UNKNOWN
-        }
-
-        when (activeCode) {
-            ErrorInfo.MOK -> {
-                isActivated = true
-                logger.info("checkActiveStatus : active success (online=${cfg.activeOnline})")
-            }
-
-            ErrorInfo.MERR_ASF_ALREADY_ACTIVATED -> {
-                isActivated = true
-                logger.info("checkActiveStatus : already activated")
-            }
-
-            else -> {
-                isActivated = false
-                logger.error("checkActiveStatus : active failed $activeCode")
-            }
-        }
-
-        val activeFileInfo = ActiveFileInfo()
-        val res = FaceEngine.getActiveFileInfo(context, activeFileInfo)
-        if (res == ErrorInfo.MOK) {
-            logger.info("getActiveFileInfo: $activeFileInfo")
-        }
-    }
-
-    fun initEngine(context: Context) {
-        faceEngine = FaceEngine()
-        afCode = faceEngine!!.init(
-            context,
-            DetectMode.ASF_DETECT_MODE_VIDEO,
-            DetectFaceOrientPriority.ASF_OP_0_ONLY,
-            1,
-            FaceEngine.ASF_FACE_DETECT or FaceEngine.ASF_MASK_DETECT or
-                    FaceEngine.ASF_LIVENESS or FaceEngine.ASF_FACE_RECOGNITION
-        )
-        logger.info("initEngine:  init: $afCode")
-        if (afCode != ErrorInfo.MOK) {
-            logger.info("初始化失败: code=$afCode")
-        }
-    }
-
-    fun unInitEngine() {
-        if (afCode == 0) {
-            afCode = faceEngine!!.unInit()
-            logger.info("unInitEngine: $afCode")
-        }
-    }
-
-    @JvmOverloads
-    fun initCamera(
-        preview: View,
-        faceOverlayView: FaceOverlayView? = null,
-        needCheckCenter: Boolean = false,
-        callBack: (Bitmap?, Int, Boolean) -> Unit
-    ) {
-        if (rgbCameraId == null) {
-            rgbCameraId = findCameraIdByFacing(Camera.CameraInfo.CAMERA_FACING_FRONT)
-                ?: findCameraIdByFacing(Camera.CameraInfo.CAMERA_FACING_BACK) // 兜底
-        }
-        val camId = rgbCameraId ?: run {
-            logger.error("找不到可用相机ID"); return
-        }
-        val cameraListener: CameraListener = object : CameraListener {
-            override fun onCameraOpened(
-                camera: Camera, cameraId: Int, displayOrientation: Int, isMirror: Boolean
-            ) {
-                logger.info("onCameraOpened: $cameraId  $displayOrientation $isMirror")
-                startDetectFaceTime = System.currentTimeMillis()
-                previewSize = camera.parameters.previewSize
-                faceOverlayView?.setCameraPreviewSize(previewSize!!.width, previewSize!!.height)
-            }
-
-            override fun onPreview(nv21: ByteArray?, camera: Camera?) {
-                if (inDetecting) return
-                inDetecting = true
-                val faces: MutableList<FaceInfo> = ArrayList()
-                val fe = faceEngine ?: run {
-                    inDetecting = false; return
-                }
-                val psize = previewSize ?: run {
-                    inDetecting = false; return
-                }
-                var code = fe.detectFaces(
-                    nv21, psize.width, psize.height, FaceEngine.CP_PAF_NV21, faces
-                )
-                faceOverlayView?.setFaceRect(faces.map { it.rect })
-
-                if (code != ErrorInfo.MOK || faces.isEmpty()) {
-                    maybeLogNoFaceTip()
-                    inDetecting = false
-                    return
-                }
-
-                // 处理活体/口罩等
-                code = fe.process(
-                    nv21, psize.width, psize.height, FaceEngine.CP_PAF_NV21, faces, processMask
-                )
-                if (code != ErrorInfo.MOK) {
-                    inDetecting = false
-                    return
-                }
-
-                val liveList: MutableList<LivenessInfo> = ArrayList()
-                val livenessCode = fe.getLiveness(liveList)
-                if (livenessCode != ErrorInfo.MOK) {
-                    inDetecting = false
-                    return
-                }
-
-                // 仅 ALIVE 通过
-                if (liveList.none { it.liveness == LivenessInfo.ALIVE }) {
-                    callBack(null, faces.size, false)
-                    inDetecting = false
-                    return
-                }
-
-                // 可选:中心区域约束
-                if (needCheckCenter && !faces[0].rect.isInCenterArea(
-                        psize.width,
-                        psize.height
-                    )
-                ) {
-                    inDetecting = false
-                    return
-                }
-
-                val bmp = ImageConvertUtils.nv21ToBitmap(nv21, psize.width, psize.height)
-                logger.debug("人脸检测结果-识别结果 : ${bmp == null} - $faces")
-                callBack(bmp, faces.size, true)
-
-            }
-
-            override fun onCameraClosed() {
-                logger.info("onCameraClosed")
-            }
-
-            override fun onCameraError(e: Exception) {
-                logger.info("onCameraError: ${e.message}")
-            }
-
-            override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) {
-                logger.info("onCameraConfigurationChanged: $cameraID  $displayOrientation")
-            }
-        }
-
-        val rotation = DisplayUtils.getRotation(preview.context) // 别用 Application 拿 rotation
-        cameraHelper = CameraHelper.Builder()
-            .previewViewSize(Point(cameraWidth, cameraHeight))
-            .rotation(rotation)
-            .specificCameraId(camId)
-            .isMirror(true) // 前摄预览一般要镜像,非必需但更自然
-            .previewOn(preview)
-            .cameraListener(cameraListener)
-            .build()
-        cameraHelper!!.init()
-        cameraHelper!!.start()
-    }
-
-    /**
-     * 登录用(保持签名不变)
-     */
-    fun checkCamera(
-        windowManager: WindowManager,
-        preview: View,
-        callBack: (Bitmap?, Long?) -> Unit
-    ) {
-        if (rgbCameraId == null) {
-            rgbCameraId = findCameraIdByFacing(Camera.CameraInfo.CAMERA_FACING_FRONT)
-                ?: findCameraIdByFacing(Camera.CameraInfo.CAMERA_FACING_BACK) // 兜底
-        }
-        val camId = rgbCameraId ?: run {
-            logger.error("找不到可用相机ID"); return
-        }
-        val metrics = DisplayMetrics()
-        windowManager.defaultDisplay.getMetrics(metrics)
-
-        val cameraListener: CameraListener = object : CameraListener {
-            override fun onCameraOpened(
-                camera: Camera, cameraId: Int, displayOrientation: Int, isMirror: Boolean
-            ) {
-                logger.info("onCameraOpened: $cameraId  $displayOrientation $isMirror")
-                previewSize = camera.parameters.previewSize
-            }
-
-            override fun onPreview(nv21: ByteArray?, camera: Camera?) {
-                if (inDetecting) return
-                inDetecting = true
-                val fe = faceEngine ?: run { return }
-                val psize = previewSize ?: run { return }
-
-                val faces: MutableList<FaceInfo> = ArrayList()
-                var code = fe.detectFaces(
-                    nv21, psize.width, psize.height, FaceEngine.CP_PAF_NV21, faces
-                )
-                if (code != ErrorInfo.MOK || faces.isEmpty()) return
-
-                code = fe.process(
-                    nv21, psize.width, psize.height, FaceEngine.CP_PAF_NV21, faces, processMask
-                )
-                if (code != ErrorInfo.MOK) return
-
-                val liveList: MutableList<LivenessInfo> = ArrayList()
-                val livenessCode = fe.getLiveness(liveList)
-                if (livenessCode != ErrorInfo.MOK) return
-                if (liveList.none { it.liveness == LivenessInfo.ALIVE }) {
-                    callBack(null, null); return
-                }
-
-                val ft = FaceFeature()
-                fe.extractFaceFeature(
-                    nv21, psize.width, psize.height, FaceEngine.CP_PAF_NV21, faces[0],
-                    ExtractType.RECOGNIZE, 0, ft
-                )
-                val searchResult = runCatching {
-                    if (fe.faceCount > 0) fe.searchFaceFeature(ft) else null
-                }.onFailure { logger.info("搜索人脸异常") }.getOrNull()
-
-                logger.debug("人脸检测结果-识别结果 : ${searchResult?.faceFeatureInfo} - $faces")
-                val bmp = ImageConvertUtils.nv21ToBitmap(nv21, psize.width, psize.height)
-                callBack(bmp, searchResult?.faceFeatureInfo?.searchId?.toLong())
-            }
-
-            override fun onCameraClosed() {
-                logger.info("onCameraClosed")
-            }
-
-            override fun onCameraError(e: Exception) {
-                logger.info("onCameraError: ${e.message}")
-            }
-
-            override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) {
-                logger.info("onCameraConfigurationChanged: $cameraID  $displayOrientation")
-            }
-        }
-
-        val rotation = DisplayUtils.getRotation(preview.context) // 别用 Application 拿 rotation
-        cameraHelper = CameraHelper.Builder()
-            .previewViewSize(Point(cameraWidth, cameraHeight))
-            .rotation(rotation)
-            .specificCameraId(camId)
-            .isMirror(true) // 前摄预览一般要镜像,非必需但更自然
-            .previewOn(preview)
-            .cameraListener(cameraListener)
-            .build()
-        cameraHelper!!.init()
-        cameraHelper!!.start()
-    }
-
-    fun stop() {
-        cameraHelper?.release()
-        cameraHelper = null
-        // 与新策略对齐:退出时回落到本地 ARC,避免悬挂的 HLK 引用
-//        disableHlkBackend()
-        // 如需同时释放引擎,可解开下行:
-        // unInitEngine()
-    }
-
-    // ----------------- 下方原有注册/比对逻辑保持不变 -----------------
-
-    /**
-     * 注册人脸
-     */
-    fun registerFace(faceData: List<Pair<Long, String>>) {
-        faceEngine?.removeFaceFeature(-1)
-        faceData.forEachIndexed { _, userFace ->
-            val faceBitmap = decodeBase64ToBitmap(userFace.second)
-            val imageData = bitmapToBgr24(faceBitmap)
-            val faceInfoList = mutableListOf<FaceInfo>()
-            val code = faceEngine?.detectFaces(
-                imageData,
-                faceBitmap.width,
-                faceBitmap.height,
-                FaceEngine.CP_PAF_BGR24,
-                faceInfoList
-            )
-            logger.info("人脸检测结果:${code}")
-            if (faceInfoList.isEmpty()) {
-                logger.info("没有检测出人脸")
-                return@forEachIndexed
-            }
-            val faceFeature = FaceFeature()
-            faceEngine?.extractFaceFeature(
-                imageData, faceBitmap.width, faceBitmap.height, FaceEngine.CP_PAF_BGR24,
-                faceInfoList[0], ExtractType.REGISTER, 0, faceFeature
-            )
-            val faceFeatureInfo = FaceFeatureInfo(userFace.first.toInt(), faceFeature.featureData)
-            try {
-                if ((faceEngine?.searchFaceFeature(faceFeature)?.maxSimilar ?: 0f) > 0.5) {
-                    val up = faceEngine?.updateFaceFeature(faceFeatureInfo)
-                    logger.info("特征更新:${up}")
-                } else {
-                    val reg = faceEngine?.registerFaceFeature(faceFeatureInfo)
-                    logger.info("特征注册:${reg}")
-                }
-            } catch (e: Exception) {
-                logger.info("找不到人脸,直接注册")
-                val reg = faceEngine?.registerFaceFeature(faceFeatureInfo)
-                logger.info("特征注册:${reg}")
-            }
-        }
-    }
-
-    /**
-     * 比对两张 Base64 人脸,返回是否同人
-     * @param threshold 阈值,一般设置 0.7f 左右
-     *
-     * 说明:
-     * - HLK 模式下协议不支持“图对图”直接比对,这里**降级为本地 ArcSoft 算分**以保证行为稳定、签名不变。
-     * - 若后续要用 HLK 做“设备端验证”,请在业务层走摄像头流并调用 HLKClient.verify()。
-     */
-    fun verifyFaceArcSoft(b64a: String, b64b: String, threshold: Float = 0.7f): Boolean {
-        if (b64a.isEmpty() || b64b.isEmpty()) return false
-
-        // 当选择 HLK 后端:协议上无“图片RPC比对”,安全降级为本地算分
-        if (backend == FaceBackend.HLK) {
-            logger.info("verifyFaceArcSoft: HLK backend active -> fallback to local ARC scoring")
-        }
-
-        val bmpA = decodeBase64ToBitmap(b64a)
-        val bmpB = decodeBase64ToBitmap(b64b)
-
-        val facesA = mutableListOf<FaceInfo>()
-        val facesB = mutableListOf<FaceInfo>()
-        val imgA = bitmapToBgr24(bmpA)
-        val imgB = bitmapToBgr24(bmpB)
-
-        val imgADetectResultCode = faceEngine?.detectFaces(
-            imgA, bmpA.width, bmpA.height, FaceEngine.CP_PAF_BGR24, facesA
-        )
-        val imgBDetectResultCode = faceEngine?.detectFaces(
-            imgB, bmpB.width, bmpB.height, FaceEngine.CP_PAF_BGR24, facesB
-        )
-        logger.info("人脸检测结果1:${facesA.size},${facesB.size}")
-        logger.info("人脸检测结果2:${imgADetectResultCode},${imgBDetectResultCode}")
-        if (facesA.isEmpty() || facesB.isEmpty()) return false
-
-        val ftA = FaceFeature()
-        val ftB = FaceFeature()
-        faceEngine?.extractFaceFeature(
-            imgA, bmpA.width, bmpA.height, FaceEngine.CP_PAF_BGR24, facesA[0],
-            ExtractType.RECOGNIZE, 0, ftA
-        )
-        faceEngine?.extractFaceFeature(
-            imgB, bmpB.width, bmpB.height, FaceEngine.CP_PAF_BGR24, facesB[0],
-            ExtractType.RECOGNIZE, 0, ftB
-        )
-
-        val compareResult = FaceSimilar()
-        val compareCode = faceEngine?.compareFaceFeature(ftA, ftB, compareResult)
-        if (compareCode != ErrorInfo.MOK) {
-            logger.info("特征比对结果:${compareCode}")
-            return false
-        }
-        logger.info("比对分数:${compareResult.score}")
-        return compareResult.score >= threshold
-    }
-
-    // —— 可复用的小工具:提取 2048B 特征(后续若要下发到 HLK,可直接拿这个结果) ——
-    private fun extractFeatureFromBase64(b64: String): ByteArray? {
-        return try {
-            val bmp = decodeBase64ToBitmap(b64)
-            val img = bitmapToBgr24(bmp)
-            val faces = mutableListOf<FaceInfo>()
-            val fe = faceEngine ?: return null
-            val code = fe.detectFaces(img, bmp.width, bmp.height, FaceEngine.CP_PAF_BGR24, faces)
-            if (code != ErrorInfo.MOK || faces.isEmpty()) return null
-            val feature = FaceFeature()
-            val ex = fe.extractFaceFeature(
-                img, bmp.width, bmp.height, FaceEngine.CP_PAF_BGR24,
-                faces[0], ExtractType.REGISTER, 0, feature
-            )
-            if (ex != ErrorInfo.MOK) null else feature.featureData
-        } catch (_: Throwable) {
-            null
-        }
-    }
-
-    private fun decodeBase64ToBitmap(b64: String): Bitmap {
-        val bytes = Base64.decode(b64, Base64.DEFAULT)
-        return BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
-    }
-
-    /** Bitmap ARGB_8888 -> BGR24 byte[] */
-    private fun bitmapToBgr24(bitmap: Bitmap): ByteArray {
-        val width = bitmap.width
-        val height = bitmap.height
-        val pixels = IntArray(width * height)
-        bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
-
-        val bgr = ByteArray(width * height * 3)
-        var idx = 0
-        for (pixel in pixels) {
-            val r = (pixel shr 16 and 0xFF).toByte()
-            val g = (pixel shr 8 and 0xFF).toByte()
-            val b = (pixel and 0xFF).toByte()
-            bgr[idx++] = b
-            bgr[idx++] = g
-            bgr[idx++] = r
-        }
-        return bgr
-    }
-}

+ 627 - 0
data/src/main/java/com/grkj/data/hardware/face/FaceUtil.kt

@@ -0,0 +1,627 @@
+package com.grkj.data.hardware.face
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.ImageFormat
+import android.graphics.Point
+import android.graphics.Rect
+import android.hardware.Camera
+import android.util.Base64
+import android.view.View
+import com.arcsoft.face.ActiveFileInfo
+import com.arcsoft.face.ErrorInfo
+import com.arcsoft.face.FaceEngine
+import com.arcsoft.face.FaceFeature
+import com.arcsoft.face.FaceFeatureInfo
+import com.arcsoft.face.FaceInfo
+import com.arcsoft.face.FaceSimilar
+import com.arcsoft.face.LivenessInfo
+import com.arcsoft.face.enums.DetectFaceOrientPriority
+import com.arcsoft.face.enums.DetectMode
+import com.arcsoft.face.enums.ExtractType
+import com.google.mlkit.vision.common.InputImage
+import com.google.mlkit.vision.face.FaceDetectorOptions
+import com.grkj.data.hardware.face.hlk.Hlk223Client
+import com.grkj.shared.config.Constants
+import com.grkj.shared.utils.DisplayUtils
+import com.grkj.shared.utils.ImageCompress
+import com.grkj.shared.utils.extension.isInCenterArea
+import com.grkj.shared.utils.face.arcsoft.CameraHelper
+import com.grkj.shared.utils.face.arcsoft.CameraListener
+import com.grkj.shared.widget.FaceOverlayView
+import com.sik.sikcore.thread.ThreadUtils
+import com.sik.sikimage.ImageConvertUtils
+import com.sik.sikimage.ImageUtils
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import org.json.JSONObject
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import java.io.File
+import java.util.zip.CRC32
+import kotlin.math.log
+
+/**
+ * FaceUtil 人脸:兼容 HLK 模组;对外 API 不变
+ * - HLK 模式:检测/活体/识别走模组 NOTE/REPLY;APP 只负责预览与回调
+ * - ARC 模式:沿用原始 ArcSoft 流程
+ */
+object FaceUtil {
+    private val logger: Logger = LoggerFactory.getLogger(FaceUtil::class.java)
+
+    private enum class FaceBackend { ARC, HLK }
+
+    @Volatile
+    private var backend: FaceBackend = FaceBackend.ARC
+
+    @Volatile
+    private var hlkClient: Hlk223Client? = null
+
+    @Volatile
+    private var hlkVerifyJob: Job? = null
+
+    @JvmStatic
+    fun enableHlkBackend(client: Hlk223Client) {
+        hlkClient = client; backend = FaceBackend.HLK
+        checkLive()
+    }
+
+    private fun checkLive() {
+        if (hlkClient == null) return
+        hlkVerifyJob?.cancel()
+        hlkVerifyJob = ioScope.launch {
+            delay(200)
+            val version = hlkClient?.getVersion()
+            logger.info("hlk version: $version")
+        }
+    }
+
+    @JvmStatic
+    fun disableHlkBackend() {
+        backend = FaceBackend.ARC; hlkClient = null
+    }
+
+    // ArcSoft 授权配置(仅 ARC 用)
+    private const val CONFIG_DIR = "arcsoft_config"
+    private const val CONFIG_FILE = "ArcSoftConfig.json"
+    private const val OFFLINE_DAT = "OfflineActive.dat"
+
+    private data class ArcSoftLicenseConfig(
+        val appId: String, val sdkKey: String, val activeKey: String,
+        val activeOnline: Boolean, val activeOfflineFilePath: String
+    )
+
+    private fun readOrInitConfig(context: Context): ArcSoftLicenseConfig {
+        val dir = File("/sdcard/iscs/", CONFIG_DIR).apply { if (!exists()) mkdirs() }
+        val cfg = File(dir, CONFIG_FILE)
+        val offlineDat = File(dir, OFFLINE_DAT)
+        if (!offlineDat.exists()) runCatching { offlineDat.createNewFile() }
+        if (!cfg.exists()) {
+            val def = ArcSoftLicenseConfig(
+                appId = Constants.APP_ID,
+                sdkKey = Constants.SDK_KEY,
+                activeKey = Constants.ACTIVE_KEY,
+                activeOnline = false,
+                activeOfflineFilePath = offlineDat.absolutePath
+            )
+            cfg.writeText(JSONObject().apply {
+                put("appId", def.appId); put("sdkKey", def.sdkKey); put("activeKey", def.activeKey)
+                put("activeOnline", def.activeOnline); put(
+                "activeOfflineFilePath",
+                def.activeOfflineFilePath
+            )
+            }.toString())
+            return def
+        }
+        val json = runCatching { JSONObject(cfg.readText()) }.getOrElse { JSONObject() }
+        fun j(k: String, d: String) = json.optString(k, d).ifBlank { d }
+        return ArcSoftLicenseConfig(
+            appId = j("appId", Constants.APP_ID),
+            sdkKey = j("sdkKey", Constants.SDK_KEY),
+            activeKey = j("activeKey", Constants.ACTIVE_KEY),
+            activeOnline = json.optBoolean("activeOnline", false),
+            activeOfflineFilePath = j("activeOfflineFilePath", offlineDat.absolutePath)
+        )
+    }
+
+    // 旧字段(保持)
+    private var cameraHelper: CameraHelper? = null
+    private var previewSize: Camera.Size? = null
+    private var rgbCameraId: Int? = null
+    private var faceEngine: FaceEngine? = null
+    private val cameraWidth: Int = 640
+    private val cameraHeight: Int = 480
+    private var afCode = -1
+    private val processMask: Int = FaceEngine.ASF_MASK_DETECT or FaceEngine.ASF_LIVENESS
+    private val registerFaceFeatureJob get() = CoroutineScope(Dispatchers.IO + SupervisorJob())
+    private const val ACTION_REQUEST_PERMISSIONS: Int = 0x001
+    var isActivated = false
+
+    @Volatile
+    var inDetecting = false
+
+    // HLK NOTE 缓存
+    @Volatile
+    private var lastAliveByHlk: Boolean = true
+
+    @Volatile
+    private var lastFaceRectByHlk: Rect? = null
+
+    @Volatile
+    private var lastUserIdByHlk: Long? = null
+
+    @Volatile
+    private var hlkVerifyRunning = false
+    private val ioScope get() = CoroutineScope(Dispatchers.IO + SupervisorJob())
+
+    // 成员区
+    @Volatile
+    private var mlDetector: com.google.mlkit.vision.face.FaceDetector? = null
+
+    @Volatile
+    private var lastMlDetectTs = 0L
+
+    private var lastNoFaceTipTs = 0L
+    private fun maybeLogNoFaceTip() {
+        val now = System.currentTimeMillis()
+        if (now - lastNoFaceTipTs > 3000) {
+            logger.info("未检测到人脸,请靠近/居中/保证光照")
+            lastNoFaceTipTs = now
+        }
+    }
+
+    private fun findCameraIdByFacing(facing: Int): Int? {
+        val count = Camera.getNumberOfCameras()
+        val info = Camera.CameraInfo()
+        for (id in 0 until count) {
+            Camera.getCameraInfo(id, info)
+            if (info.facing == facing) return id
+        }
+        return null
+    }
+
+    private fun ensureMlDetector(): com.google.mlkit.vision.face.FaceDetector {
+        mlDetector?.let { return it }
+        val opts = FaceDetectorOptions.Builder()
+            .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
+            .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_NONE)
+            .setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_NONE) // 不用笑容/眨眼
+            .enableTracking()  // 拿 trackingId 可做稳定性
+            .build()
+        return com.google.mlkit.vision.face.FaceDetection.getClient(opts).also { mlDetector = it }
+    }
+
+
+    // ===== ARC 激活/初始化 =====
+    fun checkActiveStatus(context: Context) {
+        if (backend == FaceBackend.HLK) {
+            isActivated = true; return
+        }
+        val cfg = readOrInitConfig(context)
+        val code = try {
+            if (cfg.activeOnline) FaceEngine.activeOnline(
+                context,
+                cfg.activeKey,
+                cfg.appId,
+                cfg.sdkKey
+            )
+            else FaceEngine.activeOffline(context, cfg.activeOfflineFilePath)
+        } catch (e: Throwable) {
+            logger.error("激活异常: ${e.message}", e); ErrorInfo.MERR_UNKNOWN
+        }
+        when (code) {
+            ErrorInfo.MOK, ErrorInfo.MERR_ASF_ALREADY_ACTIVATED -> isActivated = true
+            else -> {
+                isActivated = false; logger.error("checkActiveStatus : active failed $code")
+            }
+        }
+        val afi = ActiveFileInfo()
+        if (FaceEngine.getActiveFileInfo(
+                context,
+                afi
+            ) == ErrorInfo.MOK
+        ) logger.info("getActiveFileInfo: $afi")
+    }
+
+    fun initEngine(context: Context) {
+        if (backend == FaceBackend.HLK) return
+        faceEngine = FaceEngine()
+        afCode = faceEngine!!.init(
+            context,
+            DetectMode.ASF_DETECT_MODE_VIDEO, DetectFaceOrientPriority.ASF_OP_0_ONLY, 1,
+            FaceEngine.ASF_FACE_DETECT or FaceEngine.ASF_MASK_DETECT or FaceEngine.ASF_LIVENESS or FaceEngine.ASF_FACE_RECOGNITION
+        )
+        logger.info("initEngine: $afCode")
+    }
+
+    fun unInitEngine() {
+        if (backend == FaceBackend.HLK) return
+        if (afCode == 0) {
+            afCode = faceEngine!!.unInit(); logger.info("unInitEngine: $afCode")
+        }
+    }
+
+    private fun mirrorRect(src: Rect, width: Int): Rect {
+        // ML Kit 坐标:以图像 buffer 为参考,左上为原点
+        // 镜像:x' = width - (x + w)
+        val left = width - (src.left + src.width())
+        val right = width - src.left
+        return Rect(left, src.top, right, src.bottom)
+    }
+
+    // ================= initCamera =================
+    @JvmOverloads
+    fun initCamera(
+        preview: View,
+        faceOverlayView: FaceOverlayView? = null,
+        needCheckCenter: Boolean = false,
+        callBack: (Bitmap?, Int, Boolean) -> Unit
+    ) {
+        if (rgbCameraId == null) {
+            rgbCameraId = findCameraIdByFacing(Camera.CameraInfo.CAMERA_FACING_FRONT)
+                ?: findCameraIdByFacing(Camera.CameraInfo.CAMERA_FACING_BACK)
+        }
+        val camId = rgbCameraId ?: run { logger.error("找不到可用相机ID"); return }
+
+        val listener = object : CameraListener {
+            override fun onCameraOpened(
+                camera: Camera,
+                cameraId: Int,
+                displayOrientation: Int,
+                isMirror: Boolean
+            ) {
+                previewSize = camera.parameters.previewSize
+                faceOverlayView?.setCameraPreviewSize(previewSize!!.width, previewSize!!.height)
+                if (backend == FaceBackend.HLK && !hlkVerifyRunning) {
+                    hlkVerifyRunning = true
+                    hlkVerifyJob?.cancel()
+                    hlkVerifyJob = ioScope.launch {
+                        ensureMlDetector()
+                        try {
+                            hlkClient?.startVerifyWithNotes(
+                                timeoutSec = 60, loop = true,
+                                onFaceState = { rect, state, yaw, pitch, roll ->
+                                    logger.info("onFaceState: $rect, $state, $yaw, $pitch, $roll")
+                                    lastFaceRectByHlk = rect
+                                    lastAliveByHlk = (state == 0)
+                                    faceOverlayView?.setFaceRect(rect?.let { listOf(it) }
+                                        ?: emptyList())
+                                },
+                                onLiveness = { alive -> lastAliveByHlk = alive },
+                                onResult = { userId -> lastUserIdByHlk = userId?.toLong() }
+                            )
+                        } finally {
+                            hlkVerifyRunning = false
+                        }
+                    }
+                }
+            }
+
+            override fun onPreview(nv21: ByteArray, camera: Camera?) {
+                val p = previewSize ?: return
+
+                if (backend == FaceBackend.HLK) {
+                    // HLK:持续回预览;人脸数量固定 1;活体来自 NOTE
+                    // —— ML Kit 节流:~120ms 一次 ——
+                    val now = System.currentTimeMillis()
+                    val doDetect = (now - lastMlDetectTs) > 120
+                    if (doDetect) {
+                        lastMlDetectTs = now
+                        // rotation:基于你已有的 DisplayUtils
+                        val rotation = DisplayUtils.getRotation(preview.context) // 0/90/180/270
+                        val imageData = ImageUtils.rotateNV21(nv21, p.width, p.height, 270)
+                        val image = InputImage.fromByteArray(
+                            imageData, p.width, p.height, rotation, ImageFormat.NV21
+                        )
+                        val bmp = ImageConvertUtils.nv21ToBitmap(imageData, p.width, p.height)
+                        ensureMlDetector().process(image)
+                            .addOnSuccessListener { faces ->
+                                // 镜像修正:相机是 isMirror(true) → X 轴翻转
+                                val rects = faces.map { f ->
+                                    val r = f.boundingBox
+                                    mirrorRect(r, p.width) // 见下方函数
+                                }
+                                faceOverlayView?.setFaceRect(rects)
+                                // 可做中心判定
+                                val inCenter =
+                                    rects.firstOrNull()?.isInCenterArea(p.width, p.height) ?: false
+                                if (!inCenter) maybeLogNoFaceTip()
+                                // 不在此处回调活体;活体交给 HLK NOTE
+                                // —— 回调:人脸数量来自 MLKit(最近一次),活体来自 HLK NOTE ——
+                                // 为了简单起见,这里用 overlay 里最新的 rect 数量(或你自己存储 lastFaceCountByMl)
+                                val faceCount = faceOverlayView?.lastRectsCount()
+                                    ?: (if (lastFaceRectByHlk != null) 1 else 0)
+
+                                // HLK 路径下仍然以 HLK 的活体为准
+                                callBack(bmp, faceCount, lastAliveByHlk)
+                            }
+                            .addOnFailureListener {
+                                // 静默或日志
+                                logger.warn("MLKit detect failed: ${it.message}")
+                            }
+                    }
+                    return
+                }
+
+                // ARC:原流程
+                if (inDetecting) return
+                inDetecting = true
+                val fe = faceEngine ?: run { inDetecting = false; return }
+                val faces = mutableListOf<FaceInfo>()
+                var code = fe.detectFaces(nv21, p.width, p.height, FaceEngine.CP_PAF_NV21, faces)
+                faceOverlayView?.setFaceRect(faces.map { it.rect })
+                if (code != ErrorInfo.MOK || faces.isEmpty()) {
+                    maybeLogNoFaceTip(); inDetecting = false; return
+                }
+                code =
+                    fe.process(nv21, p.width, p.height, FaceEngine.CP_PAF_NV21, faces, processMask)
+                if (code != ErrorInfo.MOK) {
+                    inDetecting = false; return
+                }
+                val liveList = mutableListOf<LivenessInfo>()
+                if (fe.getLiveness(liveList) != ErrorInfo.MOK) {
+                    inDetecting = false; return
+                }
+                if (liveList.none { it.liveness == LivenessInfo.ALIVE }) {
+                    callBack(null, faces.size, false); inDetecting = false; return
+                }
+                if (needCheckCenter && !faces[0].rect.isInCenterArea(p.width, p.height)) {
+                    inDetecting = false; return
+                }
+                val bmp = ImageConvertUtils.nv21ToBitmap(nv21, p.width, p.height)
+                callBack(bmp, faces.size, true)
+                inDetecting = false
+            }
+
+            override fun onCameraClosed() {}
+            override fun onCameraError(e: Exception) {
+                logger.info("onCameraError: ${e.message}")
+            }
+
+            override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) {}
+        }
+
+        val rotation = DisplayUtils.getRotation(preview.context)
+        cameraHelper = CameraHelper.Builder()
+            .previewViewSize(Point(cameraWidth, cameraHeight))
+            .rotation(rotation)
+            .specificCameraId(camId)
+            .isMirror(true)
+            .previewOn(preview)
+            .cameraListener(listener)
+            .build()
+        cameraHelper!!.init()
+        cameraHelper!!.start()
+    }
+
+    // ================= checkCamera =================
+    fun checkCamera(
+        preview: View,
+        callBack: (Bitmap?, Long?) -> Unit
+    ) {
+        if (rgbCameraId == null) {
+            rgbCameraId = findCameraIdByFacing(Camera.CameraInfo.CAMERA_FACING_FRONT)
+                ?: findCameraIdByFacing(Camera.CameraInfo.CAMERA_FACING_BACK)
+        }
+        val camId = rgbCameraId ?: run { logger.error("找不到可用相机ID"); return }
+
+        val listener = object : CameraListener {
+            override fun onCameraOpened(
+                camera: Camera,
+                cameraId: Int,
+                displayOrientation: Int,
+                isMirror: Boolean
+            ) {
+                previewSize = camera.parameters.previewSize
+                if (backend == FaceBackend.HLK && !hlkVerifyRunning) {
+                    hlkVerifyRunning = true
+                    hlkVerifyJob?.cancel()
+                    hlkVerifyJob = ioScope.launch {
+                        try {
+                            hlkClient?.startVerifyWithNotes(
+                                timeoutSec = 15, loop = false,
+                                onFaceState = { rect, state, yaw, pitch, roll ->
+                                    logger.info("onFaceState: $rect, $state, $yaw, $pitch, $roll")
+                                    lastFaceRectByHlk = rect; lastAliveByHlk = (state == 0)
+                                },
+                                onLiveness = { alive -> lastAliveByHlk = alive },
+                                onResult = { userId -> lastUserIdByHlk = userId?.toLong() }
+                            )
+                        } finally {
+                            hlkVerifyRunning = false
+                        }
+                    }
+                }
+            }
+
+            override fun onPreview(nv21: ByteArray?, camera: Camera?) {
+                val p = previewSize ?: return
+                val bmp = ImageConvertUtils.nv21ToBitmap(nv21, p.width, p.height)
+
+                if (backend == FaceBackend.HLK) {
+                    callBack(bmp, lastUserIdByHlk)
+                    return
+                }
+
+                // ARC:原流程
+                if (inDetecting) return
+                inDetecting = true
+                val fe = faceEngine ?: run { inDetecting = false; return }
+                val faces = mutableListOf<FaceInfo>()
+                var code = fe.detectFaces(nv21, p.width, p.height, FaceEngine.CP_PAF_NV21, faces)
+                if (code != ErrorInfo.MOK || faces.isEmpty()) {
+                    inDetecting = false; return
+                }
+                code =
+                    fe.process(nv21, p.width, p.height, FaceEngine.CP_PAF_NV21, faces, processMask)
+                if (code != ErrorInfo.MOK) {
+                    inDetecting = false; return
+                }
+                val liveList = mutableListOf<LivenessInfo>()
+                val lc = fe.getLiveness(liveList)
+                if (lc != ErrorInfo.MOK) {
+                    inDetecting = false; return
+                }
+                if (liveList.none { it.liveness == LivenessInfo.ALIVE }) {
+                    callBack(null, null); inDetecting = false; return
+                }
+                val ft = FaceFeature()
+                fe.extractFaceFeature(
+                    nv21,
+                    p.width,
+                    p.height,
+                    FaceEngine.CP_PAF_NV21,
+                    faces[0],
+                    ExtractType.RECOGNIZE,
+                    0,
+                    ft
+                )
+                val searchResult =
+                    runCatching { if (fe.faceCount > 0) fe.searchFaceFeature(ft) else null }.getOrNull()
+                callBack(bmp, searchResult?.faceFeatureInfo?.searchId?.toLong())
+                inDetecting = false
+            }
+
+            override fun onCameraClosed() {}
+            override fun onCameraError(e: Exception) {
+                logger.info("onCameraError: ${e.message}")
+            }
+
+            override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) {}
+        }
+
+        val rotation = DisplayUtils.getRotation(preview.context)
+        cameraHelper = CameraHelper.Builder()
+            .previewViewSize(Point(cameraWidth, cameraHeight))
+            .rotation(rotation)
+            .specificCameraId(camId)
+            .isMirror(true)
+            .previewOn(preview)
+            .cameraListener(listener)
+            .build()
+        cameraHelper!!.init()
+        cameraHelper!!.start()
+    }
+
+    fun stop() {
+        cameraHelper?.release()
+        cameraHelper = null
+        hlkVerifyJob?.cancel()
+        hlkVerifyJob = null
+        hlkClient?.stopVerify() // 双保险:让设备侧监听循环马上跳出
+    }
+
+    // ----------------- 注册/比对(仅 ARC 有效) -----------------
+    fun registerFace(faceData: List<Pair<Long, String>>) {
+        if (backend == FaceBackend.HLK) {
+            // HLK 下请走 Hlk223PhotoEnroll / ENROLL_ITG
+
+            faceData.forEach { (uid, b64) ->
+                ThreadUtils.runOnIO {
+                    val jpegBytes = ImageCompress.base64ToJpegUnder(b64, 1024 * 6)
+                    val crc32 = CRC32().apply { update(jpegBytes) }.value.toInt()
+                    logger.info("图片大小:{}", jpegBytes.size)
+                    val userId = hlkClient?.enrollWithPhoto(jpegBytes, crc32 = crc32)
+                    logger.info("注册Id:{}", userId)
+                }
+            }
+            return
+        }
+        faceEngine?.removeFaceFeature(-1)
+        faceData.forEach { (uid, b64) ->
+            val bmp = decodeBase64ToBitmap(b64)
+            val img = bitmapToBgr24(bmp)
+            val faces = mutableListOf<FaceInfo>()
+            val code =
+                faceEngine?.detectFaces(img, bmp.width, bmp.height, FaceEngine.CP_PAF_BGR24, faces)
+            if (faces.isNullOrEmpty()) return@forEach
+            val faceFeature = FaceFeature()
+            faceEngine?.extractFaceFeature(
+                img,
+                bmp.width,
+                bmp.height,
+                FaceEngine.CP_PAF_BGR24,
+                faces[0],
+                ExtractType.REGISTER,
+                0,
+                faceFeature
+            )
+            val info = FaceFeatureInfo(uid.toInt(), faceFeature.featureData)
+            try {
+                if ((faceEngine?.searchFaceFeature(faceFeature)?.maxSimilar ?: 0f) > 0.5) {
+                    faceEngine?.updateFaceFeature(info)
+                } else {
+                    faceEngine?.registerFaceFeature(info)
+                }
+            } catch (_: Exception) {
+                faceEngine?.registerFaceFeature(info)
+            }
+        }
+    }
+
+    fun verifyFaceArcSoft(b64a: String, b64b: String, threshold: Float = 0.7f): Boolean {
+        if (backend == FaceBackend.HLK) return false
+        if (b64a.isEmpty() || b64b.isEmpty()) return false
+        val bmpA = decodeBase64ToBitmap(b64a)
+        val bmpB = decodeBase64ToBitmap(b64b)
+        val facesA = mutableListOf<FaceInfo>()
+        val facesB = mutableListOf<FaceInfo>()
+        val imgA = bitmapToBgr24(bmpA)
+        val imgB = bitmapToBgr24(bmpB)
+        val codeA =
+            faceEngine?.detectFaces(imgA, bmpA.width, bmpA.height, FaceEngine.CP_PAF_BGR24, facesA)
+        val codeB =
+            faceEngine?.detectFaces(imgB, bmpB.width, bmpB.height, FaceEngine.CP_PAF_BGR24, facesB)
+        if (codeA != ErrorInfo.MOK || codeB != ErrorInfo.MOK || facesA.isEmpty() || facesB.isEmpty()) return false
+        val ftA = FaceFeature();
+        val ftB = FaceFeature()
+        faceEngine?.extractFaceFeature(
+            imgA,
+            bmpA.width,
+            bmpA.height,
+            FaceEngine.CP_PAF_BGR24,
+            facesA[0],
+            ExtractType.RECOGNIZE,
+            0,
+            ftA
+        )
+        faceEngine?.extractFaceFeature(
+            imgB,
+            bmpB.width,
+            bmpB.height,
+            FaceEngine.CP_PAF_BGR24,
+            facesB[0],
+            ExtractType.RECOGNIZE,
+            0,
+            ftB
+        )
+        val sim = FaceSimilar()
+        if (faceEngine?.compareFaceFeature(ftA, ftB, sim) != ErrorInfo.MOK) return false
+        return sim.score >= threshold
+    }
+
+    // ===== 工具 =====
+    private fun decodeBase64ToBitmap(b64: String): Bitmap {
+        val bytes = Base64.decode(b64, Base64.DEFAULT)
+        return BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
+    }
+
+    private fun bitmapToBgr24(bitmap: Bitmap): ByteArray {
+        val w = bitmap.width;
+        val h = bitmap.height
+        val pixels = IntArray(w * h); bitmap.getPixels(pixels, 0, w, 0, 0, w, h)
+        val bgr = ByteArray(w * h * 3)
+        var i = 0
+        for (p in pixels) {
+            bgr[i++] = (p and 0xFF).toByte()
+            bgr[i++] = ((p shr 8) and 0xFF).toByte()
+            bgr[i++] = ((p shr 16) and 0xFF).toByte()
+        }
+        return bgr
+    }
+}

+ 237 - 115
data/src/main/java/com/grkj/data/hardware/face/hlk/Hlk223Client.kt

@@ -1,38 +1,60 @@
 package com.grkj.data.hardware.face.hlk
 
+import android.graphics.Rect
+import com.grkj.shared.utils.extension.toHexStrings
+import com.sik.comm.core.model.ChainStepResult
 import com.sik.comm.core.model.CommMessage
+import com.sik.comm.core.model.TxPlan
+import com.sik.comm.core.policy.ChainPolicy
+import com.sik.comm.core.protocol.LinkIO
 import com.sik.comm.impl_modbus.ModbusProtocol
-import kotlin.math.min
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.isActive
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import java.util.concurrent.TimeoutException
+import java.util.concurrent.atomic.AtomicBoolean
 
 /**
- * HLK-FM223 常用命令的“人话”封装:单帧往返 + 少量多帧场景入口。
- * 传输层复用 ModbusProtocol + PassThroughCodec(直通)。
+ * HLK-FM223 命令封装:单帧往返 + VERIFY/NOTE 持续监听(兼容粘包/半包)
  */
 class Hlk223Client(
     private val protocol: ModbusProtocol,
     private val deviceId: String
 ) {
+    private val logger: Logger = LoggerFactory.getLogger(Hlk223Client::class.java)
 
-    /** 基础收发:req -> rsp(payload 为完整 HLK 帧),再本地解析 */
-    private suspend fun exchange(mid: Int, data: ByteArray = byteArrayOf(), timeoutMs: Int? = 3000): Pair<Int, ByteArray> {
+    // ========= 基础收发(单帧请求-响应) =========
+    private suspend fun exchange(
+        mid: Int,
+        data: ByteArray = byteArrayOf(),
+        timeoutMs: Int? = 3000
+    ): Pair<Int, ByteArray> {
         val req: CommMessage = Hlk223.msg(mid, data, timeoutMs)
+        logger.info("请求:${req.payload.toHexStrings()}")
         val rsp: CommMessage = protocol.send(deviceId, req)
-        return Hlk223.parse(rsp.payload)
+        logger.info("返回:${rsp.payload.toHexStrings()}")
+        return Hlk223.parseOne(rsp.payload)
     }
 
-    // --- 基础信息 ---
+    private fun u8(b: Byte) = b.toInt() and 0xFF
+    private fun s16(hi: Int, lo: Int): Int = ((hi shl 8) or lo).toShort().toInt()
 
-    suspend fun reset() {
+    // ========= 设备信息/控制(同前) =========
+    suspend fun reset(): Boolean {
         val (mid, data) = exchange(Hlk223.MID.RESET)
-        require(mid == Hlk223.MID.REPLY && data.size >= 2)
-        val result = data[1].toInt() and 0xFF
-        require(result == 0) { "RESET failed, result=$result" }
+        return mid == Hlk223.MID.REPLY && data.size >= 2 && (u8(data[1]) == 0)
+    }
+
+    suspend fun faceReset(): Boolean {
+        val (mid, data) = exchange(Hlk223.MID.FACE_RESET)
+        return mid == Hlk223.MID.REPLY && data.size >= 2 && (u8(data[1]) == 0)
     }
 
     suspend fun getStatus(): Int {
         val (mid, data) = exchange(Hlk223.MID.GET_STATUS)
         require(mid == Hlk223.MID.REPLY && data.size >= 3)
-        return data[2].toInt() and 0xFF // IDLE/BUSY/ERROR/...
+        return u8(data[2])
     }
 
     suspend fun getVersion(): String {
@@ -47,126 +69,226 @@ class Hlk223Client(
         return data.copyOfRange(2, data.size).toString(Charsets.US_ASCII).trim('\u0000')
     }
 
-    // --- 用户管理 ---
+    suspend fun readUvcParam(): ByteArray {
+        val (mid, data) = exchange(Hlk223.MID.READ_USB_UVC_PARAM, byteArrayOf(), timeoutMs = 2000)
+        require(mid == Hlk223.MID.REPLY && data.size >= 2)
+        return data.copyOfRange(2, data.size)
+    }
 
-    suspend fun delUser(userId: Int) {
-        val data = byteArrayOf(((userId ushr 8) and 0xFF).toByte(), (userId and 0xFF).toByte())
-        val (mid, rsp) = exchange(Hlk223.MID.DEL_USER, data)
-        require(mid == Hlk223.MID.REPLY && rsp.size >= 2)
-        require((rsp[1].toInt() and 0xFF) == 0) { "DEL_USER failed" }
+    suspend fun setUvcParam(param: ByteArray): Boolean {
+        val (mid, data) = exchange(Hlk223.MID.SET_USB_UVC_PARAM, param, timeoutMs = 2000)
+        return mid == Hlk223.MID.REPLY && data.size >= 2 && (u8(data[1]) == 0)
     }
 
-    suspend fun delAll() {
-        val (mid, rsp) = exchange(Hlk223.MID.DEL_ALL)
-        require(mid == Hlk223.MID.REPLY && rsp.size >= 2)
-        require((rsp[1].toInt() and 0xFF) == 0) { "DEL_ALL failed" }
+    suspend fun setDemoMode(mode: Int): Boolean {
+        val (mid, data) = exchange(Hlk223.MID.DEMO_MODE, byteArrayOf((mode and 0xFF).toByte()))
+        return mid == Hlk223.MID.REPLY && data.size >= 2 && (u8(data[1]) == 0)
     }
 
-    data class UserInfo(val userId: Int, val isAdmin: Boolean, val name: String)
+    // ========= 用户管理(同前) =========
+    data class HlkUserInfo(val userId: Int, val isAdmin: Boolean, val name: String?)
+
+    suspend fun deleteUser(userId: Int): Boolean {
+        val d = byteArrayOf(((userId shr 8) and 0xFF).toByte(), (userId and 0xFF).toByte())
+        val (mid, payload) = exchange(Hlk223.MID.DEL_USER, d, timeoutMs = 3000)
+        return mid == Hlk223.MID.REPLY && payload.size >= 2 && (u8(payload[1]) == 0)
+    }
 
-    suspend fun getUserInfo(userId: Int): UserInfo {
-        val data = byteArrayOf(((userId ushr 8) and 0xFF).toByte(), (userId and 0xFF).toByte())
-        val (mid, rsp) = exchange(Hlk223.MID.GET_USER_INFO, data)
-        require(mid == Hlk223.MID.REPLY && rsp.size >= 2)
-        require((rsp[1].toInt() and 0xFF) == 0) { "GET_USER_INFO failed" }
-        // 具体字段偏移视固件版本略有差异,下方对齐常见布局:flags(1) + name(N)...
-        val flags = if (rsp.size > 2) (rsp[2].toInt() and 0xFF) else 0
-        val isAdmin = (flags and 0x01) == 1
-        val name = if (rsp.size > 3) rsp.copyOfRange(3, rsp.size).toString(Charsets.UTF_8).trim('\u0000') else ""
-        return UserInfo(userId, isAdmin, name)
+    suspend fun deleteAllUsers(): Boolean {
+        val (mid, payload) = exchange(Hlk223.MID.DEL_ALL, byteArrayOf(), timeoutMs = 6000)
+        return mid == Hlk223.MID.REPLY && payload.size >= 2 && (u8(payload[1]) == 0)
     }
 
-    suspend fun getAllUserId(max: Int = 256): List<Int> {
-        val (mid, rsp) = exchange(Hlk223.MID.GET_ALL_USERID)
-        require(mid == Hlk223.MID.REPLY && rsp.size >= 2)
-        require((rsp[1].toInt() and 0xFF) == 0) { "GET_ALL_USERID failed" }
-        // 紧随其后的为一串 userId(2B,BE);不同版本有 NOTE/分页,这里做最小实现:一次吃完
-        val list = mutableListOf<Int>()
+    suspend fun getAllUserIds(): IntArray {
+        val (mid, payload) = exchange(Hlk223.MID.GET_ALL_USERID, byteArrayOf(), timeoutMs = 5000)
+        if (mid != Hlk223.MID.REPLY || payload.size < 2) return intArrayOf()
+        val ids = ArrayList<Int>()
         var i = 2
-        while (i + 1 < rsp.size && list.size < max) {
-            val id = ((rsp[i].toInt() and 0xFF) shl 8) or (rsp[i + 1].toInt() and 0xFF)
-            list += id
-            i += 2
+        while (i + 1 < payload.size) {
+            ids += ((u8(payload[i]) shl 8) or u8(payload[i + 1])); i += 2
         }
-        return list
-    }
-
-    // --- 录入/验证 ---
-
-    /** 交互式录入(五次抬头转头之类),按需监听 NOTE;这里给最小版:触发并等待 REPLY 结束码 */
-    suspend fun enrollInteractive(nameUtf8: String, admin: Boolean = false, timeoutMs: Int = 30_000): Int {
-        val nameBytes = nameUtf8.toByteArray(Charsets.UTF_8)
-        val flag = if (admin) 0x01 else 0x00
-        val data = byteArrayOf(flag.toByte()) + nameBytes
-        val (mid, rsp) = exchange(Hlk223.MID.ENROLL, data, timeoutMs)
-        require(mid == Hlk223.MID.REPLY && rsp.size >= 2)
-        require((rsp[1].toInt() and 0xFF) == 0) { "ENROLL failed, code=${rsp[1].toInt() and 0xFF}" }
-        // 常见布局:尾部2字节 userId(BE)
-        val uid = if (rsp.size >= 4) (((rsp[rsp.size - 2].toInt() and 0xFF) shl 8) or (rsp.last().toInt() and 0xFF)) else 0
-        return uid
-    }
-
-    /** 单帧录入(只需一张正脸) */
-    suspend fun enrollSingle(nameUtf8: String, admin: Boolean = false, timeoutMs: Int = 10_000): Int {
-        val nameBytes = nameUtf8.toByteArray(Charsets.UTF_8)
-        val flag = if (admin) 0x01 else 0x00
-        val data = byteArrayOf(flag.toByte()) + nameBytes
-        val (mid, rsp) = exchange(Hlk223.MID.ENROLL_SINGLE, data, timeoutMs)
-        require(mid == Hlk223.MID.REPLY && rsp.size >= 2)
-        require((rsp[1].toInt() and 0xFF) == 0) { "ENROLL_SINGLE failed, code=${rsp[1].toInt() and 0xFF}" }
-        val uid = if (rsp.size >= 4) (((rsp[rsp.size - 2].toInt() and 0xFF) shl 8) or (rsp.last().toInt() and 0xFF)) else 0
-        return uid
-    }
-
-    /** 触发设备端验证(摄像头采集→库内比对),返回是否命中与命中ID(未命中为0) */
-    data class VerifyResult(val success: Boolean, val userId: Int)
-
-    suspend fun verify(timeoutMs: Int = 10_000): VerifyResult {
-        val (mid, rsp) = exchange(Hlk223.MID.VERIFY, byteArrayOf(), timeoutMs)
-        require(mid == Hlk223.MID.REPLY && rsp.size >= 2)
-        val ok = (rsp[1].toInt() and 0xFF) == 0
-        val uid = if (rsp.size >= 4) (((rsp[rsp.size - 2].toInt() and 0xFF) shl 8) or (rsp.last().toInt() and 0xFF)) else 0
-        return VerifyResult(ok && uid > 0, uid)
-    }
-
-    // --- 照片/特征下发注册 ---
-
-    /** 计算 CRC32(标准,多项式 0xEDB88320,初始化 0xFFFFFFFF,输出与 HLK 文档一致,写入大端) */
-    private fun crc32(bytes: ByteArray): Int {
-        var crc = -1 // 0xFFFFFFFF
-        for (b in bytes) {
-            var c = (crc xor (b.toInt() and 0xFF)) and 0xFFFFFFFF.toInt()
-            repeat(8) {
-                val mask = -(c and 1)
-                c = (c ushr 1) xor (0xEDB88320.toInt() and mask)
+        return ids.toIntArray()
+    }
+
+    suspend fun getUserInfo(userId: Int): HlkUserInfo? {
+        val d = byteArrayOf(((userId shr 8) and 0xFF).toByte(), (userId and 0xFF).toByte())
+        val (mid, payload) = exchange(Hlk223.MID.GET_USER_INFO, d, timeoutMs = 3000)
+        if (mid != Hlk223.MID.REPLY || payload.size < 3) return null
+        val id = (u8(payload[0]) shl 8) or u8(payload[1])
+        val admin = (u8(payload[2]) and 0x01) == 1
+        val name = if (payload.size > 3)
+            payload.copyOfRange(3, payload.size).toString(Charsets.US_ASCII).trimEnd('\u0000')
+                .ifBlank { null }
+        else null
+        return HlkUserInfo(id, admin, name)
+    }
+
+    // ========= VERIFY + NOTE 监听(使用 Framer 处理粘包/半包) =========
+    private companion object {
+        private const val NID_FACE_STATE = 0x01
+        private const val NID_LIVENESS = 0x05
+    }
+
+    private suspend fun runVerifyOnce(
+        timeoutSec: Int,
+        onFaceState: (rect: Rect?, state: Int, yaw: Int, pitch: Int, roll: Int) -> Unit,
+        onLiveness: (alive: Boolean) -> Unit,
+        onResult: (userId: Int?) -> Unit
+    ): Int? {
+        val singleReadTimeoutMs = 3000
+        val silenceGapMs = 50
+        val framer = Hlk223.Framer()
+
+        val verifyData = byteArrayOf(0x00, (timeoutSec and 0xFF).toByte())
+        val plan = TxPlan(
+            frames = listOf(
+                Hlk223.msg(
+                    Hlk223.MID.VERIFY,
+                    verifyData,
+                    timeoutMs = (timeoutSec + 2) * 1000,
+                    gapMs = silenceGapMs
+                )
+            )
+        )
+        var userIdHit: Int? = null
+
+        val policy: ChainPolicy = object : ChainPolicy {
+            override suspend fun afterSendStep(
+                stepIndex: Int,
+                sent: CommMessage,
+                io: LinkIO
+            ): ChainStepResult {
+                val deadline = System.currentTimeMillis() + (timeoutSec + 2) * 1000L
+                while (System.currentTimeMillis() < deadline &&
+                    !verifyStop.get() &&
+                    currentCoroutineContext().isActive) {
+                    val ack: CommMessage = try {
+                        io.readRaw(
+                            timeoutMs = singleReadTimeoutMs,
+                            expectedSize = null,
+                            silenceGapMs = silenceGapMs
+                        )
+                    } catch (_: TimeoutException) {
+                        continue // 没数据,继续等
+                    } catch (_: Throwable) {
+                        continue // 忽略异常,继续等
+                    }
+
+                    // 可能是多帧/半帧:增量解包
+                    val frames = try {
+                        framer.feed(ack.payload)
+                    } catch (_: Throwable) {
+                        emptyList()
+                    }
+                    if (frames.isEmpty()) continue
+
+                    for (f in frames) {
+                        val (mid, data) = try {
+                            Hlk223.parseOne(f)
+                        } catch (_: Throwable) {
+                            continue
+                        }
+                        when (mid) {
+                            Hlk223.MID.NOTE -> {
+                                val nid = data.getOrNull(0)?.let { u8(it) } ?: continue
+                                when (nid) {
+                                    NID_FACE_STATE -> if (data.size >= 1 + 16) {
+                                        fun s(i: Int) = s16(u8(data[i + 1]), u8(data[i]))
+                                        val st = s(1)
+                                        val left = s(3);
+                                        val top = s(5);
+                                        val right = s(7);
+                                        val bottom = s(9)
+                                        val yaw = s(11);
+                                        val pitch = s(13);
+                                        val roll = s(15)
+                                        val rect = if (right > left && bottom > top) Rect(
+                                            left,
+                                            top,
+                                            right,
+                                            bottom
+                                        ) else null
+                                        onFaceState(rect, st, yaw, pitch, roll)
+                                    }
+
+                                    NID_LIVENESS -> {
+                                        val alive = data.getOrNull(1)?.let { u8(it) == 1 } ?: false
+                                        onLiveness(alive)
+                                    }
+                                }
+                            }
+
+                            Hlk223.MID.REPLY -> {
+                                userIdHit =
+                                    if (data.size >= 2) ((u8(data[0]) shl 8) or u8(data[1])) else null
+                                onResult(userIdHit)
+                                return ChainStepResult(
+                                    received = listOf(ack),
+                                    continueNext = false,
+                                    interFrameDelayMs = 0
+                                )
+                            }
+
+                            else -> { /* IMAGE/其它忽略 */
+                            }
+                        }
+                    }
+                }
+                // 循环结束前复位标志(避免下次误停)
+                verifyStop.set(false)
+                onResult(userIdHit)
+                return ChainStepResult(
+                    received = emptyList(),
+                    continueNext = false,
+                    interFrameDelayMs = 0
+                )
             }
-            crc = c
         }
-        return crc
+
+        protocol.sendChain(deviceId, plan, policy)
+        return userIdHit
     }
 
     /**
-     * 通过照片/特征(BioType=0/1/2)执行 ENROLL_WITH_PHOTO 流。
-     * - BioType=2:payload 必须是 2052B(前 2048 特征 + 后 4B CRC32 大端);如果传入 2048B,会自动拼 CRC。
-     * - 返回 userId(>0)
+     * 开启验证并消费 NOTE。
+     * - loop=true:REPLY/超时后自动再开一轮。
      */
-    suspend fun enrollWithPhotoOrFeature(payload: ByteArray, bioType: Int): Int {
-        val tx = Hlk223PhotoEnroll(protocol, deviceId)
-        val data = if (bioType == 2 && payload.size == 2048) {
-            val crc = crc32(payload)
-            payload + byteArrayOf(
-                ((crc ushr 24) and 0xFF).toByte(),
-                ((crc ushr 16) and 0xFF).toByte(),
-                ((crc ushr 8) and 0xFF).toByte(),
-                (crc and 0xFF).toByte()
-            )
-        } else payload
-        return tx.enroll(data, bioType = bioType, crc32 = if (bioType == 2) 0 /*占位,首包仍写真实CRC*/ else crc32(data))
+    suspend fun startVerifyWithNotes(
+        timeoutSec: Int = 60,
+        loop: Boolean = true,
+        onFaceState: (rect: Rect?, state: Int, yaw: Int, pitch: Int, roll: Int) -> Unit,
+        onLiveness: (alive: Boolean) -> Unit = {},
+        onResult: (userId: Int?) -> Unit
+    ) {
+        do {
+            if (verifyStop.get() || !currentCoroutineContext().isActive) break
+            runVerifyOnce(timeoutSec, onFaceState, onLiveness, onResult)
+            if (verifyStop.get() || !currentCoroutineContext().isActive) break
+            kotlinx.coroutines.delay(30) // 轻微冷却
+        } while (loop)
+    }
+
+
+    private val verifyStop = AtomicBoolean(false)
+
+    /** 外部调用,停止当前/下一轮 VERIFY NOTE 监听 */
+    fun stopVerify() {
+        verifyStop.set(true)
+    }
+
+    // ========= 照片下发注册 =========
+    suspend fun enrollWithPhoto(jpg: ByteArray, bioType: Int = 1, crc32: Int): Int {
+        val helper = Hlk223PhotoEnroll(protocol, deviceId)
+        return helper.enroll(jpg, bioType, crc32)
     }
 
-    /** 语法糖:直接下发 2048B 特征(自动拼 2052B + BioType=2) */
-    suspend fun enrollFeature(feature2048: ByteArray): Int {
-        require(feature2048.size == 2048) { "feature size must be 2048 bytes" }
-        return enrollWithPhotoOrFeature(feature2048, bioType = 2)
+    suspend fun enrollIntegrated(timeoutSec: Int = 60): Boolean {
+        val payload = byteArrayOf((timeoutSec and 0xFF).toByte())
+        val (mid, data) = exchange(
+            Hlk223.MID.ENROLL_ITG,
+            payload,
+            timeoutMs = (timeoutSec + 2) * 1000
+        )
+        return mid == Hlk223.MID.REPLY && data.size >= 2 && (u8(data[1]) == 0)
     }
 }

+ 1 - 0
data/src/main/java/com/grkj/data/hardware/face/hlk/Hlk223Config.kt

@@ -26,6 +26,7 @@ object Hlk223Config {
         )
         val protocol = ModbusProtocol()
         protocol.registerConfig(modbusConfig)
+        protocol.connect("HLK-223")
         return protocol
     }
 }

+ 51 - 4
data/src/main/java/com/grkj/data/hardware/face/hlk/Hlk223Frames.kt

@@ -3,7 +3,7 @@ package com.grkj.data.hardware.face.hlk
 import com.sik.comm.core.model.CommMessage
 
 /**
- * HLK-FM223 帧工具(组帧/验帧/转 CommMessage)。
+ * HLK-FM223 帧工具(组帧/验帧/转 CommMessage + 增量式解包)。
  * 帧: [EF AA][MID][LEN_H][LEN_L][DATA...][XOR]
  * XOR = 从 MID 到 DATA 末字节的逐字节异或。
  */
@@ -31,7 +31,7 @@ object Hlk223 {
         const val ENROLL_WITH_PHOTO  = 0xF7
         const val DEMO_MODE          = 0xFE
 
-        /** 下行常见应答/上报(模组→主机) */
+        /** 下行:模组→主机 */
         const val REPLY              = 0x00
         const val NOTE               = 0x01
         const val IMAGE              = 0x02
@@ -48,8 +48,8 @@ object Hlk223 {
         return body + parity
     }
 
-    /** 解析 & 校验:返回 (mid, data) */
-    fun parse(frame: ByteArray): Pair<Int, ByteArray> {
+    /** 单帧解析(严格):frame 必须正好是一帧;返回 (mid, data) */
+    fun parseOne(frame: ByteArray): Pair<Int, ByteArray> {
         require(frame.size >= 6) { "Frame too short" }
         require(frame[0] == SYNC_H && frame[1] == SYNC_L) { "Bad sync" }
         val mid  = frame[2].toInt() and 0xFF
@@ -61,6 +61,9 @@ object Hlk223 {
         return mid to frame.copyOfRange(5, 5 + size)
     }
 
+    /** 兼容旧名:但注意它假设传入就是“单帧”;多帧/半帧请用 Framer */
+    fun parse(frame: ByteArray): Pair<Int, ByteArray> = parseOne(frame)
+
     /** 包装为 CommMessage(交给协议层发送;超时/静默间隔可透传 metadata) */
     fun msg(mid: Int, data: ByteArray = byteArrayOf(), timeoutMs: Int? = null, gapMs: Int? = null): CommMessage {
         val frame = build(mid, data)
@@ -75,4 +78,48 @@ object Hlk223 {
             metadata = meta
         )
     }
+
+    /**
+     * 增量式解包器:可处理多帧粘连 / 半帧残留 / 噪声跳过。
+     * 使用:每次把串口收到的 chunk 喂给 feed(),它会吐出“尽可能多的完整帧(含 EF AA 头)”。
+     */
+    class Framer {
+        private var buf = ByteArray(0)
+
+        fun feed(chunk: ByteArray): List<ByteArray> {
+            if (chunk.isEmpty()) return emptyList()
+            buf += chunk
+            val out = ArrayList<ByteArray>()
+            var i = 0
+            while (true) {
+                // 同步字对齐
+                while (i + 2 <= buf.size && (buf[i] != SYNC_H || buf[i + 1] != SYNC_L)) i++
+                if (i + 5 >= buf.size) break // 不够读 mid+len
+
+                val mid  = buf[i + 2].toInt() and 0xFF
+                val size = ((buf[i + 3].toInt() and 0xFF) shl 8) or (buf[i + 4].toInt() and 0xFF)
+                if (size < 0 || size > 0x1_0000) { i += 2; continue } // 防御
+
+                val frameLen = 5 + size + 1
+                if (i + frameLen > buf.size) break // 半帧,等下次
+
+                val frame = buf.copyOfRange(i, i + frameLen)
+
+                // 校验 XOR
+                val calc = frame.copyOf(frame.size - 1).drop(2).fold(0) { acc, b -> acc xor (b.toInt() and 0xFF) } and 0xFF
+                val got  = frame.last().toInt() and 0xFF
+                if (calc == got) {
+                    out += frame
+                }
+                // 无论校验是否通过,都推进 i 避免死循环;不通过等于噪声/坏帧,直接跳过
+                i += frameLen
+            }
+            // 仅保留尾部残包
+            buf = if (i >= buf.size) ByteArray(0) else buf.copyOfRange(i, buf.size)
+            return out
+        }
+
+        /** 清空缓冲 */
+        fun clear() { buf = ByteArray(0) }
+    }
 }

+ 1 - 1
data/src/main/java/com/grkj/data/hardware/face/hlk/Hlk223PhotoEnroll.kt

@@ -71,7 +71,7 @@ class Hlk223PhotoEnroll(
     }
 
     /** 执行注册,返回 userId(末包 REPLY 通常携带) */
-    suspend fun enroll(payload: ByteArray, bioType: Int = 1, crc32: Int): Int {
+    suspend fun enroll(payload: ByteArray, bioType: Int = 0, crc32: Int): Int {
         val rsps = protocol.sendChain(deviceId, buildPlan(payload, bioType, crc32), policy())
         val last = rsps.lastOrNull() ?: error("No reply")
         val (_, data) = Hlk223.parse(last.payload)

+ 1 - 1
gradle/libs.versions.toml

@@ -24,7 +24,7 @@ ksp = "2.1.10-1.0.31"
 nav_version = "2.9.0"
 kotlin_serialization_json = "1.7.3"
 fastble = "1.4.2"
-sikcomm = "1.0.17"
+sikcomm = "1.0.18"
 
 [libraries]
 androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }

+ 4 - 4
iscs_lock/src/main/AndroidManifest.xml

@@ -76,10 +76,10 @@
                 <action android:name="android.media.AUDIO_BECOMEING_NOISY" />
             </intent-filter>
         </receiver>
-        <service
-            android:name="com.sik.cronjob.services.TaskService"
-            android:exported="false"
-            android:process=":CronJobService" />
+<!--        <service-->
+<!--            android:name="com.sik.cronjob.services.TaskService"-->
+<!--            android:exported="false"-->
+<!--            android:process=":CronJobService" />-->
 
         <meta-data
             android:name="design_width_in_dp"

+ 5 - 5
iscs_lock/src/main/assets/preset/CN/preset_workflow_step.json

@@ -412,7 +412,7 @@
     "stepTitleShort": "解锁",
     "stepIcon": "unlock.svg",
     "stepDescription": "解锁",
-    "confirmType": 0,
+    "confirmType": 1,
     "confirmRoleCode": null,
     "confirmUser": null,
     "enableCancelJob": false,
@@ -443,7 +443,7 @@
     "stepTitleShort": "识别工作内容",
     "stepIcon": "ballot-check.svg",
     "stepDescription": "识别工作内容",
-    "confirmType": 0,
+    "confirmType": 1,
     "confirmRoleCode": null,
     "confirmUser": null,
     "enableCancelJob": true,
@@ -471,10 +471,10 @@
     "stepIndex": 2,
     "stepName": "判断能量源与隔离方式",
     "stepTitle": "判断能量源与隔离方式",
-    "stepTitleShort": "判断能量源与隔离方式",
+    "stepTitleShort": "能量隔离证实",
     "stepIcon": "bolt.svg",
     "stepDescription": "判断能量源与隔离方式",
-    "confirmType": 0,
+    "confirmType": 1,
     "confirmRoleCode": null,
     "confirmUser": null,
     "enableCancelJob": true,
@@ -502,7 +502,7 @@
     "stepIndex": 3,
     "stepName": "通知所有受影响的人员",
     "stepTitle": "通知所有受影响的人员",
-    "stepTitleShort": "通知所有受影响的人员",
+    "stepTitleShort": "通知人员",
     "stepIcon": "users-alt.svg",
     "stepDescription": "通知所有受影响的人员",
     "confirmType": 0,

+ 40 - 7
iscs_lock/src/main/java/com/grkj/iscs/ISCSApplication.kt

@@ -1,7 +1,11 @@
 package com.grkj.iscs
 
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.app.ActivityManager
 import android.app.AlarmManager
 import android.app.Application
+import android.app.ApplicationExitInfo
 import android.app.PendingIntent
 import android.content.Context
 import android.content.Intent
@@ -20,7 +24,7 @@ import com.grkj.data.hardware.ble.BleUtil
 import com.grkj.data.hardware.modbus.ModBusController
 import com.grkj.iscs.features.splash.activity.SplashActivity
 import com.grkj.shared.model.EventBean
-import com.grkj.data.hardware.face.ArcSoftUtil
+import com.grkj.data.hardware.face.FaceUtil
 import com.grkj.data.hardware.face.hlk.Hlk223Client
 import com.grkj.data.hardware.face.hlk.Hlk223Config
 import com.grkj.shared.utils.i18n.I18nManager
@@ -28,13 +32,11 @@ import com.grkj.shared.utils.i18n.LanguageCatalog
 import com.grkj.shared.utils.i18n.LanguageStore
 import com.grkj.shared.utils.i18n.source.XmlResourcesI18nSource
 import com.grkj.ui_base.business.HardwareBusinessManager
-import com.grkj.ui_base.service.CheckKeyInfoTask
 import com.grkj.ui_base.utils.CommonUtils
 import com.kongzue.dialogx.DialogX
 import com.scwang.smart.refresh.footer.ClassicsFooter
 import com.scwang.smart.refresh.header.ClassicsHeader
 import com.scwang.smart.refresh.layout.SmartRefreshLayout
-import com.sik.comm.impl_modbus.ModbusProtocol
 import com.sik.sikcore.SIKCore
 import com.sik.sikcore.crash.GlobalCrashCatch
 import com.sik.sikcore.log.LogUtils
@@ -62,6 +64,7 @@ class ISCSApplication : Application() {
      */
     override fun onCreate() {
         super.onCreate()
+        logLastExitReason(this)
         initLogger()
         DialogX.init(this)
         SIKCore.init(this)
@@ -92,14 +95,14 @@ class ISCSApplication : Application() {
         if (ISCSConfig.isInit) {
             BleUtil.instance?.initBle(this)
         }
-        //todo 模拟器不支持 测试用,直接创建管理员账号
-        ArcSoftUtil.checkActiveStatus(SIKCore.getApplication())
-        ArcSoftUtil.initEngine(SIKCore.getApplication())
 
         // ② 建一个 HLK 客户端(串口或你现有的 Modbus 封装)
         val hlk = Hlk223Client(Hlk223Config.getProtocol(), "HLK-223")
         // ③ 想切到 HLK 路线(但保持对外 API 不变)
-        ArcSoftUtil.enableHlkBackend(hlk)
+        FaceUtil.enableHlkBackend(hlk)
+        //todo 模拟器不支持 测试用,直接创建管理员账号
+        FaceUtil.checkActiveStatus(SIKCore.getApplication())
+        FaceUtil.initEngine(SIKCore.getApplication())
         AutoSizeConfig.getInstance().isCustomFragment = false
         StateConfig.emptyLayout = com.grkj.ui_base.R.layout.layout_empty
         ThreadUtils.runOnIO {
@@ -156,6 +159,36 @@ class ISCSApplication : Application() {
         }
     }
 
+    @SuppressLint("NewApi")
+    fun logLastExitReason(context: Context) {
+        val am = context.getSystemService(Activity.ACTIVITY_SERVICE) as ActivityManager
+        val list = am.getHistoricalProcessExitReasons(context.packageName, 0, 5)
+        if (list.isNullOrEmpty()) return
+        val last = list.first()
+        val reason = when (last.reason) {
+            ApplicationExitInfo.REASON_ANR -> "ANR"
+            ApplicationExitInfo.REASON_CRASH -> "CRASH"
+            ApplicationExitInfo.REASON_CRASH_NATIVE -> "CRASH_NATIVE"
+            ApplicationExitInfo.REASON_DEPENDENCY_DIED -> "DEPENDENCY_DIED"
+            ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE -> "EXCESSIVE_RESOURCE"
+            ApplicationExitInfo.REASON_EXIT_SELF -> "EXIT_SELF"
+            ApplicationExitInfo.REASON_FREEZER -> "FREEZER"
+            ApplicationExitInfo.REASON_INITIALIZATION_FAILURE -> "INIT_FAILURE"
+            ApplicationExitInfo.REASON_LOW_MEMORY -> "LOW_MEMORY"
+            ApplicationExitInfo.REASON_OTHER -> "OTHER"
+            ApplicationExitInfo.REASON_PACKAGE_STATE_CHANGE -> "PKG_STATE_CHANGE"
+            ApplicationExitInfo.REASON_PACKAGE_UPDATED -> "PKG_UPDATED"
+            ApplicationExitInfo.REASON_PERMISSION_CHANGE -> "PERMISSION_CHANGE"
+            ApplicationExitInfo.REASON_SIGNALED -> "SIGNALED(${last.status})"
+            ApplicationExitInfo.REASON_USER_REQUESTED -> "USER_REQUESTED"
+            ApplicationExitInfo.REASON_USER_STOPPED -> "USER_STOPPED"
+            else -> "UNKNOWN(${last.reason})"
+        }
+        android.util.Log.w("ExitInfo",
+            "lastExit: reason=$reason, pss=${last.pss}KB, rss=${last.rss}KB, " +
+                    "timestamp=${java.util.Date(last.timestamp)}, description=${last.description}")
+    }
+
     private fun initLogger() {
         val logDir = File(getExternalFilesDir(null), "iscs/logs")
         if (!logDir.exists()) logDir.mkdirs()

+ 8 - 11
iscs_lock/src/main/java/com/grkj/iscs/features/login/dialog/LoginDialog.kt

@@ -7,8 +7,8 @@ import com.grkj.data.enums.LoginResultEnum
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.DialogLoginBinding
 import com.grkj.iscs.features.login.viewmodel.LoginViewModel
-import com.grkj.data.hardware.face.ArcSoftUtil
-import com.grkj.data.hardware.face.ArcSoftUtil.inDetecting
+import com.grkj.data.hardware.face.FaceUtil
+import com.grkj.data.hardware.face.FaceUtil.inDetecting
 import com.grkj.ui_base.skin.loadSkinIcon
 import com.grkj.ui_base.utils.CommonUtils
 import com.grkj.data.utils.event.LoadingEvent
@@ -22,7 +22,6 @@ import com.sik.sikcore.SIKCore
 import com.sik.sikcore.extension.setDebouncedClickListener
 import com.sik.sikcore.thread.ThreadUtils
 import com.sik.sikimage.ImageConvertUtils
-import kotlinx.coroutines.delay
 
 /**
  * 登录弹窗
@@ -58,7 +57,7 @@ class LoginDialog(
         customDialog?.setMaskColor(CommonUtils.getColor(com.grkj.skin.R.attr.scrim))
         customDialog.setDialogLifecycleCallback(object : DialogLifecycleCallback<CustomDialog>() {
             override fun onDismiss(dialog: CustomDialog) {
-                ArcSoftUtil.stop()
+                FaceUtil.stop()
                 super.onDismiss(dialog)
             }
         })
@@ -86,7 +85,7 @@ class LoginDialog(
             override fun onDismiss(dialog: CustomDialog) {
                 when (mLoginType) {
                     0 -> {
-                        ArcSoftUtil.stop()
+                        FaceUtil.stop()
                     }
 
                     1 -> {
@@ -112,10 +111,9 @@ class LoginDialog(
             mBinding.tvTip.text = mPairList[mLoginType].first
             when (mLoginType) {
                 0 -> {
-                    if (!ArcSoftUtil.isActivated) {
+                    if (!FaceUtil.isActivated) {
                         PopTip.tip(
                             CommonUtils.getStr("face_can_not_process")
-                                .toString()
                         )
                     } else {
                         startFace()
@@ -182,8 +180,7 @@ class LoginDialog(
     private fun startFace() {
         inDetecting = false
         ActivityTracker.getCurrentActivity()?.let { context ->
-            ArcSoftUtil.checkCamera(
-                context.windowManager,
+            FaceUtil.checkCamera(
                 mBinding.preview!!
             ) { bitmap, userId ->
                 viewModel.loginWithUserId(userId).observe(lifecycleOwner) {
@@ -195,13 +192,13 @@ class LoginDialog(
                                         inDetecting = false
                                         inFaceChecking = false
                                     } else {
-                                        ArcSoftUtil.stop()
+                                        FaceUtil.stop()
                                     }
                                     callBack?.invoke(it)
                                 }
                             }
                     } else {
-                        ArcSoftUtil.stop()
+                        FaceUtil.stop()
                         callBack?.invoke(it)
                     }
                 }

+ 2 - 2
iscs_lock/src/main/java/com/grkj/iscs/features/login/viewmodel/LoginViewModel.kt

@@ -8,7 +8,7 @@ import com.grkj.data.domain.logic.IRoleLogic
 import com.grkj.data.domain.logic.IUserLogic
 import com.grkj.data.domain.logic.IWorkflowLogic
 import com.grkj.shared.config.AESConfig
-import com.grkj.data.hardware.face.ArcSoftUtil
+import com.grkj.data.hardware.face.FaceUtil
 import com.grkj.ui_base.base.BaseViewModel
 import com.sik.sikcore.extension.file
 import com.sik.sikcore.extension.toJson
@@ -112,7 +112,7 @@ class LoginViewModel @Inject constructor(
             val user = userLogic.getAllFaceData()
             val userFaceData = user.filter { it.content.file().exists() }
                 .map { it.userId to it.content.file().readText() }
-            ArcSoftUtil.registerFace(userFaceData)
+//            FaceUtil.registerFace(userFaceData)
             emit(true)
         }
     }

+ 23 - 26
iscs_lock/src/main/java/com/grkj/iscs/features/main/dialog/CheckFaceDialog.kt

@@ -7,7 +7,7 @@ import com.grkj.data.data.MainDomainData
 import com.grkj.data.enums.LoginResultEnum
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.DialogCheckFaceBinding
-import com.grkj.data.hardware.face.ArcSoftUtil
+import com.grkj.data.hardware.face.FaceUtil
 import com.grkj.ui_base.base.BaseViewModel
 import com.grkj.ui_base.skin.loadSkinIcon
 import com.grkj.ui_base.utils.CommonUtils
@@ -52,14 +52,14 @@ class CheckFaceDialog(
         customDialog.setMaskColor(CommonUtils.getColor(com.grkj.skin.R.attr.scrim))
         customDialog.setDialogLifecycleCallback(object : DialogLifecycleCallback<CustomDialog>() {
             override fun onDismiss(dialog: CustomDialog) {
-                ArcSoftUtil.stop()
+                FaceUtil.stop()
                 super.onDismiss(dialog)
             }
         })
         mBinding.closeIv.setDebouncedClickListener {
             when (mLoginType) {
                 0 -> {
-                    ArcSoftUtil.stop()
+                    FaceUtil.stop()
                 }
 
                 1 -> {
@@ -72,7 +72,7 @@ class CheckFaceDialog(
             override fun onDismiss(dialog: CustomDialog) {
                 when (mLoginType) {
                     0 -> {
-                        ArcSoftUtil.stop()
+                        FaceUtil.stop()
                     }
 
                     1 -> {
@@ -87,7 +87,7 @@ class CheckFaceDialog(
         mBinding.tvTip.text = mPairList[mLoginType].first
         when (mLoginType) {
             0 -> {
-                if (!ArcSoftUtil.isActivated) {
+                if (!FaceUtil.isActivated) {
                     PopTip.tip(
                         CommonUtils.getStr("face_can_not_process")
                     )
@@ -105,8 +105,9 @@ class CheckFaceDialog(
                             true
                         )
                     }
+
                     override fun onScan(bitmap: Bitmap) {
-                        if (FingerprintUtil.isZKDevice){
+                        if (FingerprintUtil.isZKDevice) {
                             LoadingEvent.sendLoadingEvent(
                                 CommonUtils.getStr("doing_checking"), true
                             )
@@ -155,35 +156,31 @@ class CheckFaceDialog(
 
 
     private fun startFace() {
-        ArcSoftUtil.inDetecting = false
+        FaceUtil.inDetecting = false
         ActivityTracker.getCurrentActivity()?.let { context ->
-            ArcSoftUtil.initCamera(
+            FaceUtil.checkCamera(
                 mBinding.preview!!,
-            ) { bitmap, faceSize, alive ->
+            ) { bitmap, userId ->
                 bitmap?.let { itBitmap ->
-                    if (faceSize == 0) {
-                        ArcSoftUtil.inDetecting = false
+                    if (userId == null) {
+                        FaceUtil.inDetecting = false
                         return@let
                     }
                     if (inFaceChecking) {
-                        ArcSoftUtil.inDetecting = false
+                        FaceUtil.inDetecting = false
                         return@let
                     }
                     inFaceChecking = true
-                    viewModel.checkFace(
-                        ImageConvertUtils.bitmapToBase64(itBitmap).toString(), userId
-                    ).observe(lifecycleOwner) {
-                        LoadingEvent.sendLoadingEvent()
-                        if (it == LoginResultEnum.FACE_VERIFY_FAILED) {
-                            ArcSoftUtil.stop()
-                            dialog?.dismiss()
-                            callBack?.invoke(it)
-                            PopTip.tip(CommonUtils.getStr("face_login_failed"))
-                        } else {
-                            ArcSoftUtil.stop()
-                            dialog?.dismiss()
-                            callBack?.invoke(it)
-                        }
+                    LoadingEvent.sendLoadingEvent()
+                    if (userId != this.userId) {
+                        FaceUtil.stop()
+                        dialog?.dismiss()
+                        callBack?.invoke(LoginResultEnum.FACE_VERIFY_FAILED)
+                        PopTip.tip(CommonUtils.getStr("face_login_failed"))
+                    } else {
+                        FaceUtil.stop()
+                        dialog?.dismiss()
+                        callBack?.invoke(LoginResultEnum.FACE_VERIFY_SUCCESS)
                     }
                     LoadingEvent.sendLoadingEvent(CommonUtils.getStr("doing_checking"))
                 }

+ 9 - 10
iscs_lock/src/main/java/com/grkj/iscs/features/main/dialog/data_manage/RegisterFaceDialog.kt

@@ -1,7 +1,6 @@
 package com.grkj.iscs.features.main.dialog.data_manage
 
 import android.graphics.Bitmap
-import android.view.Gravity
 import android.view.View
 import androidx.core.view.isVisible
 import com.grkj.data.data.CommonConstants
@@ -9,7 +8,7 @@ import com.grkj.data.data.MainDomainData
 import com.grkj.data.utils.FileStorageUtils
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.DialogRegisterFaceBinding
-import com.grkj.data.hardware.face.ArcSoftUtil
+import com.grkj.data.hardware.face.FaceUtil
 import com.grkj.shared.utils.CancellableTimer
 import com.grkj.ui_base.utils.CommonUtils
 import com.kongzue.dialogx.dialogs.CustomDialog
@@ -33,7 +32,7 @@ class RegisterFaceDialog(private val onConfirm: (String, String) -> Unit) :
     private val captureTimer = CancellableTimer(4000, 1000, {
         binding.countDownTip.text = "${(3000 - it) / 1000}"
     }) {
-        ArcSoftUtil.inDetecting = true
+        FaceUtil.inDetecting = true
         isFaceDetect = true
         binding.previewLayout.visibility = View.INVISIBLE
         binding.image.visibility = View.VISIBLE
@@ -44,7 +43,7 @@ class RegisterFaceDialog(private val onConfirm: (String, String) -> Unit) :
     }
     private val reCaptureTimer = CancellableTimer(2000, 1000, {}) {
         isFaceDetect = false
-        ArcSoftUtil.inDetecting = false
+        FaceUtil.inDetecting = false
         isInCountDown = false
     }
 
@@ -89,8 +88,8 @@ class RegisterFaceDialog(private val onConfirm: (String, String) -> Unit) :
     private fun startFace() {
         binding.previewLayout.isVisible = true
         binding.image.isVisible = false
-        ArcSoftUtil.inDetecting = false
-        ArcSoftUtil.initCamera(
+        FaceUtil.inDetecting = false
+        FaceUtil.initCamera(
             binding.preview,
             binding.faceOverlayView,
             true,
@@ -99,14 +98,14 @@ class RegisterFaceDialog(private val onConfirm: (String, String) -> Unit) :
             logger.info("人脸检测结果: ${bitmap == null},$faceSize,$alive")
             if (faceSize > 1) {
                 binding.tipTv.text = CommonUtils.getStr("only_one_person_allowed")
-                ArcSoftUtil.inDetecting = false
+                FaceUtil.inDetecting = false
                 stopCountDown()
                 return@initCamera
             }
             if (alive == false) {
                 binding.tipTv.text =
                     CommonUtils.getStr("real_person_verification_required")
-                ArcSoftUtil.inDetecting = false
+                FaceUtil.inDetecting = false
                 stopCountDown()
                 return@initCamera
             }
@@ -117,7 +116,7 @@ class RegisterFaceDialog(private val onConfirm: (String, String) -> Unit) :
                 mCapturedBitmap = bitmap
                 binding.image.setImageBitmap(bitmap)
             }
-            ArcSoftUtil.inDetecting = false
+            FaceUtil.inDetecting = false
         }
     }
 
@@ -136,7 +135,7 @@ class RegisterFaceDialog(private val onConfirm: (String, String) -> Unit) :
     }
 
     private fun releaseFace() {
-        ArcSoftUtil.stop()
+        FaceUtil.stop()
     }
 
     companion object {

+ 2 - 2
iscs_lock/src/main/java/com/grkj/iscs/features/main/entity/QuickEntranceMenuItemEntity.kt

@@ -77,14 +77,13 @@ data class QuickEntranceMenuItemEntity(
                 RoleFunctionalPermissionsEnum.LOCKED_POINT,
                 RoleFunctionalPermissionsEnum.CREATE_SOP_JOB -> R.navigation.nav_job_manage
 
-                RoleFunctionalPermissionsEnum.EXCEPTION_JOB -> R.navigation.nav_exception_job_manage
-
                 RoleFunctionalPermissionsEnum.SLOT_MANAGE,
                 RoleFunctionalPermissionsEnum.KEY_MANAGE,
                 RoleFunctionalPermissionsEnum.LOCK_MANAGE,
                 RoleFunctionalPermissionsEnum.CARD_MANAGE,
                 RoleFunctionalPermissionsEnum.RFID_MANAGE -> R.navigation.nav_hardware_manage
 
+                RoleFunctionalPermissionsEnum.EXCEPTION_JOB,
                 RoleFunctionalPermissionsEnum.EXCEPTION_REPORT,
                 RoleFunctionalPermissionsEnum.EXCEPTION_MANAGE -> R.navigation.nav_exception_manage
 
@@ -123,6 +122,7 @@ data class QuickEntranceMenuItemEntity(
                 RoleFunctionalPermissionsEnum.LOCK_MANAGE -> R.id.action_hardwareManageHomeFragment_to_lockManageFragment
                 RoleFunctionalPermissionsEnum.CARD_MANAGE -> R.id.action_hardwareManageHomeFragment_to_cardManageFragment
                 RoleFunctionalPermissionsEnum.RFID_MANAGE -> R.id.action_hardwareManageHomeFragment_to_rfidTokenManageFragment
+                RoleFunctionalPermissionsEnum.EXCEPTION_JOB -> R.id.action_exceptionManageHomeFragment_to_nav_exception_job_manage
                 RoleFunctionalPermissionsEnum.EXCEPTION_REPORT -> R.id.action_exceptionManageHomeFragment_to_exceptionReportFragment
                 RoleFunctionalPermissionsEnum.EXCEPTION_MANAGE -> R.id.action_exceptionManageHomeFragment_to_exceptionManageFragment
                 RoleFunctionalPermissionsEnum.USER_INFO -> R.id.action_userInfoHomeFragment_to_userInfoFragment

+ 4 - 0
iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/data_manage/UserManageFragment.kt

@@ -378,6 +378,7 @@ class UserManageFragment : BaseFragment<FragmentUserManageBinding>() {
                         mFingerprintPressTimes++
                         if (mFingerprintPressTimes == maxPressTimes) {
                             dialog.dismiss()
+                            hideLoading()
                             showToast(CommonUtils.getStr("fingerprint_add_success_tip"))
                             registerResult(mFingerprintGroupName)
                         } else if (mFingerprintInputErrorTimes == inputFingerprintErrorTimes) {
@@ -392,12 +393,14 @@ class UserManageFragment : BaseFragment<FragmentUserManageBinding>() {
                                 .observe(this@UserManageFragment) {
                                     getUserData(false)
                                 }
+                            hideLoading()
                             showToast(CommonUtils.getStr("please_re_press_fingerprint_again"))
                         } else {
                             pressTip?.text = CommonUtils.getStr(
                                 "fingerprint_scan_tip",
                                 maxPressTimes - mFingerprintPressTimes
                             )
+                            hideLoading()
                             showToast(CommonUtils.getStr("please_press_fingerprint_again"))
                         }
                     } else {
@@ -414,6 +417,7 @@ class UserManageFragment : BaseFragment<FragmentUserManageBinding>() {
                                 .observe(this@UserManageFragment) {
                                     getUserData(false)
                                 }
+                            hideLoading()
                             showToast(CommonUtils.getStr("please_re_press_fingerprint_again"))
                         }
                     }

+ 15 - 17
iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/hardware_manage/LockManageFragment.kt

@@ -108,24 +108,22 @@ class LockManageFragment : BaseFragment<FragmentLockManageBinding>() {
         }
         bind.root.setDebouncedClickListener {
             UpdateLockDialog.show(item) { vo, dialog ->
-                viewModel.validateLockData(vo.lockNfc ?: "").observe(this) {
-                    viewModel.updateLock(vo).observe(this) { ok ->
-                        dialog.dismiss()
-                        val titleRes =
-                            if (ok) "action_succeed" else "action_failed"
-                        val msgRes =
-                            if (ok) "update_lock_succeed" else "update_lock_failed"
+                viewModel.updateLock(vo).observe(this) { ok ->
+                    dialog.dismiss()
+                    val titleRes =
+                        if (ok) "action_succeed" else "action_failed"
+                    val msgRes =
+                        if (ok) "update_lock_succeed" else "update_lock_failed"
 
-                        TipDialog.show(
-                            title = CommonUtils.getStr(titleRes),
-                            dialogType = if (ok) TipDialog.DialogType.SUCCESS else TipDialog.DialogType.ERROR,
-                            msg = CommonUtils.getStr(msgRes),
-                            showCancel = false,
-                            onConfirmClick = {
-                                loadLocks(true)
-                            },
-                        )
-                    }
+                    TipDialog.show(
+                        title = CommonUtils.getStr(titleRes),
+                        dialogType = if (ok) TipDialog.DialogType.SUCCESS else TipDialog.DialogType.ERROR,
+                        msg = CommonUtils.getStr(msgRes),
+                        showCancel = false,
+                        onConfirmClick = {
+                            loadLocks(true)
+                        },
+                    )
                 }
             }
         }

+ 1 - 1
iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/home/HomeFragment.kt

@@ -156,7 +156,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
     }
 
     private fun pickDateTime(startTime: Boolean = true, timeView: TextView) {
-        AutoSize.autoConvertDensity(requireActivity(), 600f, false)
+        AutoSize.autoConvertDensity(requireActivity(), 600f, isPortrait())
         CardDatePickerDialog.builder(requireContext()).setTitle(
             if (startTime) CommonUtils.getStr("start_time") else CommonUtils.getStr("end_time")
         )

+ 2 - 0
iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/CreateJobFragment.kt

@@ -37,6 +37,7 @@ import com.grkj.ui_base.base.BaseFormFragment
 import com.grkj.ui_base.dialog.TipDialog
 import com.grkj.ui_base.skin.loadSkinIcon
 import com.grkj.ui_base.utils.CommonUtils
+import com.grkj.ui_base.utils.removeBgTint
 import com.sik.sikcore.data.GlobalDataTempStore
 import com.sik.sikcore.extension.file
 import com.sik.sikcore.extension.getMMKVData
@@ -319,6 +320,7 @@ class CreateJobFragment : BaseFormFragment<FragmentCreateJobBinding>() {
         itemBinding.stepNameTv.text = item.stepTitleShort
         itemBinding.stepIndexTv.text = item.stepIndex.toString()
         itemBinding.dividerIv.isVisible = item.stepIndex != viewModel.workflowSteps.last().stepIndex
+        itemBinding.stepLayout.removeBgTint()
     }
 
     override fun initData() {

+ 2 - 0
iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/CreateSopFragment.kt

@@ -37,6 +37,7 @@ import com.grkj.ui_base.base.BaseFormFragment
 import com.grkj.ui_base.dialog.TipDialog
 import com.grkj.ui_base.skin.loadSkinIcon
 import com.grkj.ui_base.utils.CommonUtils
+import com.grkj.ui_base.utils.removeBgTint
 import com.sik.sikcore.data.GlobalDataTempStore
 import com.sik.sikcore.extension.file
 import com.sik.sikcore.extension.getMMKVData
@@ -285,6 +286,7 @@ class CreateSopFragment : BaseFormFragment<FragmentCreateSopBinding>() {
         itemBinding.stepNameTv.text = item.stepTitleShort
         itemBinding.stepIndexTv.text = item.stepIndex.toString()
         itemBinding.dividerIv.isVisible = item.stepIndex != viewModel.workflowSteps.last().stepIndex
+        itemBinding.stepLayout.removeBgTint()
     }
 
     override fun initData() {

+ 2 - 0
iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/CreateSopJobFragment.kt

@@ -36,6 +36,7 @@ import com.grkj.ui_base.base.BaseFormFragment
 import com.grkj.ui_base.dialog.TipDialog
 import com.grkj.ui_base.skin.loadSkinIcon
 import com.grkj.ui_base.utils.CommonUtils
+import com.grkj.ui_base.utils.removeBgTint
 import com.sik.sikcore.data.GlobalDataTempStore
 import com.sik.sikcore.extension.file
 import com.sik.sikcore.extension.setDebouncedClickListener
@@ -238,6 +239,7 @@ class CreateSopJobFragment : BaseFormFragment<FragmentCreateSopJobBinding>() {
         itemBinding.stepNameTv.text = item.stepTitleShort
         itemBinding.stepIndexTv.text = item.stepIndex.toString()
         itemBinding.dividerIv.isVisible = item.stepIndex != viewModel.workflowSteps.last().stepIndex
+        itemBinding.stepLayout.removeBgTint()
     }
 
     override fun initData() {

+ 2 - 0
iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/EditJobFragment.kt

@@ -36,6 +36,7 @@ import com.grkj.ui_base.base.BaseFormFragment
 import com.grkj.ui_base.dialog.TipDialog
 import com.grkj.ui_base.skin.loadSkinIcon
 import com.grkj.ui_base.utils.CommonUtils
+import com.grkj.ui_base.utils.removeBgTint
 import com.sik.sikcore.data.GlobalDataTempStore
 import com.sik.sikcore.extension.file
 import com.sik.sikcore.extension.setDebouncedClickListener
@@ -252,6 +253,7 @@ class EditJobFragment : BaseFormFragment<FragmentEditJobBinding>() {
         itemBinding.stepNameTv.text = item.stepTitleShort
         itemBinding.stepIndexTv.text = item.stepIndex.toString()
         itemBinding.dividerIv.isVisible = item.stepIndex != viewModel.workflowSteps.last().stepIndex
+        itemBinding.stepLayout.removeBgTint()
     }
 
     /**

+ 2 - 0
iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/EditSopFragment.kt

@@ -37,6 +37,7 @@ import com.grkj.ui_base.base.BaseFormFragment
 import com.grkj.ui_base.dialog.TipDialog
 import com.grkj.ui_base.skin.loadSkinIcon
 import com.grkj.ui_base.utils.CommonUtils
+import com.grkj.ui_base.utils.removeBgTint
 import com.sik.sikcore.data.GlobalDataTempStore
 import com.sik.sikcore.extension.file
 import com.sik.sikcore.extension.setDebouncedClickListener
@@ -250,6 +251,7 @@ class EditSopFragment : BaseFormFragment<FragmentEditSopBinding>() {
         itemBinding.stepNameTv.text = item.stepTitleShort
         itemBinding.stepIndexTv.text = item.stepIndex.toString()
         itemBinding.dividerIv.isVisible = item.stepIndex != viewModel.workflowSteps.last().stepIndex
+        itemBinding.stepLayout.removeBgTint()
     }
 
     /**

+ 2 - 0
iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/EditSopJobFragment.kt

@@ -36,6 +36,7 @@ import com.grkj.ui_base.base.BaseFormFragment
 import com.grkj.ui_base.dialog.TipDialog
 import com.grkj.ui_base.skin.loadSkinIcon
 import com.grkj.ui_base.utils.CommonUtils
+import com.grkj.ui_base.utils.removeBgTint
 import com.sik.sikcore.data.GlobalDataTempStore
 import com.sik.sikcore.extension.file
 import com.sik.sikcore.extension.setDebouncedClickListener
@@ -240,6 +241,7 @@ class EditSopJobFragment : BaseFormFragment<FragmentEditSopJobBinding>() {
         itemBinding.stepNameTv.text = item.stepTitleShort
         itemBinding.stepIndexTv.text = item.stepIndex.toString()
         itemBinding.dividerIv.isVisible = item.stepIndex != viewModel.workflowSteps.last().stepIndex
+        itemBinding.stepLayout.removeBgTint()
     }
 
     /**

+ 32 - 3
iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/JobExecuteFragment.kt

@@ -14,6 +14,11 @@ import com.drake.brv.utils.grid
 import com.drake.brv.utils.linear
 import com.drake.brv.utils.models
 import com.drake.brv.utils.setup
+import com.google.android.flexbox.AlignItems
+import com.google.android.flexbox.FlexDirection
+import com.google.android.flexbox.FlexWrap
+import com.google.android.flexbox.FlexboxLayoutManager
+import com.google.android.flexbox.JustifyContent
 import com.grkj.data.data.EventConstants
 import com.grkj.data.data.MMKVConstants
 import com.grkj.data.data.MainDomainData
@@ -39,6 +44,7 @@ import com.grkj.ui_base.skin.loadSkinIcon
 import com.grkj.ui_base.utils.CommonUtils
 import com.grkj.ui_base.utils.changeBgTint
 import com.grkj.ui_base.utils.event.FlashTipEvent
+import com.grkj.ui_base.utils.event.InRFIDScanModeEvent
 import com.grkj.ui_base.utils.event.RFIDCardReadEvent
 import com.grkj.ui_base.utils.event.UiEvent
 import com.grkj.ui_base.utils.extension.toggleExpandView
@@ -145,21 +151,42 @@ class JobExecuteFragment : BaseFragment<FragmentJobExecuteBinding>() {
                 toUnLock(viewModel.groupInfo[0].groupId)
             }
         }
-        binding.waitToColockRv.grid(3).dividerSpace(10, DividerOrientation.GRID).setup {
+        binding.waitToColockRv.apply {
+            layoutManager = FlexboxLayoutManager(context).apply {
+                flexDirection = FlexDirection.ROW               // 横向
+                flexWrap = FlexWrap.WRAP
+                justifyContent = JustifyContent.SPACE_EVENLY     // 等间距居中
+                alignItems = AlignItems.CENTER               // 垂直居中对齐
+            }
+        }.dividerSpace(10, DividerOrientation.GRID).setup {
             addType<IsJobTicketUserDataVo>(R.layout.item_job_execute_colock)
             onBind {
                 onColockerRVListBinding(this)
             }
         }
 
-        binding.alreadyColockRv.grid(3).dividerSpace(10, DividerOrientation.GRID).setup {
+        binding.alreadyColockRv.apply {
+            layoutManager = FlexboxLayoutManager(context).apply {
+                flexDirection = FlexDirection.ROW               // 横向
+                flexWrap = FlexWrap.WRAP
+                justifyContent = JustifyContent.SPACE_EVENLY     // 等间距居中
+                alignItems = AlignItems.CENTER               // 垂直居中对齐
+            }
+        }.dividerSpace(10, DividerOrientation.GRID).setup {
             addType<IsJobTicketUserDataVo>(R.layout.item_job_execute_colock)
             onBind {
                 onColockerRVListBinding(this)
             }
         }
 
-        binding.alreadyUncolockRv.grid(3).dividerSpace(10, DividerOrientation.GRID).setup {
+        binding.alreadyUncolockRv.apply {
+            layoutManager = FlexboxLayoutManager(context).apply {
+                flexDirection = FlexDirection.ROW               // 横向
+                flexWrap = FlexWrap.WRAP
+                justifyContent = JustifyContent.SPACE_EVENLY     // 等间距居中
+                alignItems = AlignItems.CENTER               // 垂直居中对齐
+            }
+        }.dividerSpace(10, DividerOrientation.GRID).setup {
             addType<IsJobTicketUserDataVo>(R.layout.item_job_execute_colock)
             onBind {
                 onColockerRVListBinding(this)
@@ -591,6 +618,7 @@ class JobExecuteFragment : BaseFragment<FragmentJobExecuteBinding>() {
 
     override fun onPause() {
         super.onPause()
+        InRFIDScanModeEvent.sendInRFIDScanModeEvent(false)
         FlashTipEvent.sendFlashTipEvent()
     }
 
@@ -701,6 +729,7 @@ class JobExecuteFragment : BaseFragment<FragmentJobExecuteBinding>() {
 
     override fun onResume() {
         super.onResume()
+        InRFIDScanModeEvent.sendInRFIDScanModeEvent()
         if (GlobalDataTempStore.getInstance()
                 .hasData(DataTransferConstants.KEY_SELECTED_MEMBER_LOCKER_DATA)
         ) {

+ 9 - 9
iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/user_info/SetFaceFragment.kt

@@ -11,7 +11,7 @@ import com.grkj.data.utils.FileStorageUtils
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.FragmentSetFaceBinding
 import com.grkj.iscs.features.main.viewmodel.user_info.UserInfoViewModel
-import com.grkj.data.hardware.face.ArcSoftUtil
+import com.grkj.data.hardware.face.FaceUtil
 import com.grkj.shared.utils.CancellableTimer
 import com.grkj.ui_base.base.BaseFragment
 import com.grkj.ui_base.utils.CommonUtils
@@ -35,7 +35,7 @@ class SetFaceFragment : BaseFragment<FragmentSetFaceBinding>() {
     private val captureTimer = CancellableTimer(4000, 1000, {
         binding.countDownTip.text = "${(3000 - it) / 1000}"
     }) {
-        ArcSoftUtil.inDetecting = true
+        FaceUtil.inDetecting = true
         isFaceDetect = true
         binding.previewLayout.visibility = View.INVISIBLE
         binding.image.visibility = View.VISIBLE
@@ -46,7 +46,7 @@ class SetFaceFragment : BaseFragment<FragmentSetFaceBinding>() {
     }
     private val reCaptureTimer = CancellableTimer(2000, 1000, {}) {
         isFaceDetect = false
-        ArcSoftUtil.inDetecting = false
+        FaceUtil.inDetecting = false
         isInCountDown = false
     }
 
@@ -144,8 +144,8 @@ class SetFaceFragment : BaseFragment<FragmentSetFaceBinding>() {
     private fun startFace() {
         binding.previewLayout.isVisible = true
         binding.image.isVisible = false
-        ArcSoftUtil.inDetecting = false
-        ArcSoftUtil.initCamera(
+        FaceUtil.inDetecting = false
+        FaceUtil.initCamera(
             binding.preview,
             binding.faceOverlayView,
             true,
@@ -154,14 +154,14 @@ class SetFaceFragment : BaseFragment<FragmentSetFaceBinding>() {
             logger.info("人脸检测结果: ${bitmap == null},$faceSize,$alive")
             if (faceSize > 1) {
                 binding.tipTv.text = CommonUtils.getStr("only_one_person_allowed")
-                ArcSoftUtil.inDetecting = false
+                FaceUtil.inDetecting = false
                 stopCountDown()
                 return@initCamera
             }
             if (alive == false) {
                 binding.tipTv.text =
                     CommonUtils.getStr("real_person_verification_required")
-                ArcSoftUtil.inDetecting = false
+                FaceUtil.inDetecting = false
                 stopCountDown()
                 return@initCamera
             }
@@ -172,7 +172,7 @@ class SetFaceFragment : BaseFragment<FragmentSetFaceBinding>() {
                 mCapturedBitmap = bitmap
                 binding.image.setImageBitmap(bitmap)
             }
-            ArcSoftUtil.inDetecting = false
+            FaceUtil.inDetecting = false
         }
     }
 
@@ -191,7 +191,7 @@ class SetFaceFragment : BaseFragment<FragmentSetFaceBinding>() {
     }
 
     private fun releaseFace() {
-        ArcSoftUtil.stop()
+        FaceUtil.stop()
     }
 
 }

+ 4 - 0
iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/user_info/SetFingerprintFragment.kt

@@ -171,6 +171,7 @@ class SetFingerprintFragment : BaseFragment<FragmentSetFingerprintBinding>() {
                         mFingerprintPressTimes++
                         if (mFingerprintPressTimes == maxPressTimes) {
                             dialog.dismiss()
+                            hideLoading()
                             showToast(CommonUtils.getStr("fingerprint_add_success_tip"))
                             getData()
                         } else if (mFingerprintInputErrorTimes == inputFingerprintErrorTimes) {
@@ -185,12 +186,14 @@ class SetFingerprintFragment : BaseFragment<FragmentSetFingerprintBinding>() {
                                 .observe(this@SetFingerprintFragment) {
                                     getData()
                                 }
+                            hideLoading()
                             showToast(CommonUtils.getStr("please_re_press_fingerprint_again"))
                         } else {
                             pressTip?.text = CommonUtils.getStr(
                                 "fingerprint_scan_tip",
                                 maxPressTimes - mFingerprintPressTimes
                             )
+                            hideLoading()
                             showToast(CommonUtils.getStr("please_press_fingerprint_again"))
                         }
                     } else {
@@ -207,6 +210,7 @@ class SetFingerprintFragment : BaseFragment<FragmentSetFingerprintBinding>() {
                                 .observe(this@SetFingerprintFragment) {
                                     getData()
                                 }
+                            hideLoading()
                             showToast(CommonUtils.getStr("please_re_press_fingerprint_again"))
                         }
                     }

+ 3 - 0
iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/user_info/SetJobCardFragment.kt

@@ -10,6 +10,7 @@ import com.grkj.shared.model.EventBean
 import com.grkj.ui_base.base.BaseFragment
 import com.grkj.ui_base.dialog.TipDialog
 import com.grkj.ui_base.utils.CommonUtils
+import com.grkj.ui_base.utils.event.InRFIDScanModeEvent
 import com.grkj.ui_base.utils.event.RFIDCardReadEvent
 import com.sik.sikcore.extension.setDebouncedClickListener
 import dagger.hilt.android.AndroidEntryPoint
@@ -30,6 +31,7 @@ class SetJobCardFragment : BaseFragment<FragmentSetJobCardBinding>() {
             navController.popBackStack()
         }
         binding.setOrResetJobCard.setDebouncedClickListener {
+            InRFIDScanModeEvent.sendInRFIDScanModeEvent(true)
             binding.jobCardSetLayout.isVisible = true
             binding.jobCardViewLayout.isVisible = false
             canHandlerCardNo = true
@@ -51,6 +53,7 @@ class SetJobCardFragment : BaseFragment<FragmentSetJobCardBinding>() {
                 canHandlerCardNo = false
                 viewModel.saveUserJobCard((event.data as RFIDCardReadEvent).rfidNo)
                     .observe(this@SetJobCardFragment) {
+                        InRFIDScanModeEvent.sendInRFIDScanModeEvent(false)
                         TipDialog.show(
                             msg = CommonUtils.getStr("save_success"),
                             showCancel = false,

+ 9 - 11
iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/user_info/UserInfoFragment.kt

@@ -12,14 +12,12 @@ import com.grkj.data.utils.FileStorageUtils
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.FragmentUserInfoBinding
 import com.grkj.iscs.features.main.viewmodel.user_info.UserInfoViewModel
-import com.grkj.data.hardware.face.ArcSoftUtil
+import com.grkj.data.hardware.face.FaceUtil
 import com.grkj.shared.utils.CancellableTimer
 import com.grkj.ui_base.base.BaseFragment
 import com.grkj.ui_base.dialog.TipDialog
 import com.grkj.ui_base.utils.CommonUtils
-import com.grkj.ui_base.utils.event.LogoutEvent
 import com.grkj.ui_base.utils.event.RefreshAvatarEvent
-import com.kongzue.dialogx.dialogs.PopTip
 import com.sik.sikcore.date.TimeUtils
 import com.sik.sikcore.extension.file
 import com.sik.sikcore.extension.setDebouncedClickListener
@@ -39,7 +37,7 @@ class UserInfoFragment : BaseFragment<FragmentUserInfoBinding>() {
     private val captureTimer = CancellableTimer(4000, 1000, {
         binding.countDownTip.text = "${(3000 - it) / 1000}"
     }) {
-        ArcSoftUtil.inDetecting = true
+        FaceUtil.inDetecting = true
         isFaceDetect = true
         binding.previewLayout.visibility = View.INVISIBLE
         binding.image.visibility = View.VISIBLE
@@ -50,7 +48,7 @@ class UserInfoFragment : BaseFragment<FragmentUserInfoBinding>() {
     }
     private val reCaptureTimer = CancellableTimer(2000, 1000, {}) {
         isFaceDetect = false
-        ArcSoftUtil.inDetecting = false
+        FaceUtil.inDetecting = false
         isInCountDown = false
     }
 
@@ -183,8 +181,8 @@ class UserInfoFragment : BaseFragment<FragmentUserInfoBinding>() {
     private fun startFace() {
         binding.previewLayout.isVisible = true
         binding.image.isVisible = false
-        ArcSoftUtil.inDetecting = false
-        ArcSoftUtil.initCamera(
+        FaceUtil.inDetecting = false
+        FaceUtil.initCamera(
             binding.preview,
             binding.faceOverlayView,
             true,
@@ -193,14 +191,14 @@ class UserInfoFragment : BaseFragment<FragmentUserInfoBinding>() {
             logger.info("人脸检测结果: ${bitmap == null},$faceSize,$alive")
             if (faceSize > 1) {
                 binding.tipTv.text = CommonUtils.getStr("only_one_person_allowed")
-                ArcSoftUtil.inDetecting = false
+                FaceUtil.inDetecting = false
                 stopCountDown()
                 return@initCamera
             }
             if (alive == false) {
                 binding.tipTv.text =
                     CommonUtils.getStr("real_person_verification_required")
-                ArcSoftUtil.inDetecting = false
+                FaceUtil.inDetecting = false
                 stopCountDown()
                 return@initCamera
             }
@@ -211,7 +209,7 @@ class UserInfoFragment : BaseFragment<FragmentUserInfoBinding>() {
                 mCapturedBitmap = bitmap
                 binding.image.setImageBitmap(bitmap)
             }
-            ArcSoftUtil.inDetecting = false
+            FaceUtil.inDetecting = false
         }
     }
 
@@ -230,6 +228,6 @@ class UserInfoFragment : BaseFragment<FragmentUserInfoBinding>() {
     }
 
     private fun releaseFace() {
-        ArcSoftUtil.stop()
+        FaceUtil.stop()
     }
 }

+ 2 - 2
iscs_lock/src/main/java/com/grkj/iscs/features/main/viewmodel/user_info/UserInfoViewModel.kt

@@ -9,7 +9,7 @@ import com.grkj.data.domain.vo.FingerprintDataVo
 import com.grkj.data.domain.vo.SysBiometricDataVo
 import com.grkj.data.domain.logic.IHardwareLogic
 import com.grkj.data.domain.logic.IUserLogic
-import com.grkj.data.hardware.face.ArcSoftUtil
+import com.grkj.data.hardware.face.FaceUtil
 import com.grkj.shared.utils.BCryptUtils
 import com.grkj.ui_base.base.BaseViewModel
 import dagger.hilt.android.lifecycle.HiltViewModel
@@ -169,7 +169,7 @@ class UserInfoViewModel @Inject constructor(
 
     fun registerFaceFeature(imageData: String): LiveData<Boolean> {
         return liveData(Dispatchers.IO) {
-            ArcSoftUtil.registerFace(listOf((MainDomainData.userInfo?.userId ?: 0) to imageData))
+//            FaceUtil.registerFace(listOf((MainDomainData.userInfo?.userId ?: 0) to imageData))
             emit(true)
         }
     }

+ 85 - 93
iscs_lock/src/main/java/com/grkj/iscs/features/splash/activity/SplashActivity.kt

@@ -3,6 +3,7 @@ package com.grkj.iscs.features.splash.activity
 import android.content.Intent
 import android.view.Gravity
 import androidx.activity.viewModels
+import androidx.lifecycle.ProcessLifecycleOwner
 import androidx.lifecycle.lifecycleScope
 import com.grkj.data.data.MMKVConstants
 import com.grkj.data.local.database.BackupScheduler
@@ -30,108 +31,98 @@ import com.sik.sikandroid.permission.PermissionUtils
 import com.sik.sikcore.extension.getMMKVData
 import com.sik.sikcore.extension.saveMMKVData
 import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.*
 import java.util.Locale
 
 @AndroidEntryPoint
 class SplashActivity : BaseActivity<ActivitySplashBinding>() {
     private val viewModel: SplashViewModel by viewModels()
-    override fun getLayoutId(): Int {
-        return R.layout.activity_splash
-    }
+
+    override fun getLayoutId(): Int = R.layout.activity_splash
 
     override fun initView() {
-        // 应用启动时按已存配置安排下一次(默认=每天 00:00)
-        GlobalManager.cronJobManager.bindService()
         initConfig()
-        PermissionUtils.checkAndRequestPermissions(Constants.needPermission) {
-            logger.info("授权结果:${it}")
-            if (!it) {
-                PermissionUtils.requestAllFilesAccessPermission {
-                    logger.info("授权结果:${it}")
-                    if (it) {
-                        initDatabase()
-                    }
+
+        PermissionUtils.checkAndRequestPermissions(Constants.needPermission) { granted ->
+            logger.info("授权结果:$granted")
+            if (!granted) {
+                PermissionUtils.requestAllFilesAccessPermission { allFiles ->
+                    logger.info("授权结果:$allFiles")
+                    if (allFiles) routeAndWarmup()  // 先上屏再干活
                 }
-            }else{
-                initDatabase()
+            } else {
+                routeAndWarmup()
             }
         }
-        lifecycleScope.launch {
-            BackupScheduler.applySaved(this@SplashActivity)
-            DbReadyGate.await()
-            val hardwareMode = MMKVConstants.KEY_HARDWARE_MODE.getMMKVData(HardwareMode.CAN.name)
-            if (hardwareMode.isNotEmpty()) {
-                if (hardwareMode.uppercase() == "MODBUS") {
-                    MMKVConstants.KEY_HARDWARE_MODE.saveMMKVData(HardwareMode.RS485.name)
-                }
-                StartListenerEvent.sendStartListenerEvent()
-                HardwareMode.getCurrentHardwareMode()
-                    .connectAndAddListener()
+    }
+
+    /** 先导航上屏;重活丢到进程级生命周期的后台协程里做(不会随 Splash 销毁被取消) */
+    private fun routeAndWarmup() {
+        // 先决定去哪里 → 立刻跳转并结束自己,释放主线程压力
+        val isAppInit = MMKVConstants.APP_INIT.getMMKVData(false)
+        startActivity(Intent(this, if (isAppInit) LoginActivity::class.java else InitActivity::class.java))
+        finish()
+
+        // 后台继续:单并发 IO,避免并行把 GC/JIT 压爆
+        val appScope = ProcessLifecycleOwner.get().lifecycleScope
+        val IO1 = Dispatchers.IO.limitedParallelism(1)
+
+        appScope.launch(IO1) {
+            // 0) 轻量预热 DB(触发构建/迁移/打开)
+            runCatching {
+                val db = ISCSDatabase.instance
+//                db.openHelper.writableDatabase
             }
-            val targetRegion = "US" // 自己决定用什么;也可以是 "CN" / 配置项 / 服务器下发
-            val entries = LanguageRegistry.entriesFromSources(targetRegion)
-            PresetData.targetRegion =
-                entries.find { it.isSelected }?.region ?: targetRegion
-            viewModel.checkPresetData().observe(this@SplashActivity) {
-                viewModel.checkSysMenuAndRole().observe(this@SplashActivity) {
-                    val isAppInit = MMKVConstants.APP_INIT.getMMKVData(false)
-                    if (isAppInit) {
-                        startActivity(Intent(this@SplashActivity, LoginActivity::class.java))
-                        finish()
-                    } else {
-                        startActivity(Intent(this@SplashActivity, InitActivity::class.java))
-                        finish()
+
+            // 1) 备份/恢复
+            runCatching { BackupScheduler.applySaved(this@SplashActivity) }
+
+            // 2) 等待数据库 Ready(若已 ready 会秒过)
+            runCatching { DbReadyGate.await() }
+
+            // 3) 硬件模式与连接(短超时,避免卡 3s)
+            runCatching {
+                val mode = MMKVConstants.KEY_HARDWARE_MODE.getMMKVData(HardwareMode.CAN.name)
+                if (mode.isNotEmpty()) {
+                    if (mode.uppercase() == "MODBUS") {
+                        MMKVConstants.KEY_HARDWARE_MODE.saveMMKVData(HardwareMode.RS485.name)
+                    }
+                    StartListenerEvent.sendStartListenerEvent()
+                    withTimeoutOrNull(800) {
+                        HardwareMode.getCurrentHardwareMode().connectAndAddListener()
                     }
                 }
             }
-        }
-    }
 
-    private fun initDatabase(){
-        // 已有权限的话,直接预热:
-        CoroutineScope(Dispatchers.IO).launch {
-            // 触发构建 + 迁移 + 打开;onOpen 回调里会 DbReadyGate.open()
-            val db = ISCSDatabase.instance
-            //todo 测试用,直接进入,不初始化
-//                            DbReadyGate.await()
-//                            withContext(Dispatchers.Main) {
-//                                val targetRegion = "US" // 自己决定用什么;也可以是 "CN" / 配置项 / 服务器下发
-//                                val entries = LanguageRegistry.entriesFromSources(targetRegion)
-//                                PresetData.targetRegion =
-//                                    entries.find { it.isSelected }?.region ?: targetRegion
-//                                viewModel.checkPresetData().observe(this@SplashActivity) {
-//                                    viewModel.checkSysMenuAndRole().observe(this@SplashActivity) {
-//                                        startActivity(
-//                                            Intent(
-//                                                this@SplashActivity,
-//                                                LoginActivity::class.java
-//                                            )
-//                                        )
-//                                        finish()
-//                                    }
-//                                }
-//                            }
+            // 4) 语言区域(保持你原逻辑)
+            runCatching {
+                val targetRegion = "US"
+                val entries = LanguageRegistry.entriesFromSources(targetRegion)
+                PresetData.targetRegion = entries.find { it.isSelected }?.region ?: targetRegion
+            }
+
+            // 5) 预置数据 & 菜单/角色校验(完成即可,已在目标页上,不阻塞 UI)
+            withContext(Dispatchers.Main.immediate) {
+                viewModel.checkPresetData().observeForever {
+                    viewModel.checkSysMenuAndRole().observeForever {
+                        // 这里无需再导航,已在目标页;如需可发事件通知完成
+                    }
+                }
+            }
         }
     }
 
+    /** 保留你的原 UI 文本配置 */
     fun initConfig() {
         if (LanguageStore.currentMode() == LanguageStore.Mode.FOLLOW_SYSTEM) {
-            val targetRegion = "US" // 自己决定用什么;也可以是 "CN" / 配置项 / 服务器下发
+            val targetRegion = "US"
             val entries = LanguageRegistry.entriesFromSources(targetRegion)
             LanguageStore.setExplicit(
-                Locale.forLanguageTag(
-                    entries.find { it.region == targetRegion }?.tag ?: entries[0].tag
-                )
+                Locale.forLanguageTag(entries.find { it.region == targetRegion }?.tag ?: entries[0].tag)
             )
         }
         val dialogXTextInfo = TextInfo()
-        val dialogXTitleTextInfo = TextInfo().apply {
-            setBold(true)
-            setGravity(Gravity.CENTER)
-        }
+        val dialogXTitleTextInfo = TextInfo().apply { setBold(true); setGravity(Gravity.CENTER) }
         dialogXTextInfo.fontSize = 18
         dialogXTitleTextInfo.fontSize = 22
         DialogX.popTextInfo = dialogXTextInfo
@@ -141,21 +132,22 @@ class SplashActivity : BaseActivity<ActivitySplashBinding>() {
         DialogX.menuTextInfo = dialogXTextInfo
         DialogX.okButtonTextInfo = dialogXTextInfo
         DialogX.titleTextInfo = dialogXTitleTextInfo
-        ClassicsHeader.REFRESH_HEADER_PULLING = I18nManager.t("header_pulling")//"下拉可以刷新";
-        ClassicsHeader.REFRESH_HEADER_REFRESHING = I18nManager.t("header_refreshing")//"正在刷新...";
-        ClassicsHeader.REFRESH_HEADER_LOADING = I18nManager.t("header_loading")//"正在加载...";
-        ClassicsHeader.REFRESH_HEADER_RELEASE = I18nManager.t("header_release")//"释放立即刷新";
-        ClassicsHeader.REFRESH_HEADER_FINISH = I18nManager.t("header_finish")//"刷新完成";
-        ClassicsHeader.REFRESH_HEADER_FAILED = I18nManager.t("header_failed")//"刷新失败";
-        ClassicsHeader.REFRESH_HEADER_UPDATE = I18nManager.t("header_update")//"上次更新 M-d HH:mm";
-        ClassicsHeader.REFRESH_HEADER_SECONDARY = I18nManager.t("header_secondary")//"释放进入二楼"
-
-        ClassicsFooter.REFRESH_FOOTER_PULLING = I18nManager.t("footer_pulling")//"上拉加载更多";
-        ClassicsFooter.REFRESH_FOOTER_RELEASE = I18nManager.t("footer_release")//"释放立即加载";
-        ClassicsFooter.REFRESH_FOOTER_LOADING = I18nManager.t("footer_loading")//"正在刷新...";
-        ClassicsFooter.REFRESH_FOOTER_REFRESHING = I18nManager.t("footer_refreshing")//"正在加载...";
-        ClassicsFooter.REFRESH_FOOTER_FINISH = I18nManager.t("footer_finish")//"加载完成";
-        ClassicsFooter.REFRESH_FOOTER_FAILED = I18nManager.t("footer_failed")//"加载失败";
-        ClassicsFooter.REFRESH_FOOTER_NOTHING = I18nManager.t("footer_nothing")//"全部加载完成";
+
+        ClassicsHeader.REFRESH_HEADER_PULLING = I18nManager.t("header_pulling")
+        ClassicsHeader.REFRESH_HEADER_REFRESHING = I18nManager.t("header_refreshing")
+        ClassicsHeader.REFRESH_HEADER_LOADING = I18nManager.t("header_loading")
+        ClassicsHeader.REFRESH_HEADER_RELEASE = I18nManager.t("header_release")
+        ClassicsHeader.REFRESH_HEADER_FINISH = I18nManager.t("header_finish")
+        ClassicsHeader.REFRESH_HEADER_FAILED = I18nManager.t("header_failed")
+        ClassicsHeader.REFRESH_HEADER_UPDATE = I18nManager.t("header_update")
+        ClassicsHeader.REFRESH_HEADER_SECONDARY = I18nManager.t("header_secondary")
+
+        ClassicsFooter.REFRESH_FOOTER_PULLING = I18nManager.t("footer_pulling")
+        ClassicsFooter.REFRESH_FOOTER_RELEASE = I18nManager.t("footer_release")
+        ClassicsFooter.REFRESH_FOOTER_LOADING = I18nManager.t("footer_loading")
+        ClassicsFooter.REFRESH_FOOTER_REFRESHING = I18nManager.t("footer_refreshing")
+        ClassicsFooter.REFRESH_FOOTER_FINISH = I18nManager.t("footer_finish")
+        ClassicsFooter.REFRESH_FOOTER_FAILED = I18nManager.t("footer_failed")
+        ClassicsFooter.REFRESH_FOOTER_NOTHING = I18nManager.t("footer_nothing")
     }
-}
+}

+ 26 - 1
iscs_lock/src/main/res/drawable/icon_add.xml

@@ -7,6 +7,31 @@
 
     <path
         android:fillColor="#0078E8"
-        android:pathData="M512,51.2c-254,0 -460.8,206.8 -460.8,460.8s206.8,460.8 460.8,460.8S972.8,766 972.8,512s-206.8,-460.8 -460.8,-460.8zM747.5,532.5h-204.8v204.8c0,12.3 -8.2,20.5 -20.5,20.5s-20.5,-8.2 -20.5,-20.5v-204.8h-204.8c-12.3,0 -20.5,-8.2 -20.5,-20.5s8.2,-20.5 20.5,-20.5h204.8v-204.8c0,-12.3 8.2,-20.5 20.5,-20.5s20.5,8.2 20.5,20.5v204.8h204.8c12.3,0 20.5,8.2 20.5,20.5s-8.2,20.5 -20.5,20.5z" />
+        android:pathData="M512,51.2
+        c-254,0 -460.8,206.8 -460.8,460.8
+        s206.8,460.8 460.8,460.8
+        S972.8,766 972.8,512
+        s-206.8,-460.8 -460.8,-460.8z" />
+    <path
+        android:fillColor="#FFFFFF"
+        android:tint="@null"
+        android:pathData="
+        M747.5,532.5
+        h-204.8v
+        204.8
+        c0,12.3 -8.2,20.5 -20.5,20.5
+        s-20.5,-8.2 -20.5,-20.5
+        v-204.8
+        h-204.8
+        c-12.3,0 -20.5,-8.2 -20.5,-20.5
+        s8.2,-20.5 20.5,-20.5
+        h204.8
+        v-204.8
+        c0,-12.3 8.2,-20.5 20.5,-20.5
+        s20.5,8.2 20.5,20.5
+        v204.8
+        h204.8
+        c12.3,0 20.5,8.2 20.5,20.5
+        s-8.2,20.5 -20.5,20.5z" />
 
 </vector>

+ 1 - 1
iscs_lock/src/main/res/layout/item_home_text_drop_down.xml

@@ -19,7 +19,7 @@
         <TextView
             android:id="@+id/drop_down_text"
             android:layout_width="0dp"
-            android:layout_height="match_parent"
+            android:layout_height="wrap_content"
             android:layout_weight="1"
             android:gravity="center_vertical"
             android:paddingHorizontal="@dimen/iscs_space_2"

+ 2 - 3
iscs_lock/src/main/res/layout/item_locker_group.xml

@@ -4,8 +4,7 @@
 
     <FrameLayout
         android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:padding="@dimen/iscs_space_2">
+        android:layout_height="wrap_content">
 
         <LinearLayout
             android:id="@+id/group_layout"
@@ -47,7 +46,7 @@
             <androidx.recyclerview.widget.RecyclerView
                 android:id="@+id/group_locker_rv"
                 android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
+                android:layout_height="match_parent"
                 android:layout_gravity="center"
                 android:minHeight="@dimen/locker_item_min_height"
                 android:paddingBottom="@dimen/iscs_space_2" />

+ 1 - 0
iscs_lock/src/main/res/layout/item_point_group.xml

@@ -76,6 +76,7 @@
             android:id="@+id/group_point_rv"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
+            android:minHeight="@dimen/common_rv_min_height"
             android:paddingBottom="@dimen/iscs_space_2" />
     </LinearLayout>
 </layout>

+ 10 - 7
iscs_mc/src/main/java/com/grkj/iscs_mc/ISCSMCApplication.kt

@@ -12,25 +12,23 @@ import coil.ImageLoader
 import coil.decode.SvgDecoder
 import coil.memory.MemoryCache
 import com.drake.statelayout.StateConfig
-import com.google.android.material.color.DynamicColors
 import com.grkj.data.config.ISCSConfig
 import com.grkj.data.data.EventConstants
 import com.grkj.data.di.LogicManager
-import com.grkj.data.enums.HardwareMode
-import com.grkj.data.enums.RFIDScanMode
 import com.grkj.data.hardware.ble.BleUtil
 import com.grkj.data.hardware.modbus.ModBusController
 import com.grkj.data.local.database.DbReadyGate
 import com.grkj.data.local.database.ISCSDatabase
 import com.grkj.iscs_mc.features.splash.activity.SplashActivity
 import com.grkj.shared.model.EventBean
-import com.grkj.data.hardware.face.ArcSoftUtil
+import com.grkj.data.hardware.face.FaceUtil
+import com.grkj.data.hardware.face.hlk.Hlk223Client
+import com.grkj.data.hardware.face.hlk.Hlk223Config
 import com.grkj.shared.utils.i18n.I18nManager
 import com.grkj.shared.utils.i18n.LanguageCatalog
 import com.grkj.shared.utils.i18n.LanguageStore
 import com.grkj.shared.utils.i18n.source.XmlResourcesI18nSource
 import com.grkj.ui_base.business.HardwareBusinessManager
-import com.grkj.ui_base.skin.SkinManager
 import com.grkj.ui_base.utils.CommonUtils
 import com.kongzue.dialogx.DialogX
 import com.scwang.smart.refresh.footer.ClassicsFooter
@@ -93,9 +91,14 @@ class ISCSMCApplication : Application() {
         if (ISCSConfig.isInit) {
             BleUtil.instance?.initBle(this)
         }
+
+        // ② 建一个 HLK 客户端(串口或你现有的 Modbus 封装)
+        val hlk = Hlk223Client(Hlk223Config.getProtocol(), "HLK-223")
+        // ③ 想切到 HLK 路线(但保持对外 API 不变)
+        FaceUtil.enableHlkBackend(hlk)
         //todo 模拟器不支持 测试用,直接创建管理员账号
-        ArcSoftUtil.checkActiveStatus(SIKCore.getApplication())
-        ArcSoftUtil.initEngine(SIKCore.getApplication())
+        FaceUtil.checkActiveStatus(SIKCore.getApplication())
+        FaceUtil.initEngine(SIKCore.getApplication())
         AutoSizeConfig.getInstance().isCustomFragment = false
         StateConfig.emptyLayout = com.grkj.ui_base.R.layout.layout_empty
         ThreadUtils.runOnIO {

+ 8 - 9
iscs_mc/src/main/java/com/grkj/iscs_mc/features/login/dialog/LoginDialog.kt

@@ -7,8 +7,8 @@ import com.grkj.data.enums.LoginResultEnum
 import com.grkj.iscs_mc.R
 import com.grkj.iscs_mc.databinding.DialogLoginBinding
 import com.grkj.iscs_mc.features.login.viewmodel.LoginViewModel
-import com.grkj.data.hardware.face.ArcSoftUtil
-import com.grkj.data.hardware.face.ArcSoftUtil.inDetecting
+import com.grkj.data.hardware.face.FaceUtil
+import com.grkj.data.hardware.face.FaceUtil.inDetecting
 import com.grkj.ui_base.skin.loadSkinIcon
 import com.grkj.ui_base.utils.CommonUtils
 import com.grkj.data.utils.event.LoadingEvent
@@ -52,7 +52,7 @@ class LoginDialog(
         customDialog?.setMaskColor(CommonUtils.getColor(com.grkj.skin.R.attr.scrim))
         customDialog.setDialogLifecycleCallback(object : DialogLifecycleCallback<CustomDialog>() {
             override fun onDismiss(dialog: CustomDialog?) {
-                ArcSoftUtil.stop()
+                FaceUtil.stop()
                 super.onDismiss(dialog)
             }
         })
@@ -78,7 +78,7 @@ class LoginDialog(
             override fun onDismiss(dialog: CustomDialog?) {
                 when (mLoginType) {
                     0 -> {
-                        ArcSoftUtil.stop()
+                        FaceUtil.stop()
                     }
 
                     1 -> {
@@ -104,7 +104,7 @@ class LoginDialog(
             mBinding.tvTip.text = mPairList[mLoginType].first
             when (mLoginType) {
                 0 -> {
-                    if (!ArcSoftUtil.isActivated) {
+                    if (!FaceUtil.isActivated) {
                         PopTip.tip(
                             CommonUtils.getStr("face_can_not_process").toString()
                         )
@@ -172,8 +172,7 @@ class LoginDialog(
     private fun startFace() {
         inDetecting = false
         ActivityTracker.getCurrentActivity()?.let { context ->
-            ArcSoftUtil.checkCamera(
-                context.windowManager, mBinding.preview!!
+            FaceUtil.checkCamera(mBinding.preview!!
             ) { bitmap, userId ->
                 viewModel.loginWithUserId(userId).observe(lifecycleOwner) {
                     if (it == LoginResultEnum.FACE_VERIFY_FAILED) {
@@ -185,12 +184,12 @@ class LoginDialog(
                                         inFaceChecking = false
                                     }
                                 } else {
-                                    ArcSoftUtil.stop()
+                                    FaceUtil.stop()
                                 }
                                 callBack?.invoke(it)
                             }
                     } else {
-                        ArcSoftUtil.stop()
+                        FaceUtil.stop()
                         callBack?.invoke(it)
                     }
                 }

+ 7 - 9
iscs_mc/src/main/java/com/grkj/iscs_mc/features/login/fragment/LoginFragment.kt

@@ -24,7 +24,6 @@ import com.grkj.iscs_mc.R
 import com.grkj.iscs_mc.databinding.FragmentLoginBinding
 import com.grkj.iscs_mc.databinding.ItemLoginMethodBinding
 import com.grkj.iscs_mc.features.login.dialog.ChangeLangDialog
-import com.grkj.iscs_mc.features.login.dialog.LoginDialog
 import com.grkj.iscs_mc.features.login.entity.LoginMenuEntity
 import com.grkj.iscs_mc.features.login.viewmodel.LoginViewModel
 import com.grkj.iscs_mc.features.main.activity.MainActivity
@@ -38,8 +37,8 @@ import com.grkj.ui_base.skin.loadSkinIcon
 import com.grkj.ui_base.utils.CommonUtils
 import com.grkj.ui_base.utils.changeBgTint
 import com.grkj.data.utils.event.LoadingEvent
-import com.grkj.data.hardware.face.ArcSoftUtil
-import com.grkj.data.hardware.face.ArcSoftUtil.inDetecting
+import com.grkj.data.hardware.face.FaceUtil
+import com.grkj.data.hardware.face.FaceUtil.inDetecting
 import com.grkj.ui_base.utils.event.RFIDCardReadEvent
 import com.grkj.ui_base.utils.extension.getAppVersionName
 import com.grkj.ui_base.utils.extension.serialNo
@@ -146,7 +145,7 @@ class LoginFragment : BaseFragment<FragmentLoginBinding>() {
         }
         binding.modeTip.isVisible = false
         binding.tvCancel.setDebouncedClickListener {
-            ArcSoftUtil.stop()
+            FaceUtil.stop()
             hideLogin()
         }
         binding.tvLogin.setDebouncedClickListener {
@@ -289,7 +288,7 @@ class LoginFragment : BaseFragment<FragmentLoginBinding>() {
         } else if (loginType == 0) {
             binding.loginTip.text = CommonUtils.getStr(com.grkj.ui_base.R.string.face_login_tip)
             binding.ivIcon.loadSkinIcon("face-id-svgrepo-com.svg")
-            if (!ArcSoftUtil.isActivated) {
+            if (!FaceUtil.isActivated) {
                 PopTip.tip(
                     CommonUtils.getStr(com.grkj.ui_base.R.string.face_not_activated)
                 )
@@ -307,8 +306,7 @@ class LoginFragment : BaseFragment<FragmentLoginBinding>() {
     private fun startFace() {
         inDetecting = false
         ActivityTracker.getCurrentActivity()?.let { context ->
-            ArcSoftUtil.checkCamera(
-                context.windowManager, binding.preview
+            FaceUtil.checkCamera(binding.preview
             ) { bitmap, userId ->
                 viewModel.loginWithUserId(userId).observe(this) {
                     if (it == LoginResultEnum.FACE_VERIFY_FAILED) {
@@ -320,13 +318,13 @@ class LoginFragment : BaseFragment<FragmentLoginBinding>() {
                                         inFaceChecking = false
                                     }
                                 } else {
-                                    ArcSoftUtil.stop()
+                                    FaceUtil.stop()
                                     hideLoading()
                                     checkLoginResult(it)
                                 }
                             }
                     } else {
-                        ArcSoftUtil.stop()
+                        FaceUtil.stop()
                         checkLoginResult(it)
                     }
                 }

+ 2 - 2
iscs_mc/src/main/java/com/grkj/iscs_mc/features/login/viewmodel/LoginViewModel.kt

@@ -6,7 +6,7 @@ import com.grkj.data.domain.logic.IUserLogic
 import com.grkj.data.enums.LoginResultEnum
 import com.grkj.data.hardware.can.CanCommands
 import com.grkj.data.hardware.can.CanHelper
-import com.grkj.data.hardware.face.ArcSoftUtil
+import com.grkj.data.hardware.face.FaceUtil
 import com.grkj.ui_base.base.BaseViewModel
 import com.sik.sikcore.extension.file
 import dagger.hilt.android.lifecycle.HiltViewModel
@@ -105,7 +105,7 @@ class LoginViewModel @Inject constructor(
             val user = userLogic.getAllFaceData()
             val userFaceData = user.filter { it.content.file().exists() }
                 .map { it.userId to it.content.file().readText() }
-            ArcSoftUtil.registerFace(userFaceData)
+//            FaceUtil.registerFace(userFaceData)
             emit(true)
         }
     }

+ 9 - 10
iscs_mc/src/main/java/com/grkj/iscs_mc/features/main/dialog/data_manage/RegisterFaceDialog.kt

@@ -1,7 +1,6 @@
 package com.grkj.iscs_mc.features.main.dialog.data_manage
 
 import android.graphics.Bitmap
-import android.view.Gravity
 import android.view.View
 import androidx.core.view.isVisible
 import com.grkj.data.data.CommonConstants
@@ -9,7 +8,7 @@ import com.grkj.data.data.MainDomainData
 import com.grkj.data.utils.FileStorageUtils
 import com.grkj.iscs_mc.R
 import com.grkj.iscs_mc.databinding.DialogRegisterFaceBinding
-import com.grkj.data.hardware.face.ArcSoftUtil
+import com.grkj.data.hardware.face.FaceUtil
 import com.grkj.shared.utils.CancellableTimer
 import com.grkj.ui_base.utils.CommonUtils
 import com.kongzue.dialogx.dialogs.CustomDialog
@@ -33,7 +32,7 @@ class RegisterFaceDialog(private val onConfirm: (String, String) -> Unit) :
     private val captureTimer = CancellableTimer(4000, 1000, {
         binding.countDownTip.text = "${(3000 - it) / 1000}"
     }) {
-        ArcSoftUtil.inDetecting = true
+        FaceUtil.inDetecting = true
         isFaceDetect = true
         binding.previewLayout.visibility = View.INVISIBLE
         binding.image.visibility = View.VISIBLE
@@ -44,7 +43,7 @@ class RegisterFaceDialog(private val onConfirm: (String, String) -> Unit) :
     }
     private val reCaptureTimer = CancellableTimer(2000, 1000, {}) {
         isFaceDetect = false
-        ArcSoftUtil.inDetecting = false
+        FaceUtil.inDetecting = false
         isInCountDown = false
     }
 
@@ -89,8 +88,8 @@ class RegisterFaceDialog(private val onConfirm: (String, String) -> Unit) :
     private fun startFace() {
         binding.previewLayout.isVisible = true
         binding.image.isVisible = false
-        ArcSoftUtil.inDetecting = false
-        ArcSoftUtil.initCamera(
+        FaceUtil.inDetecting = false
+        FaceUtil.initCamera(
             binding.preview,
             binding.faceOverlayView,
             true,
@@ -99,14 +98,14 @@ class RegisterFaceDialog(private val onConfirm: (String, String) -> Unit) :
             logger.info("人脸检测结果: ${bitmap == null},$faceSize,$alive")
             if (faceSize > 1) {
                 binding.tipTv.text = CommonUtils.getStr("only_one_person_allowed")
-                ArcSoftUtil.inDetecting = false
+                FaceUtil.inDetecting = false
                 stopCountDown()
                 return@initCamera
             }
             if (alive == false) {
                 binding.tipTv.text =
                     CommonUtils.getStr("real_person_verification_required")
-                ArcSoftUtil.inDetecting = false
+                FaceUtil.inDetecting = false
                 stopCountDown()
                 return@initCamera
             }
@@ -117,7 +116,7 @@ class RegisterFaceDialog(private val onConfirm: (String, String) -> Unit) :
                 mCapturedBitmap = bitmap
                 binding.image.setImageBitmap(bitmap)
             }
-            ArcSoftUtil.inDetecting = false
+            FaceUtil.inDetecting = false
         }
     }
 
@@ -136,7 +135,7 @@ class RegisterFaceDialog(private val onConfirm: (String, String) -> Unit) :
     }
 
     private fun releaseFace() {
-        ArcSoftUtil.stop()
+        FaceUtil.stop()
     }
 
     companion object {

+ 4 - 0
iscs_mc/src/main/java/com/grkj/iscs_mc/features/main/fragment/data_manage/UserManageFragment.kt

@@ -378,6 +378,7 @@ class UserManageFragment : BaseFragment<FragmentUserManageBinding>() {
                         mFingerprintPressTimes++
                         if (mFingerprintPressTimes == maxPressTimes) {
                             dialog.dismiss()
+                            hideLoading()
                             showToast(CommonUtils.getStr("fingerprint_add_success_tip"))
                             registerResult(mFingerprintGroupName)
                         } else if (mFingerprintInputErrorTimes == inputFingerprintErrorTimes) {
@@ -392,12 +393,14 @@ class UserManageFragment : BaseFragment<FragmentUserManageBinding>() {
                                 .observe(this@UserManageFragment) {
                                     getUserData(false)
                                 }
+                            hideLoading()
                             showToast(CommonUtils.getStr("please_re_press_fingerprint_again"))
                         } else {
                             pressTip?.text = CommonUtils.getStr(
                                 "fingerprint_scan_tip",
                                 maxPressTimes - mFingerprintPressTimes
                             )
+                            hideLoading()
                             showToast(CommonUtils.getStr("please_press_fingerprint_again"))
                         }
                     } else {
@@ -414,6 +417,7 @@ class UserManageFragment : BaseFragment<FragmentUserManageBinding>() {
                                 .observe(this@UserManageFragment) {
                                     getUserData(false)
                                 }
+                            hideLoading()
                             showToast(CommonUtils.getStr("please_re_press_fingerprint_again"))
                         }
                     }

+ 9 - 9
iscs_mc/src/main/java/com/grkj/iscs_mc/features/main/fragment/user_info/SetFaceFragment.kt

@@ -11,7 +11,7 @@ import com.grkj.data.utils.FileStorageUtils
 import com.grkj.iscs_mc.R
 import com.grkj.iscs_mc.databinding.FragmentSetFaceBinding
 import com.grkj.iscs_mc.features.main.viewmodel.user_info.UserInfoViewModel
-import com.grkj.data.hardware.face.ArcSoftUtil
+import com.grkj.data.hardware.face.FaceUtil
 import com.grkj.shared.utils.CancellableTimer
 import com.grkj.ui_base.base.BaseFragment
 import com.grkj.ui_base.utils.CommonUtils
@@ -35,7 +35,7 @@ class SetFaceFragment : BaseFragment<FragmentSetFaceBinding>() {
     private val captureTimer = CancellableTimer(4000, 1000, {
         binding.countDownTip.text = "${(3000 - it) / 1000}"
     }) {
-        ArcSoftUtil.inDetecting = true
+        FaceUtil.inDetecting = true
         isFaceDetect = true
         binding.previewLayout.visibility = View.INVISIBLE
         binding.image.visibility = View.VISIBLE
@@ -46,7 +46,7 @@ class SetFaceFragment : BaseFragment<FragmentSetFaceBinding>() {
     }
     private val reCaptureTimer = CancellableTimer(2000, 1000, {}) {
         isFaceDetect = false
-        ArcSoftUtil.inDetecting = false
+        FaceUtil.inDetecting = false
         isInCountDown = false
     }
 
@@ -144,8 +144,8 @@ class SetFaceFragment : BaseFragment<FragmentSetFaceBinding>() {
     private fun startFace() {
         binding.previewLayout.isVisible = true
         binding.image.isVisible = false
-        ArcSoftUtil.inDetecting = false
-        ArcSoftUtil.initCamera(
+        FaceUtil.inDetecting = false
+        FaceUtil.initCamera(
             binding.preview,
             binding.faceOverlayView,
             true,
@@ -154,14 +154,14 @@ class SetFaceFragment : BaseFragment<FragmentSetFaceBinding>() {
             logger.info("人脸检测结果: ${bitmap == null},$faceSize,$alive")
             if (faceSize > 1) {
                 binding.tipTv.text = CommonUtils.getStr("only_one_person_allowed")
-                ArcSoftUtil.inDetecting = false
+                FaceUtil.inDetecting = false
                 stopCountDown()
                 return@initCamera
             }
             if (alive == false) {
                 binding.tipTv.text =
                     CommonUtils.getStr("real_person_verification_required")
-                ArcSoftUtil.inDetecting = false
+                FaceUtil.inDetecting = false
                 stopCountDown()
                 return@initCamera
             }
@@ -172,7 +172,7 @@ class SetFaceFragment : BaseFragment<FragmentSetFaceBinding>() {
                 mCapturedBitmap = bitmap
                 binding.image.setImageBitmap(bitmap)
             }
-            ArcSoftUtil.inDetecting = false
+            FaceUtil.inDetecting = false
         }
     }
 
@@ -191,7 +191,7 @@ class SetFaceFragment : BaseFragment<FragmentSetFaceBinding>() {
     }
 
     private fun releaseFace() {
-        ArcSoftUtil.stop()
+        FaceUtil.stop()
     }
 
 }

+ 4 - 0
iscs_mc/src/main/java/com/grkj/iscs_mc/features/main/fragment/user_info/SetFingerprintFragment.kt

@@ -173,6 +173,7 @@ class SetFingerprintFragment : BaseFragment<FragmentSetFingerprintBinding>() {
                         mFingerprintPressTimes++
                         if (mFingerprintPressTimes == maxPressTimes) {
                             dialog?.dismiss()
+                            hideLoading()
                             showToast(CommonUtils.getStr("fingerprint_add_success_tip"))
                             getData()
                         } else if (mFingerprintInputErrorTimes == inputFingerprintErrorTimes) {
@@ -187,12 +188,14 @@ class SetFingerprintFragment : BaseFragment<FragmentSetFingerprintBinding>() {
                                 .observe(this@SetFingerprintFragment) {
                                     getData()
                                 }
+                            hideLoading()
                             showToast(CommonUtils.getStr("please_re_press_fingerprint_again"))
                         } else {
                             pressTip?.text = CommonUtils.getStr(
                                 "fingerprint_scan_tip",
                                 maxPressTimes - mFingerprintPressTimes
                             )
+                            hideLoading()
                             showToast(CommonUtils.getStr("please_press_fingerprint_again"))
                         }
                     } else {
@@ -209,6 +212,7 @@ class SetFingerprintFragment : BaseFragment<FragmentSetFingerprintBinding>() {
                                 .observe(this@SetFingerprintFragment) {
                                     getData()
                                 }
+                            hideLoading()
                             showToast(CommonUtils.getStr("please_re_press_fingerprint_again"))
                         }
                     }

+ 9 - 11
iscs_mc/src/main/java/com/grkj/iscs_mc/features/main/fragment/user_info/UserInfoFragment.kt

@@ -12,13 +12,11 @@ import com.grkj.data.utils.FileStorageUtils
 import com.grkj.iscs_mc.R
 import com.grkj.iscs_mc.databinding.FragmentUserInfoBinding
 import com.grkj.iscs_mc.features.main.viewmodel.user_info.UserInfoViewModel
-import com.grkj.data.hardware.face.ArcSoftUtil
+import com.grkj.data.hardware.face.FaceUtil
 import com.grkj.shared.utils.CancellableTimer
 import com.grkj.ui_base.base.BaseFragment
 import com.grkj.ui_base.dialog.TipDialog
 import com.grkj.ui_base.utils.CommonUtils
-import com.grkj.ui_base.utils.event.LogoutEvent
-import com.kongzue.dialogx.dialogs.PopTip
 import com.sik.sikcore.date.TimeUtils
 import com.sik.sikcore.extension.file
 import com.sik.sikcore.extension.setDebouncedClickListener
@@ -38,7 +36,7 @@ class UserInfoFragment : BaseFragment<FragmentUserInfoBinding>() {
     private val captureTimer = CancellableTimer(4000, 1000, {
         binding.countDownTip.text = "${(3000 - it) / 1000}"
     }) {
-        ArcSoftUtil.inDetecting = true
+        FaceUtil.inDetecting = true
         isFaceDetect = true
         binding.previewLayout.visibility = View.INVISIBLE
         binding.image.visibility = View.VISIBLE
@@ -49,7 +47,7 @@ class UserInfoFragment : BaseFragment<FragmentUserInfoBinding>() {
     }
     private val reCaptureTimer = CancellableTimer(2000, 1000, {}) {
         isFaceDetect = false
-        ArcSoftUtil.inDetecting = false
+        FaceUtil.inDetecting = false
         isInCountDown = false
     }
 
@@ -181,8 +179,8 @@ class UserInfoFragment : BaseFragment<FragmentUserInfoBinding>() {
     private fun startFace() {
         binding.previewLayout.isVisible = true
         binding.image.isVisible = false
-        ArcSoftUtil.inDetecting = false
-        ArcSoftUtil.initCamera(
+        FaceUtil.inDetecting = false
+        FaceUtil.initCamera(
             binding.preview,
             binding.faceOverlayView,
             true,
@@ -191,14 +189,14 @@ class UserInfoFragment : BaseFragment<FragmentUserInfoBinding>() {
             logger.info("人脸检测结果: ${bitmap == null},$faceSize,$alive")
             if (faceSize > 1) {
                 binding.tipTv.text = CommonUtils.getStr("only_one_person_allowed")
-                ArcSoftUtil.inDetecting = false
+                FaceUtil.inDetecting = false
                 stopCountDown()
                 return@initCamera
             }
             if (alive == false) {
                 binding.tipTv.text =
                     CommonUtils.getStr("real_person_verification_required")
-                ArcSoftUtil.inDetecting = false
+                FaceUtil.inDetecting = false
                 stopCountDown()
                 return@initCamera
             }
@@ -209,7 +207,7 @@ class UserInfoFragment : BaseFragment<FragmentUserInfoBinding>() {
                 mCapturedBitmap = bitmap
                 binding.image.setImageBitmap(bitmap)
             }
-            ArcSoftUtil.inDetecting = false
+            FaceUtil.inDetecting = false
         }
     }
 
@@ -228,6 +226,6 @@ class UserInfoFragment : BaseFragment<FragmentUserInfoBinding>() {
     }
 
     private fun releaseFace() {
-        ArcSoftUtil.stop()
+        FaceUtil.stop()
     }
 }

+ 2 - 2
iscs_mc/src/main/java/com/grkj/iscs_mc/features/main/viewmodel/user_info/UserInfoViewModel.kt

@@ -9,7 +9,7 @@ import com.grkj.data.domain.vo.FingerprintDataVo
 import com.grkj.data.domain.vo.SysBiometricDataVo
 import com.grkj.data.local.dos.IsJobCard
 import com.grkj.data.local.dos.SysUserCharacteristicDo
-import com.grkj.data.hardware.face.ArcSoftUtil
+import com.grkj.data.hardware.face.FaceUtil
 import com.grkj.shared.utils.BCryptUtils
 import com.grkj.ui_base.base.BaseViewModel
 import dagger.hilt.android.lifecycle.HiltViewModel
@@ -169,7 +169,7 @@ class UserInfoViewModel @Inject constructor(
 
     fun registerFaceFeature(imageData: String): LiveData<Boolean> {
         return liveData(Dispatchers.IO) {
-            ArcSoftUtil.registerFace(listOf((MainDomainData.userInfo?.userId ?: 0) to imageData))
+//            FaceUtil.registerFace(listOf((MainDomainData.userInfo?.userId ?: 0) to imageData))
             emit(true)
         }
     }

+ 1 - 0
shared/build.gradle.kts

@@ -63,6 +63,7 @@ dependencies {
     implementation("com.google.dagger:hilt-android:2.56.2")
     ksp("com.google.dagger:hilt-android-compiler:2.56.2")
     api("com.github.SilverIceKey:SIKCronJob:1.0.5")
+    api("com.google.mlkit:face-detection:16.1.7")
     testImplementation(libs.junit)
     api(
         fileTree(

+ 22 - 0
shared/src/main/java/com/grkj/shared/utils/Crc32Utils.kt

@@ -0,0 +1,22 @@
+package com.grkj.shared.utils
+
+object Crc32Utils {
+    private val table = IntArray(256).apply {
+        for (i in indices) {
+            var c = i
+            repeat(8) {
+                c = if ((c and 1) != 0) 0xEDB88320.toInt() xor (c ushr 1) else c ushr 1
+            }
+            this[i] = c
+        }
+    }
+
+    fun compute(data: ByteArray): Int {
+        var crc = -1
+        for (b in data) {
+            val idx = (crc xor (b.toInt() and 0xFF)) and 0xFF
+            crc = table[idx] xor (crc ushr 8)
+        }
+        return crc.inv()
+    }
+}

+ 96 - 0
shared/src/main/java/com/grkj/shared/utils/ImageCompress.kt

@@ -0,0 +1,96 @@
+package com.grkj.shared.utils
+
+object ImageCompress {
+
+    /**
+     * 把任意 Base64 图片压成 <= targetBytes 的 JPEG 字节数组。
+     * 先质量二分,仍超限再按比例缩放并重试,直到满足或触底。
+     */
+    fun base64ToJpegUnder(
+        base64: String,
+        targetBytes: Int,
+        minQuality: Int = 0,         // 质量下限
+        startQuality: Int = 92,       // 初始质量
+        minSide: Int = 50            // 缩放触底,避免过小
+    ): ByteArray {
+        require(targetBytes > 0) { "targetBytes must > 0" }
+
+        val clean = base64.substringAfter(",") // 去 data uri 前缀
+        val raw = android.util.Base64.decode(clean, android.util.Base64.DEFAULT)
+
+        // 任意格式解码成 Bitmap
+        val src = android.graphics.BitmapFactory.decodeByteArray(raw, 0, raw.size)
+            ?: error("Base64 不是有效图片")
+
+        // PNG 可能带透明通道,JPEG 不支持:铺白底去 alpha
+        val bmp = if (src.hasAlpha()) {
+            val out = android.graphics.Bitmap.createBitmap(src.width, src.height, android.graphics.Bitmap.Config.ARGB_8888)
+            val c = android.graphics.Canvas(out)
+            c.drawColor(android.graphics.Color.WHITE)
+            c.drawBitmap(src, 0f, 0f, null)
+            out
+        } else src
+
+        try {
+            // 先只做质量二分
+            var jpeg = compressWithQualityBinarySearch(bmp, targetBytes, startQuality, minQuality)
+            if (jpeg.size <= targetBytes) return jpeg
+
+            // 还超:按比例缩放后再二分压缩,循环直到满足或触底
+            var cur = bmp
+            while (jpeg.size > targetBytes) {
+                // 估算缩放比例(乘个 0.95 留余量)
+                val ratio = kotlin.math.sqrt(targetBytes.toDouble() / jpeg.size.toDouble()) * 0.95
+                if (ratio >= 0.999) break
+                val newW = (cur.width * ratio).toInt().coerceAtLeast(minSide)
+                val newH = (cur.height * ratio).toInt().coerceAtLeast(minSide)
+                if (newW == cur.width || newH == cur.height) break
+
+                val scaled = android.graphics.Bitmap.createScaledBitmap(cur, newW, newH, true)
+                if (scaled != cur && cur !== bmp) cur.recycle()
+                cur = scaled
+
+                jpeg = compressWithQualityBinarySearch(cur, targetBytes, startQuality, minQuality)
+                if (newW <= minSide || newH <= minSide) break
+            }
+            return jpeg
+        } finally {
+            if (bmp !== src) bmp.recycle()
+            src.recycle()
+        }
+    }
+
+    private fun compressWithQualityBinarySearch(
+        bmp: android.graphics.Bitmap,
+        targetBytes: Int,
+        startQ: Int,
+        minQ: Int
+    ): ByteArray {
+        var lo = minQ
+        var hi = 100
+        var best = ByteArray(0)
+
+        // 先用 startQ 试一次
+        best = jpegBytes(bmp, startQ)
+        if (best.size <= targetBytes) return best
+
+        // 二分:找 <= target 的最大质量
+        while (lo <= hi) {
+            val mid = (lo + hi) / 2
+            val bytes = jpegBytes(bmp, mid)
+            if (bytes.size > targetBytes) {
+                hi = mid - 1
+            } else {
+                best = bytes
+                lo = mid + 1
+            }
+        }
+        return if (best.isNotEmpty()) best else jpegBytes(bmp, minQ)
+    }
+
+    private fun jpegBytes(bmp: android.graphics.Bitmap, quality: Int): ByteArray {
+        val baos = java.io.ByteArrayOutputStream()
+        bmp.compress(android.graphics.Bitmap.CompressFormat.JPEG, quality.coerceIn(1,100), baos)
+        return baos.toByteArray()
+    }
+}

+ 4 - 0
shared/src/main/java/com/grkj/shared/widget/FaceOverlayView.kt

@@ -52,6 +52,10 @@ class FaceOverlayView @JvmOverloads constructor(
         invalidate()
     }
 
+    // 在 FaceOverlayView 增加:
+    fun lastRectsCount(): Int = faceRectF?.size ?: 0
+
+
     override fun onDraw(canvas: Canvas) {
         super.onDraw(canvas)
         val rects = faceRectF ?: return

+ 6 - 1
ui-base/src/main/java/com/grkj/ui_base/base/BaseActivity.kt

@@ -146,12 +146,17 @@ abstract class BaseActivity<V : ViewDataBinding> : AppCompatActivity(), CustomAd
 
             EventConstants.EVENT_JUMP_TO -> {
                 (event.data as JumpViewEvent).apply {
-                    graphMap?.filter { it.value == navGraphId }?.firstNotNullOf {
+                    graphMap?.entries?.firstOrNull { it.value == navGraphId }?.let {
                         navBar?.menu?.findItem(it.key)?.let {
                             navBar?.selectedItemId = it.itemId
                             navController.navigate(targetId)
                             MainDomainData.fromQuickEntry = true
                         }
+                    } ?: run {
+                        navBar?.clearSelected()
+                        replaceNavGraph(navGraphId)
+                        navController.navigate(targetId)
+                        MainDomainData.fromQuickEntry = true
                     }
                 }
             }

+ 2 - 2
ui-base/src/main/java/com/grkj/ui_base/utils/event/InRFIDScanModeEvent.kt

@@ -14,8 +14,8 @@ class InRFIDScanModeEvent(val inRfidScanMode: Boolean = true) {
          * 发送是否进入扫描模式事件
          */
         @JvmStatic
-        fun sendInRFIDScanModeEvent(show: Boolean = true) {
-            val inRfidScanModeEvent = InRFIDScanModeEvent(show)
+        fun sendInRFIDScanModeEvent(inRfidScanMode: Boolean = true) {
+            val inRfidScanModeEvent = InRFIDScanModeEvent(inRfidScanMode)
             val bottomNavVisibilityEventBean = EventBean<InRFIDScanModeEvent>(
                 EventConstants.EVENT_IN_RFID_SCAN_MODE, inRfidScanModeEvent
             )