Compare commits

...
8 Commits
Author SHA1 Message Date
lvmeng 5047712198 提交历史代码 2026-03-18 14:32:26 +08:00
lvmeng 3768bd94f2 识别成功未提示及采集时间长问题优化 2026-02-28 10:03:04 +08:00
lvmeng dcfb684b66 优化 2026-02-05 15:06:00 +08:00
lvmeng e5940605c1 优化 2026-02-05 08:39:26 +08:00
lvmeng 96e795c749 优化 2026-02-03 19:01:57 +08:00
lvmeng c7cad6d8b5 备份代码 2026-02-03 17:02:28 +08:00
lvmeng 2656e89244 增量人脸数据查询优化 2025-12-31 11:53:35 +08:00
lvmeng b5daf0f5df 拉取全量和增量人脸数据,增加已采集数据提示 2025-12-29 14:29:30 +08:00
55 changed files with 5715 additions and 453 deletions
+13 -1
View File
@@ -68,5 +68,17 @@ dependencies {
implementation(libs.accompanist.permissions)
implementation(project(":lib_face"))
implementation("com.github.Dimezis:BlurView:version-3.2.0")
// implementation("com.github.Dimezis:BlurView:version-3.2.0")
// retrofit网络请求
implementation(libs.retrofit)
implementation(libs.converter.gson)
// okhttp
implementation(libs.okhttp)
implementation(libs.logging.interceptor)
// gson
implementation(libs.gson)
// implementation("com.google.android.material:flowlayout:1.12.0")
}
+11
View File
@@ -30,6 +30,16 @@
android:usesCleartextTraffic="true"
android:theme="@style/Theme.FaceCollect">
<!-- 注册BootReceiver,监听开机完成广播 -->
<receiver
android:name=".receiver.BootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<activity
android:name=".MainActivity"
android:exported="true"
@@ -41,6 +51,7 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="com.sw.face.collect.InitActivity" />
</application>
</manifest>
@@ -0,0 +1,18 @@
package com.sw.face.collect
import com.sw.face.collect.base.BaseActivity
import com.sw.face.collect.databinding.ActivityInitBinding
import com.sw.face.collect.databinding.ActivityMainBinding
class InitActivity : BaseActivity<ActivityInitBinding>() {
override fun inflateViewBinding() = ActivityInitBinding.inflate(layoutInflater)
override fun initialize() {
super.initialize()
binding.btnStart.setOnClickListener {
finish()
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,887 @@
//package com.sw.face.collect
//
//import android.Manifest
//import android.content.Intent
//import android.content.pm.PackageManager
//import android.graphics.Point
//import android.hardware.Camera
//import android.util.Log
//import android.view.View
//import android.view.ViewGroup
//import android.view.ViewTreeObserver
//import android.widget.FrameLayout
//import androidx.activity.viewModels
//import androidx.core.content.ContextCompat
//import androidx.lifecycle.Observer
//import androidx.lifecycle.lifecycleScope
//import com.arcsoft.face.ErrorInfo
//import com.sw.face.collect.base.BaseActivity
//import com.sw.face.collect.base.GlobalData
//import com.sw.face.collect.databinding.ActivityMainBinding
//import com.sw.face.collect.ext.dp
//import com.sw.face.collect.ext.gone
//import com.sw.face.collect.ext.toast
//import com.sw.face.collect.ext.visible
//import com.sw.face.collect.model.UserFaceModel
//import com.sw.face.collect.socket.LanServer
//import com.sw.face.collect.socket.TcpClient
//import com.sw.face.collect.utils.Base64
//import com.sw.face.collect.utils.BitmapUtils
//import com.sw.face.collect.utils.FaceEngineUtils
//import com.sw.face.collect.utils.IntervalExecutor
//import com.sw.face.collect.utils.SpTool
//import com.sw.face.collect.utils.countDownByFlow
//import com.sw.face.collect.view.LanServerListenerImpl
//import com.sw.face.collect.view.TcpClientListenerImpl
//import com.sw.face.collect.viewmodel.MainViewModel
//import com.sw.plate.utils.L
//import com.sw.plate.utils.NV21ToBitmap
//import com.sw.plate.utils.ToastUtils
//import com.sw.plate.utils.arcface.ConfigUtil
//import com.sw.plate.utils.arcface.ErrorCodeUtil
//import com.sw.plate.utils.arcface.FaceRectTransformer
//import com.sw.plate.utils.arcface.FaceRectView
//import com.sw.plate.utils.arcface.FaceRectView.DrawInfo
//import com.sw.plate.utils.arcface.PreviewConfig
//import com.sw.plate.utils.arcface.camera.CameraListener
//import com.sw.plate.utils.arcface.camera.DualCameraHelper
//import com.sw.plate.utils.arcface.face.constants.LivenessType
//import com.sw.plate.utils.arcface.face.model.CompareResult
//import com.sw.plate.utils.arcface.face.model.FacePreviewInfo
//import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration
//import com.sw.plate.utils.arcface.facedb.FaceDatabase
//import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
//import com.sw.plate.utils.arcface.model.UserFaceInfo
//import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
//import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel.REGISTER_STATUS_READY
//import kotlinx.coroutines.Job
//import kotlinx.coroutines.runBlocking
//import org.json.JSONObject
//import kotlin.getValue
//import kotlin.system.exitProcess
//
//class MainActivityBak2 : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlobalLayoutListener {
// // private val debouncer = Debouncer(2000)
//
// companion object {
// private const val TAG = "MainActivity"
// }
//
// // 虹软人脸配置 ⬇
// private var isRecognition = false
// private var rgbCameraHelper: DualCameraHelper? = null
// private var rgbFaceRectTransformer: FaceRectTransformer? = null
//
// private var livenessType = LivenessType.IR
//
// // private val livenessType = LivenessType.RGB
// private var openRectInfoDraw = false
//
// private val nv21ToBitmap by lazy { NV21ToBitmap(this) }
//
// //
// private var irCameraHelper: DualCameraHelper? = null
// private var irFaceRectTransformer: FaceRectTransformer? = null
//
// private val recognizeViewModel: RecognizeViewModel by lazy { viewModels<RecognizeViewModel>().value }
//
// private val mainViewModel by viewModels<MainViewModel>()
//
// private var currentUserId: String? = null
// private var lastFaceTrackId = -1
//
// override fun inflateViewBinding() = ActivityMainBinding.inflate(layoutInflater)
//
// override fun onGlobalLayout() {
// Log.d(TAG, "onGlobalLayout")
// binding.dualCameraTexturePreviewRgb.getViewTreeObserver().removeOnGlobalLayoutListener(this)
// openCamera()
// }
//
// private var pageNo = 1
// override fun initialize() {
// super.initialize()
// mainViewModel.getUserFaceCache(pageNo = pageNo)
// FaceEngineUtils.activeEngine()
//
// binding.btnCollectFace.setOnClickListener {
// requestSinglePermissionResult(Manifest.permission.CAMERA) { granted ->
// if (granted.not()) {
// toast("没有相机权限")
// return@requestSinglePermissionResult
// }
// collectFace()
// }
// }
// binding.tvTitle.setOnClickListener { finish() }
// addSocketListener()
//
// binding.flFace.visible()
// binding.flRecognizeIr.visible()
// binding.flCameraView.visible()
// binding.tvFaceTip.visible()
// binding.layoutState.gone()
//
//// resumeCamera()
// startFaceTask()
// }
//
//
// private fun openCamera() {
// Log.d(TAG, "openCamera")
// if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) !=
// PackageManager.PERMISSION_GRANTED
// ) {
// toast("无摄像头权限")
// return
// }
// try {
//// val cameraCount = Camera.getNumberOfCameras()
//// if (cameraCount < 3) {
//// toast("摄像头数量异常"+cameraCount)
//// return
//// }
// recognizeViewModel.init(
// PreviewConfig(
// PreviewConfig.DEFAULT_IR_CAMERA_ID, PreviewConfig.DEFAULT_RGB_CAMERA_ID,
// 0, 0
// )
// )
// initRgbCamera()
// if (DualCameraHelper.hasDualCamera() && livenessType === LivenessType.IR) {
// initIrCamera()
// }
// } catch (e: Exception) {
// Log.d(TAG, "打开摄像头失败")
// toast("打开摄像头失败+${e.message}")
// }
// }
//
//
// /**
// * 启用虹软人脸识别
// */
// fun setupArcCamera() {
// initArcViewModel()
// initArcView()
// openRectInfoDraw = true
// }
//
// private fun sendFaceData(faceData: String) {
// // 给某个客户端发送
// val jsonObject = JSONObject().also {
// it.put("type", "faceFeature")
// it.put("content", faceData)
// }
// tcpClient?.send(jsonObject)
//
// // TODO: 保存采集的人脸数据,暂时生成id,实际id在结算终端生成并发送到各台设备
// saveFaceInfo(faceData)
// }
//
// private fun saveFaceInfo(faceData: String) {
// Thread {
// try {
// val tempId = "${System.currentTimeMillis()}-123456789"
// val faceEntity = FaceEntity(
// tempId,
// null,
// Base64.decode(faceData)
// ).also {
// it.userType = "2"
// }
// FaceDatabase.getInstance(this).faceDao().insert(faceEntity)
// recognizeViewModel.refreshFaceList();
// runOnUiThread {
// toast("你的人脸信息已采集")
// }
// } catch (e: Exception) {
// e.printStackTrace()
// }
// }.start()
// }
//
// private var tcpClient: TcpClient? = null
//
// private fun addSocketListener() {
// //val ipAddress = NetworkUtils.getIPAddress(true)
// //Log.d(TAG, "addSocketListener,ipAddress: $ipAddress");
// tcpClient = TcpClient(
// "192.168.1.95",
// 5000,
// GlobalData.deviceId, // clientId
// 5000, // connectTimeoutMs
// 10 * 1000, // heartbeatIntervalMs
// 30 * 1000 // heartbeatTimeoutMs
// )
//
// tcpClient?.setListener(object : TcpClientListenerImpl() {
// override fun onSendSuccess(json: JSONObject?) {
// super.onSendSuccess(json)
//// toast("发送成功")
// }
//
// override fun onSendFailed(json: JSONObject?, e: java.lang.Exception?) {
// super.onSendFailed(json, e)
// toast("发送失败${e?.message}")
// }
//
// override fun onMessage(json: JSONObject?) {
// super.onMessage(json)
//
// val type = json?.getInt("type")
// // 根据服务端消息类型处理删除新增临时用户人脸数据---------------------
// when (type) {
// LanServer.TYPE_ADD_FACE_DATA -> {
// //采集设备id自己生成,不接收服务端的,在本地只判断是否存在同一人脸数据
// }
// LanServer.TYPE_CLEAR_FACE_DATA -> {
// clearFaceData()
// }
// else-> {}
// }
// }
// })
// tcpClient?.start();
//
// try {
// LanServer.getInstance().let {
// it.setListener(object : LanServerListenerImpl() {
// override fun onMessageReceived(clientId: String?, message: JSONObject?) {
// super.onMessageReceived(clientId, message)
// toast("收到消息clientId=$clientId")
// }
// })
// it.start()
// }
// } catch (e: Exception) {
// e.printStackTrace()
// }
// }
//
// // private var imageBitmap: Bitmap? = null
// private fun collectFace() {
// //toast("currentUserId=$currentUserId")
// if (currentUserId != null) {
// toast("您已采集过人脸信息")
// return
// }
// currentUserId = null
// userFaceInfo = null
// recognizeViewModel.updateRegisterStatus(REGISTER_STATUS_READY)
// //showWaitingDialog("人脸信息中……")
//
// binding.tvFaceTip.gone()
// binding.btnCollectFace.gone()
// binding.layoutState.visible()
// binding.pbCollectLoading.visible()
// binding.tvCollectState.text = "采集中......"
// getFaceData { faceData ->
// Log.d(TAG, "faceData: $faceData")
// runOnUiThread {
// //hideWaitingDialog()
// //toast("人脸信息已采集")
// binding.pbCollectLoading.gone()
// //binding.tvCollectState.text = "采集完成"
// userFaceInfo = null
// countDown()
// }
// sendFaceData(faceData)
// }
// }
//
// private var job: Job? = null
//
// private fun getFaceData(block: (String) -> Unit) {
// runBlocking {
// job = executor.startIntervalTask(5) {
//// if (currentUserId != null && currentUserId!!.isNotBlank()) {
//// toast("您已采集过人脸信息")
//// return@startIntervalTask
//// }
// if (userFaceInfo == null) {
// return@startIntervalTask
// }
// val faceData = Base64.encode(userFaceInfo!!.faceFeature!!.featureData)
// block(faceData)
// job?.cancel()
// }
// }
//
// }
//
// val executor = IntervalExecutor()
//
//
// public override fun onStop() {
// rgbCameraHelper?.release()
// rgbCameraHelper = null
// irCameraHelper?.release()
// irCameraHelper = null
// recognizeViewModel.destroy()
// super.onStop()
// }
//
// fun resumeCamera() {
// Log.d(TAG, "resumeCamera isRecognition = $isRecognition")
// isRecognition = true
// if (rgbCameraHelper != null && rgbCameraHelper!!.isStopped) {
// rgbCameraHelper!!.start()
// }
// }
//
// fun pauseCamera() {
// Log.d(TAG, "pauseCamera isRecognition = $isRecognition")
// isRecognition = false
//
// recognizeViewModel.onPreviewFrame(ByteArray(1382400), true)
// }
//
// private var userFaceInfo: UserFaceInfo? = null
// private var isCheckCamera = false
// private fun initArcViewModel() {
// if (DualCameraHelper.hasDualCamera()) {
// livenessType = LivenessType.IR
// isCheckCamera = true
// } else {
// livenessType = LivenessType.RGB
// isCheckCamera = false
// }
// recognizeViewModel.setLiveType(livenessType)
//
// recognizeViewModel.prepareRegister()
// recognizeViewModel.setOnRegisterFinishedCallback { facePreviewInfo, userFaceInfo ->
// this.userFaceInfo = userFaceInfo
// Log.d(TAG, "initArcViewModel: userFaceInfo == null : ${userFaceInfo == null}")
// }
// recognizeViewModel.ftInitCode.observe(this, Observer { ftInitCode: Int? ->
// if (ftInitCode != ErrorInfo.MOK) {
// val error: String? = context.getString(
// R.string.specific_engine_init_failed, "ftEngine",
// ftInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(ftInitCode!!)
// )
// Log.d(TAG, "ftInitCode observe = $error")
// ToastUtils.showToast(error)
// }
// })
// recognizeViewModel.frInitCode.observe(this, Observer { frInitCode: Int? ->
// if (frInitCode != ErrorInfo.MOK) {
// val error: String? = context.getString(
// R.string.specific_engine_init_failed, "frEngine",
// frInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(frInitCode!!)
// )
// Log.d(TAG, "frInitCode observe = $error")
// ToastUtils.showToast(error)
// }
// })
// recognizeViewModel.flInitCode.observe(this, Observer { flInitCode: Int? ->
// if (flInitCode != ErrorInfo.MOK) {
// val error: String? = context.getString(
// R.string.specific_engine_init_failed, "flEngine",
// flInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(flInitCode!!)
// )
// Log.d(TAG, "flInitCode observe = $error")
// ToastUtils.showToast(error)
// }
// })
//
// recognizeViewModel.recognizeConfiguration
// .observe(this, Observer { recognizeConfiguration: RecognizeConfiguration? ->
// Log.d(TAG, "recognizeConfiguration: observe = ${recognizeConfiguration.toString()}")
// })
// recognizeViewModel.recognizeNotice.observe(this, Observer { notice: String? ->
// Log.d(TAG, "recognizeNotice observe notice = $notice")
// })
//
// recognizeViewModel.recognizeUserId.observe(
// this,
// Observer { compareResult: CompareResult ->
// Log.d(
// TAG,
// "recognizeUserId observe compareResult = ${compareResult.trackId}, userId = ${compareResult.faceEntity.userName}"
// )
// lastFaceTrackId = compareResult.trackId
// val faceEntity = compareResult.faceEntity
// currentUserId = faceEntity.userName
// if (!currentUserId.isNullOrBlank()) {
// toast("您已采集过人脸信息")
// return@Observer
// }
//// //未识别到,拍摄照片
//
// })
//
// recognizeViewModel.drawRectInfoText.observe(this, Observer { info ->
// Log.d(TAG, "drawRectInfoText observe info = $info")
// })
// }
//
// private fun initArcView() {
// //在布局结束后才做初始化操作
// binding.dualCameraTexturePreviewRgb.getViewTreeObserver().addOnGlobalLayoutListener(this)
// recognizeViewModel.getCompareResultList().getValue()
// }
//
// /**
// * 调整View的宽高,使预览显示正常且采集框固定为
// *
// * @param rgbPreview RGB预览View
// * @param previewView 显示预览数据的view
// * @param faceRectView 画框的view
// * @param previewSize 预览大小
// * @param displayOrientation 相机旋转角度
// * @param scale 缩放比例
// * @return 调整后的LayoutParams
// */
// private fun adjustPreviewViewSize(
// rgbPreview: View,
// previewView: View,
// faceRectView: FaceRectView,
// previewSize: Camera.Size,
// displayOrientation: Int,
// scale: Float
// ): ViewGroup.LayoutParams {
//// val w = 640.dp//300.dp
// val w = 640.dp//300.dp
// val h = 480.dp//510.dp
// val layoutParams = FrameLayout.LayoutParams(w, h);
// previewView.setLayoutParams(layoutParams)
// faceRectView.setLayoutParams(layoutParams)
//
// return layoutParams
// }
//
// private fun initRgbCamera() {
// val cameraListener: CameraListener = object : CameraListener {
// override fun onCameraOpened(
// camera: Camera,
// cameraId: Int,
// displayOrientation: Int,
// isMirror: Boolean
// ) {
// Log.d(
// TAG,
// "initRgbCamera Rgb onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation"
// )
// runOnUiThread({
// val previewSizeRgb = camera.getParameters().getPreviewSize()
// val layoutParams = adjustPreviewViewSize(
// binding.dualCameraTexturePreviewRgb,
// binding.dualCameraTexturePreviewRgb,
// binding.dualCameraFaceRectView,
// previewSizeRgb, displayOrientation,
// 0.5F
// //0.6F
// )
//
// Log.d(
// TAG,
// "initRgbCamera previewSizeRgb = ${previewSizeRgb.width}-${previewSizeRgb.height}"
// )
// Log.d(
// TAG,
// "initRgbCamera layoutParams = ${layoutParams.width}-${layoutParams.height}"
// )
// Log.d(
// TAG,
// "initRgbCamera isMirror = ${isMirror}, isDrawRgbRectHorizontalMirror = ${
// ConfigUtil.isDrawRgbRectHorizontalMirror(
// context
// )
// }, isDrawRgbRectVerticalMirror = ${
// ConfigUtil.isDrawRgbRectVerticalMirror(
// context
// )
// }"
// )
// // 调整识别窗口位置
// rgbFaceRectTransformer = FaceRectTransformer(
// previewSizeRgb.width,
// previewSizeRgb.height,
// layoutParams.width,
// layoutParams.height,
// displayOrientation,
//// 90,
// cameraId,
// isMirror,
// true,
// false
// )
//
// recognizeViewModel.onRgbCameraOpened(camera)
// recognizeViewModel.setRgbFaceRectTransformer(rgbFaceRectTransformer)
// })
// }
//
// override fun onPreview(nv21: ByteArray?, camera: Camera?) {
//// faceByteArray = nv21
// if (!isRecognition) {
// return
// }
// if (isCheckCamera) {
// L.e("检查摄像头数据")
//
// isCheckCamera = false
//
// val isBw: Boolean = BitmapUtils.isBlackWhite(
// nv21ToBitmap.nv21ToBitmap(
// nv21,
// rgbFaceRectTransformer!!.previewWidth,
// rgbFaceRectTransformer!!.previewHeight,
// 0
// )
// )
// if (isBw) {
// L.e("黑白切换摄像头")
// switchCamera()
// }
// L.e("摄像头正常")
// }
// binding.dualCameraFaceRectView.clearFaceInfo()
// val facePreviewInfoList: MutableList<FacePreviewInfo>? =
// recognizeViewModel.onPreviewFrame(nv21, true)
// if (facePreviewInfoList != null
// && rgbFaceRectTransformer != null
// ) {
// drawPreviewInfo(facePreviewInfoList)
// }
// recognizeViewModel.clearLeftFace(facePreviewInfoList)
// }
//
// override fun onCameraClosed() {
// Log.d(TAG, "initRgbCamera onCameraClosed: ")
// }
//
// override fun onCameraError(e: java.lang.Exception) {
// Log.d(TAG, "initRgbCamera onCameraError: ${e.message}")
// e.printStackTrace()
// }
//
// override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) {
// Log.d(
// TAG,
// "initRgbCamera onCameraConfigurationChanged: threadName = ${Thread.currentThread().name}"
// )
// if (rgbFaceRectTransformer != null) {
// rgbFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation
// }
// Log.d(
// TAG,
// "initRgbCamera onCameraConfigurationChanged: $cameraID $displayOrientation"
// )
// }
// }
// val measuredWidth = binding.dualCameraTexturePreviewRgb.measuredWidth
// val measuredHeight = binding.dualCameraTexturePreviewRgb.measuredHeight
// Log.d(TAG, "initRgbCamera measuredWidth=$measuredWidthmeasuredHeight=$measuredHeight")
//
// val previewConfig = recognizeViewModel.previewConfig
// rgbCameraHelper = DualCameraHelper.Builder()
// .previewViewSize(Point(measuredWidth, measuredHeight))
//// .previewViewSize(Point(608.dp, 456.dp))
//// .previewViewSize(Point(1024, 768))
// .rotation(windowManager.defaultDisplay.rotation)
// .additionalRotation(previewConfig.rgbAdditionalDisplayOrientation) // 角度
// .previewSize(recognizeViewModel.loadPreviewSize())
//// .previewSize(Point(1080, 720))
// .specificCameraId(previewConfig.rgbCameraId)
// .isMirror(true)
// .previewOn(binding.dualCameraTexturePreviewRgb)
// .cameraListener(cameraListener)
// .build()
// rgbCameraHelper?.run {
// init()
//// setSurfaceFrameCallback { bitmap ->
//// imageBitmap = BitmapFlipUtil.flipHorizontally(bitmap)
////
////// binding.ivRecImage.visible()
////// binding.ivRecImage.setImageBitmap(imageBitmap)
//// }
// start()
// }
// }
//
// /**
// * 初始化红外相机,若活体检测类型是可见光活体检测或不启用活体,则不需要启用
// */
// private fun initIrCamera() {
// Log.d(TAG, "initIrCamera: livenessType = $livenessType")
// if (livenessType === LivenessType.RGB) {
// return
// }
// val irCameraListener: CameraListener = object : CameraListener {
// override fun onCameraOpened(
// camera: Camera,
// cameraId: Int,
// displayOrientation: Int,
// isMirror: Boolean
// ) {
// Log.d(
// TAG,
// "initIrCamera IR onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation"
// )
// val previewSizeIr = camera.getParameters().getPreviewSize()
// val layoutParams = adjustPreviewViewSize(
// binding.dualCameraTexturePreviewRgb,
// binding.dualCameraTexturePreviewIr, binding.dualCameraFaceRectViewIr,
// previewSizeIr, displayOrientation, 0.25f
// )
//
// irFaceRectTransformer = FaceRectTransformer(
// previewSizeIr.width, previewSizeIr.height,
//// layoutParams.width, layoutParams.height,
// layoutParams.width, layoutParams.height,
// displayOrientation, cameraId, isMirror,
// ConfigUtil.isDrawIrRectHorizontalMirror(context),
// ConfigUtil.isDrawIrRectVerticalMirror(context)
// )
//
// recognizeViewModel.onIrCameraOpened(camera)
// recognizeViewModel.setIrFaceRectTransformer(irFaceRectTransformer)
// }
//
//
// override fun onPreview(nv21: ByteArray?, camera: Camera?) {
// recognizeViewModel.refreshIrPreviewData(nv21)
// }
//
// override fun onCameraClosed() {
// Log.d(TAG, "initIrCamera onCameraClosed: ")
// }
//
// override fun onCameraError(e: java.lang.Exception) {
// Log.d(TAG, "initIrCamera onCameraError: ${e.message}")
// e.printStackTrace()
// }
//
// override fun onCameraConfigurationChanged(
// cameraID: Int,
// displayOrientation: Int
// ) {
// if (irFaceRectTransformer != null) {
// irFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation
// }
// Log.d(
// TAG,
// "initIrCamera onCameraConfigurationChanged: cameraID = $cameraID, displayOrientation = $displayOrientation"
// )
// }
// }
//
// val previewConfig = recognizeViewModel.previewConfig
// irCameraHelper = DualCameraHelper.Builder()
// .previewViewSize(
// Point(
// binding.dualCameraTexturePreviewIr.measuredWidth,
// binding.dualCameraTexturePreviewIr.measuredHeight
// )
// )
//// .previewViewSize(Point(4,3))
// .rotation(windowManager.defaultDisplay.rotation)
// .specificCameraId(previewConfig.irCameraId)
// .previewOn(binding.dualCameraTexturePreviewIr)
// .cameraListener(irCameraListener)
// .isMirror(true)
// .previewSize(recognizeViewModel.loadPreviewSize()) //相机预览大小设置,RGB与IR需使用相同大小
// .additionalRotation(previewConfig.irAdditionalDisplayOrientation) //额外旋转角度
// .build()
// irCameraHelper?.init()
// try {
// irCameraHelper?.start()
// } catch (e: RuntimeException) {
// ToastUtils.showToast(e.message + context.getString(R.string.camera_error_notice))
// }
// }
//
// /**
// * 绘制RGB、IR画面的实时人脸信息
// *
// * @param facePreviewInfoList RGB画面的实时人脸信息
// */
// private fun drawPreviewInfo(facePreviewInfoList: MutableList<FacePreviewInfo>) {
//// Timber.tag(TAG).d("drawPreviewInfo facePreviewInfoList = ${facePreviewInfoList.size}, rgbFaceRectTransformer = ${rgbFaceRectTransformer != null}")
// if (rgbFaceRectTransformer != null) {
// val rgbDrawInfoList: MutableList<DrawInfo>? = recognizeViewModel.getDrawInfo(
// facePreviewInfoList,
// LivenessType.RGB,
// openRectInfoDraw
// )
// // 识别成功
// binding.dualCameraFaceRectView.drawRealtimeFaceInfo(rgbDrawInfoList)
// }
//
// val listIsEmpty = facePreviewInfoList.isEmpty()
// val listFirstTrackId = if (listIsEmpty.not()) facePreviewInfoList[0].trackId else null
// Log.d(
// TAG,
// "listIsEmpty=$listIsEmpty,lastFaceTrackId=$lastFaceTrackId,listFirstTrackId=$listFirstTrackId"
// )
// if (listIsEmpty || (lastFaceTrackId != listFirstTrackId)) {
// if (lastFaceTrackId != -1 && currentUserId != null) {
// Log.d(TAG, "$lastFaceTrackId 用户离开")
// lastFaceTrackId = -1
// currentUserId = null
// }
// }
// }
//
// fun switchCamera() {
// try {
// L.e("开始切换摄像头")
// if (rgbCameraHelper != null && irCameraHelper != null) {
// rgbCameraHelper!!.stop()
// irCameraHelper!!.stop()
// rgbCameraHelper!!.switchCameraId()
// irCameraHelper!!.switchCameraId()
// rgbCameraHelper!!.start()
// irCameraHelper!!.start()
// rgbFaceRectTransformer!!.cameraId =
// if (rgbFaceRectTransformer!!.getCameraId() === 1) 0 else 1
// // showLongToast(getString(R.string.notice_change_detect_degree));
// L.e("切换摄像头成功")
// } else {
// L.e("切换摄像头失败")
//
// // showToast(getString(R.string.switch_camera_failed));
// exit()
// val newIntent = Intent(this, MainActivityBak2::class.java) // 要启动的Activity
// newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
// startActivity(newIntent)
// }
// } catch (e: java.lang.RuntimeException) {
// L.e("切换摄像头失败")
// toast(e.message + getString(R.string.camera_error_notice))
//
// exit()
// val newIntent = Intent(this, MainActivityBak2::class.java) // 要启动的Activity
// newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
// startActivity(newIntent)
// }
// }
//
// private fun exit() {
// finish()
// exitProcess(0)
// }
//
// override fun onDestroy() {
// countDownJob?.cancel()
// faceTaskJob?.cancel()
// super.onDestroy()
// }
//
// private var countDownJob: Job? = null
// private fun countDown() {
// countDownJob = countDownByFlow(
// total = 3,
// scope = lifecycleScope,
// onStart = {
// binding.tvCollectState.text = "采集完成(3s"
// },
// onTick = { seconds ->
// binding.tvCollectState.text = "采集完成(${seconds}s"
// },
// onFinish = {
// //binding.tvCollectState.text = "采集完成"
// currentUserId = null
// binding.layoutState.gone()
// binding.tvFaceTip.visible()
// binding.btnCollectFace.visible()
// }
// )
// }
//
// private val intervalExecutor by lazy { IntervalExecutor() }
// private var faceTaskJob: Job? = null
//
// private var taskPageNo = 1
// fun startFaceTask() {
// faceTaskJob =
// intervalExecutor.startIntervalTaskWithInitialDelay(60 * 1000L, 30 * 1000L) {
// val timestamp = SpTool.getLastFaceTimestamp()
// if (timestamp == 0L) {
// return@startIntervalTaskWithInitialDelay
// }
// mainViewModel.getFaceIncrementList(
// pageNo = taskPageNo,
// timestamp = timestamp,
// onAllQueryFinished = {
// taskPageNo = 1
// },
// onPageQueryFinished = { list ->
// runOnUiThread {
// if (list.isEmpty()) {
// return@runOnUiThread
// }
// updateFaceData(list)
// }
// }
// )
// }
// }
//
// private fun updateFaceData(list: List<UserFaceModel>) {
// Thread {
// val faceList = mutableListOf<FaceEntity>()
// try {
// list.forEach { model ->
// if (model.faceDeleted == true) {
// //删除数据
// FaceDatabase.getInstance(this).faceDao().deleteFaceById(model.userId)
// } else {
// //保存数据
// val faceEntity = FaceEntity(
// model.userId,
// null,
// Base64.decode(model.faceFeatureStr)
// ).also {
// it.userType = "1"
// }
// faceList.add(faceEntity)
// }
// }
// } catch (e: Exception) {
// e.printStackTrace()
// }
// try {
// if (faceList.isNotEmpty()) {
// FaceDatabase.getInstance(this).faceDao().insert(faceList)
// }
// recognizeViewModel.refreshFaceList();
// } catch (e: Exception) {
// e.printStackTrace()
// }
// }.start()
// }
//
//// private var dinnerTypeJob: Job? = null
//// fun startDinnerTypeTask() {
//// dinnerTypeJob =
//// intervalExecutor.startIntervalTaskWithInitialDelay(10 * 60 * 1000L, 15 * 60 * 1000L) {
//// mainViewModel.getDinnerType {
////
//// }
//// }
//// }
//
// private fun clearFaceData() {
// Thread {
// val faceDao = FaceDatabase.getInstance(this).faceDao()
// faceDao.deleteTempUserFaceData()
// recognizeViewModel.refreshFaceList();
// }.start()
// }
//
// override fun onResume() {
// super.onResume()
// setupArcCamera()
//
// resumeCamera()
// }
//
// override fun onPause() {
// super.onPause()
// pauseCamera()
// }
//
//}
File diff suppressed because it is too large Load Diff
@@ -11,6 +11,7 @@ class MyApp: App() {
companion object {
private const val TAG = "MyApp"
var DEBUG = true
@SuppressLint("StaticFieldLeak")
var instance: Context? = null
}
@@ -16,6 +16,8 @@ import com.sw.face.collect.databinding.DialogWaitingBinding
import com.sw.face.collect.view.CustomDialog
import com.sw.face.collect.R
import com.sw.face.collect.model.IActivityResult
import com.sw.face.collect.utils.PermissionHelper
import kotlin.collections.set
abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
@@ -51,6 +53,14 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
}
private val permissionHelpers = mutableMapOf<Int, PermissionHelper>()
/**
* 注册PermissionHelper以便处理结果
*/
fun registerPermissionHelper(helper: PermissionHelper) {
permissionHelpers[helper.requestCode] = helper
}
/**
* 显示等待提示框
@@ -15,7 +15,7 @@ object GlobalData {
/**
* 具体业务baseurl
*/
var appBaseUrl: String = ""
var appBaseUrl: String = "https://dev.yixiong-tech.com:8081"
/**
* 横排数量
@@ -155,13 +155,34 @@ fun EditText.addOnActionSearchListener(searchCallback: () -> Unit) {
}
//fun View.clickWithDebounce(delay: Long = 300, action: () -> Unit) {
// var job: Job? = null
// setOnClickListener {
// job?.cancel()
// job = CoroutineScope(Dispatchers.Main).launch {
// delay(delay)
// action()
// }
// }
//}
/**
* 极简版防重复点击扩展函数
* @param delay 防抖时间(默认300ms
* @param action 点击执行逻辑
*/
fun View.clickWithDebounce(delay: Long = 300, action: () -> Unit) {
var job: Job? = null
setOnClickListener {
job?.cancel()
job = CoroutineScope(Dispatchers.Main).launch {
delay(delay)
action()
// 用View的tag存储是否可点击的状态(默认可点击)
if (tag as? Boolean ?: true) {
tag = false // 标记为不可点击
action() // 立即执行点击逻辑
// 启动协程,延迟后恢复可点击状态
CoroutineScope(Dispatchers.Main).launch {
delay(delay)
tag = true // 恢复可点击
}
}
}
}
@@ -250,3 +271,4 @@ fun String.copyText(context: Context, showToast: Boolean = true) {
Toast.makeText(context, "文本已复制到剪贴板", Toast.LENGTH_SHORT).show()
}
}
@@ -0,0 +1,10 @@
package com.sw.face.collect.model
data class ApiResponse<T>(
val code: String,
val msg: String? = "",
val data: T? = null,
val result: T? = null,
)
@@ -0,0 +1,6 @@
package com.sw.face.collect.model
data class RespCodeMsg(
val code: String?,
val msg: String?
)
@@ -0,0 +1,62 @@
package com.sw.face.collect.model
//import android.os.Parcelable
//import com.google.gson.annotations.SerializedName
//import kotlinx.parcelize.Parcelize
/**
* 用户人脸信息
*/
//@Parcelize
data class UserFaceModel(
// @SerializedName("faceFeature")
// val faceFeature: String? = "",
// @SerializedName("faceFeatureString")
// val faceFeatureString: String? = "",
// @SerializedName("faceType")
// val faceType: String? = "",
// @SerializedName("userFaceId")
// val userFaceId: String? = "",
// @SerializedName("userId")
val userId: String? = "",
val faceFeatureStr: String? = "",
val faceUpdateTimestamp: Long?=null,
val faceDeleted: Boolean? = false,
/**
* 会员编号
*/
val cardNo: String,
/**
* 是否会员
*/
val isMember: Boolean
)
// : Parcelable
//@Parcelize
data class UserFaceModel2(
val userId: String? = "",
val faceFeatureString: String? = "",
val face: String? = ""
)
// : Parcelable
data class FaceData(
val nextPageIndex: Int,
val total: Int,
val size: Int,
val current: Int,
val pages: Int,
val records: List<UserFaceModel2>? = null
)
data class DinnerType(
val id: String? = "",
//餐次
val dinnerType: String? = "",
//餐次收费模式(0称重,1固定,2不收费)
val chargeType: Int = 0,
//固定收费(元) 仅charge_type = 1 时
val fixedAmount: Double?=null
)
@@ -0,0 +1,47 @@
package com.sw.face.collect.network
import android.util.Log
import com.sw.face.collect.network.api.ApiService
import com.sw.face.collect.network.interceptor.RequestInterceptor
import com.sw.face.collect.MyApp
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
object ApiClient {
// private const val BASE_URL = "http://device.shuziweidao.com:8889/"
private const val BASE_URL = "https://dev.yixiong-tech.com:8081"
//private const val TIME_OUT = 30L // 超时时间(秒)
//todo 临时测试改为5秒
private const val TIME_OUT = 5L // 超时时间(秒)
private val okHttpClient = OkHttpClient.Builder()
.connectTimeout(TIME_OUT, TimeUnit.SECONDS)
.readTimeout(TIME_OUT, TimeUnit.SECONDS)
.writeTimeout(TIME_OUT, TimeUnit.SECONDS)
.addNetworkInterceptor(HttpLoggingInterceptor(logger = {
Log.d("ApiClient","okhttp logger ==>${it}")
}).apply {
level = if (MyApp.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
})
.addInterceptor(RequestInterceptor())
.build()
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
// .addCallAdapterFactory(CoroutineCallAdapterFactory()) // 协程适配器
.build()
val apiService: ApiService by lazy {
retrofit.create(ApiService::class.java)
}
}
@@ -0,0 +1,66 @@
package com.sw.face.collect.network.api
import com.sw.face.collect.base.GlobalData
import com.sw.face.collect.model.ApiResponse
import com.sw.face.collect.model.DinnerType
import com.sw.face.collect.model.UserFaceModel
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.http.Body
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.PartMap
import retrofit2.http.Query
import retrofit2.http.Url
interface ApiService {
/**
* 获取人脸数据
*/
@POST
suspend fun getUserFaceCache(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/faceFeature/list",
@Body param: Map<String, Int>
): ApiResponse<List<UserFaceModel>?>
/**
* 获取人脸增量数据
*/
@POST
suspend fun getFaceIncrementList(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/faceFeature/increment/list",
@Body param: Map<String, Long>
): ApiResponse<List<UserFaceModel>?>
/**
* 获取当前餐点类型
*/
@GET
suspend fun getDinnerType(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/getRegionRule",
): ApiResponse<DinnerType?>
@Multipart
@POST
suspend fun uploadImage(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/upload",
@Part file: MultipartBody.Part,
): ApiResponse<String?>
/**
* 添加人脸数据
*/
@POST
@FormUrlEncoded
suspend fun addUserFace(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/addUserByFace",
@Field("featureChar") faceData:String,
@Field("url") imageUr:String
): ApiResponse<UserFaceModel?>
}
@@ -0,0 +1,40 @@
package com.sw.face.collect.network.interceptor
import android.text.TextUtils
import com.sw.dualscreen.utils.SPUtil
import com.sw.face.collect.base.GlobalData
import com.sw.plate.App
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
/**
* 请求拦截器
*/
class RequestInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val requestBuilder = originalRequest.newBuilder()
.header("Content-Type", "application/json")
.header("Accept", "application/json")
// .header("Authorization", "Bearer ${getToken()}")
// .header("X-Access-Token", getToken(originalRequest))
.header("X-Access-Token", "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjYW50ZWVuSWQiOiJiZTE1NDgzMS0zNDY2LTNiYTItYTJlYS01NzY1MmM5MTlmZWQiLCJ0eXBlIjoiNCIsInVzZXJJZCI6IjAwMDAwMDAwMDAwMDAwMDAwMDEifQ.sN40cOC-O5WQFrF4IDUs8fFlkNdUKLbJt_rHyTsgYYM")
// .header("X-DEVICE-CODE", "bcf396ed-78f6-3864-9837-7c37c5b2ec41")
.header("X-DEVICE-CODE", GlobalData.deviceId)
.header("authorization", "57ee87183f2a4fa59683ec9ef41c8f5d")
val newRequest = requestBuilder.build()
return chain.proceed(newRequest)
}
// private fun getToken(originRequest: Request): String {
// val tokenParam = originRequest.header("X-Access-Token")
// if (!TextUtils.isEmpty(tokenParam)) return tokenParam!!
// // 从本地获取token的逻辑
// val spUtil = SPUtil.getInstance(context = App.getContext())
// return spUtil.get(GlobalKey.KEY_TOKEN, "") as String
//// return "eyJhbGciOiJIUzUxMiJ9.eyJpZCI6MTQ2LCJ1c2VyTmFtZSI6IjEzNjgxNDQ4ODU2IiwibmFtZSI6IuW-kOejiiIsInBhc3N3b3JkIjoiOTllOTQ1ZmVjZmZjNWIzNDI4MmUwNDRlODYyMzdjM2UxZjU5OWY5OCIsInNhbHQiOiI0NmEzMzUzYWU4OTA0MDYxYjMzODU5ZWNlYTBlMGE2NyIsInBob25lIjoiMTM2ODE0NDg4NTYiLCJzdGF0dXMiOjEsInVzZXJUeXBlIjoyLCJjcmVhdGVVc2VyTm8iOiIxNDEiLCJjcmVhdGVUaW1lIjoxNjI2OTQzNDE2MDAwLCJ1cGRhdGVVc2VyTm8iOiIxNDEiLCJ1cGRhdGVUaW1lIjoxNjI2OTQzNDE2MDAwLCJpc0RlbCI6ZmFsc2UsImVhSWQiOjk5LCJlYUlkTGlzdCI6Ijk5IiwiaXNTaG9wTWFuYWdlciI6dHJ1ZSwidXNlck5vIjoiMWY5Nzk5ZWMtODlkYi00MWYyLTk1YTEtY2UzNTA3Y2QyMTU2In0.f7wImPgBOYMV0AqRchnXGPkUWZN9dFJ9gLPsaB8uNldd21IfXLjJl8y-FiWVuVUvlwUvGpgqGDFR1JKj5H7amw"
// }
}
@@ -0,0 +1,19 @@
package com.sw.face.collect.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import com.sw.face.collect.MainActivity
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_BOOT_COMPLETED == intent.action) {
Log.d("BootReceiver","设备启动完成,开始执行自启动逻辑")
val intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
}
}
@@ -0,0 +1,57 @@
package com.sw.dualscreen.repository
import com.google.gson.JsonParseException
import com.sw.face.collect.model.ApiResponse
import com.sw.face.collect.model.RespCodeMsg
import com.sw.face.collect.utils.GsonUtils
import retrofit2.HttpException
import java.io.IOException
import java.net.ConnectException
import java.net.SocketTimeoutException
import javax.net.ssl.SSLHandshakeException
abstract class BaseRepository {
suspend fun <T> safeApiCall(apiCall: suspend () -> ApiResponse<T>): ApiResponse<T> {
return try {
apiCall()
} catch (e: Exception) {
// Timber.e("safeApiCall Exception: ${e.stackTraceToString()}")
when (e) {
is HttpException -> {
val respData = e.response()?.errorBody()?.string()
val respCodeMsg = GsonUtils.fromJson(respData, RespCodeMsg::class.java)
if (respCodeMsg?.msg.isNullOrBlank()) {
ApiResponse(code = "${e.code()}", msg = e.message())
} else {
ApiResponse(code = respCodeMsg.code ?:"-10", msg = respCodeMsg.msg)
}
}
is SocketTimeoutException -> {
ApiResponse(code = "-2", msg = "请求超时: ${e.message}")
}
is ConnectException -> {
ApiResponse(code = "-3", msg = "连接失败: ${e.message}")
}
is SSLHandshakeException -> {
ApiResponse(code = "-4", msg = "SSL握手失败: ${e.message}")
}
is JsonParseException -> {
ApiResponse(code = "-5", msg = "JSON解析错误: ${e.message}")
}
is IOException -> {
ApiResponse(code = "-6", msg = "网络IO错误: ${e.message}")
}
else -> {
ApiResponse(code = "-1", msg = "未知错误: ${e.message ?: "无错误信息"}")
}
}
}
}
}
@@ -0,0 +1,78 @@
package com.sw.face.collect.repository
import com.sw.dualscreen.repository.BaseRepository
import com.sw.face.collect.model.ApiResponse
import com.sw.face.collect.model.DinnerType
import com.sw.face.collect.model.UserFaceModel
import com.sw.face.collect.network.api.ApiService
import com.sw.face.collect.utils.FileUtils
import java.io.File
/**
* 远程数据处理
*/
class RemoteRepository constructor(
private val apiService: ApiService
) : BaseRepository() {
/**
* 获取人脸数据
*/
suspend fun getUserFaceCache(
pageNum: Int,
pageSize: Int = 100,
): ApiResponse<List<UserFaceModel>?> {
return safeApiCall {
apiService.getUserFaceCache(
param = mapOf(
"pageNum" to pageNum,
"pageSize" to pageSize
)
)
}
}
/**
* 获取人脸数据
*/
suspend fun getFaceIncrementList(
pageNum: Long,
pageSize: Long = 100L,
timestamp: Long
): ApiResponse<List<UserFaceModel>?> {
return safeApiCall {
apiService.getFaceIncrementList(
param = mapOf(
"pageNum" to pageNum,
"pageSize" to pageSize,
"timestamp" to timestamp
)
)
}
}
/**
* 获取当前餐点类型
*/
suspend fun getDinnerType(): ApiResponse<DinnerType?> {
return safeApiCall {
apiService.getDinnerType()
}
}
suspend fun uploadImage(file: File): ApiResponse<String?> {
return safeApiCall {
val part = FileUtils.genRequestPart(file)
if (part == null) {
ApiResponse(code = "-1", msg = "解析图片失败")
} else {
apiService.uploadImage(file = part)
}
}
}
suspend fun addUserFace(faceData: String, imageUr:String): ApiResponse<UserFaceModel?> {
return safeApiCall { apiService.addUserFace(faceData = faceData, imageUr = imageUr) }
}
}
@@ -1,7 +1,8 @@
package com.sw.face.collect.socket;
public class LanServer {
public static final int TYPE_ADD_FACE_DATA = 1;
public static final int TYPE_CLEAR_FACE_DATA = 2;
private static volatile LanCommunicationManager instance;
public static LanCommunicationManager getInstance() {
@@ -18,4 +19,17 @@ public class LanServer {
}
return instance;
}
void a() {
//HashMap<String, Object> data = new HashMap<>();
//data.put("type", LanServer.TYPE_ADD_FACE_DATA);
//data.put("faceId", tempUserId);//收到采集信息,每次生成新的ID
//data.put("faceFeature", faceFeatureString);
//lanServer.broadcast(new JSONObject(data));
//
//HashMap<String, Object> data = new HashMap<>();
//data.put("type", LanServer.TYPE_CLEAR_FACE_DATA);
//lanServer.broadcast(new JSONObject(data));
}
}
@@ -0,0 +1,59 @@
package com.sw.face.collect.utils
import android.content.Context
import android.graphics.Bitmap
import android.os.Environment
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import kotlin.io.use
object BitmapSaver {
// 保存到公共目录(需WRITE_EXTERNAL_STORAGE权限)
fun saveToPublicDirectory(
bitmap: Bitmap,
folderName: String = Environment.DIRECTORY_PICTURES,
fileName: String,
format: Bitmap.CompressFormat = Bitmap.CompressFormat.PNG,
quality: Int = 100
): File? {
val dir = Environment.getExternalStoragePublicDirectory(folderName)
if (!dir.exists()) dir.mkdirs()
return saveBitmap(bitmap, File(dir, fileName), format, quality)
}
// 保存到应用私有目录(无需权限)
fun saveToAppFilesDir(
bitmap: Bitmap,
context: Context,
fileName: String,
format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
quality: Int = 100
): File? {
//val dir = context.getExternalFilesDir(null)
val dir = context.cacheDir
val cropFile = File(dir, "crop")
if (cropFile.exists().not()) {
cropFile.mkdirs()
}
return saveBitmap(bitmap, File(cropFile, fileName), format, quality)
}
private fun saveBitmap(
bitmap: Bitmap,
outputFile: File,
format: Bitmap.CompressFormat,
quality: Int
): File? {
return try {
FileOutputStream(outputFile).use { fos ->
bitmap.compress(format, quality, fos)
fos.flush()
}
outputFile
} catch (e: IOException) {
e.printStackTrace()
null
}
}
}
@@ -0,0 +1,135 @@
package com.sw.face.collect.utils
import android.os.CountDownTimer
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.Log
/**
* 无状态残留的CountDownTimer封装(解决时间残留问题)
* @param millisInFuture 总时长(毫秒)
* @param countDownInterval 倒计时间隔(毫秒)
* @param onTick 倒计时回调(剩余毫秒)
* @param onFinish 结束回调
*/
class SafeCountDownTimer(
private val millisInFuture: Long,
private val countDownInterval: Long,
private val onTick: (Long) -> Unit,
private val onFinish: () -> Unit
) {
// 独立Handler,避免与其他CountDownTimer复用消息队列
private val handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
synchronized(this@SafeCountDownTimer) {
// 仅当timer未取消时处理消息,避免残留消息触发
if (!isCancelled) {
super.handleMessage(msg)
}
}
}
}
// 核心:标记是否已取消,避免残留消息触发
@Volatile
private var isCancelled = false
// 实际的CountDownTimer实例
private var timer: CountDownTimer? = null
/**
* 启动倒计时(启动前自动清空旧状态)
*/
fun start() {
// 第一步:强制取消旧实例+清空所有残留消息(核心!)
cancel()
// 第二步:重置取消标记
isCancelled = false
// 第三步:新建CountDownTimer实例,绑定独立Handler
timer = object : CountDownTimer(millisInFuture, countDownInterval) {
override fun onTick(millisUntilFinished: Long) {
// 双重校验:仅未取消时触发回调
if (!isCancelled) {
onTick.invoke(millisUntilFinished)
}
}
override fun onFinish() {
if (!isCancelled) {
onFinish.invoke()
// 结束后主动清空,避免残留
cancel()
}
}
}
// 第四步:启动新实例
timer?.start()
Log.d("SafeCountDownTimer", "倒计时启动,总时长:$millisInFuture 毫秒")
}
/**
* 取消倒计时(彻底清空所有状态)
*/
fun cancel() {
// 标记为已取消,拦截后续消息
isCancelled = true
// 取消timer实例
timer?.cancel()
timer = null
// 清空Handler所有残留消息(核心:解决旧消息触发问题)
handler.removeCallbacksAndMessages(null)
Log.d("SafeCountDownTimer", "倒计时已取消,清空所有残留状态")
}
}
// ========== 封装成工具类,方便全局调用 ==========
object CountDownManager {
// 保存当前倒计时实例,避免重复启动
private var currentTimer: SafeCountDownTimer? = null
/**
* 启动/重启倒计时(适配人脸识别防抖场景)
* @param totalSeconds 总秒数(如15
* @param onTick 每秒回调剩余秒数
* @param onFinish 结束回调
*/
fun startCountDown(
totalSeconds: Int = 15,
onTick: (Int) -> Unit = {},
onFinish: () -> Unit
) {
// 先取消旧倒计时
cancelCountDown()
// 新建安全倒计时实例
currentTimer = SafeCountDownTimer(
millisInFuture = totalSeconds * 1000L,
countDownInterval = 1000L,
onTick = { millis ->
// 转换为秒数回调
val sec = (millis / 1000).toInt() + 1
onTick.invoke(sec)
},
onFinish = {
onFinish.invoke()
}
)
// 启动倒计时
currentTimer?.start()
}
/**
* 取消当前倒计时
*/
fun cancelCountDown() {
currentTimer?.cancel()
currentTimer = null
}
}
@@ -0,0 +1,88 @@
package com.sw.face.collect.utils
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import android.util.Log
import java.util.concurrent.atomic.AtomicBoolean
/**
* 人脸识别防抖倒计时工具类(单例+防重复+无死循环)
*/
object CountDownUtil {
// 原子布尔值:标记是否正在倒计时(防重复启动)
private val isCounting = AtomicBoolean(false)
// 当前倒计时Job(用于取消)
private var currentJob: Job? = null
/**
* 启动/重启防抖倒计时(同一时间仅一个倒计时运行)
* @param totalSeconds 倒计时总秒数(如15
* @param lifecycleScope Activity/Fragment的lifecycleScope(必须)
* @param onTick 每秒回调剩余秒数(主线程)
* @param onFinish 倒计时结束回调(主线程)
*/
fun startCountDown(
total: Int = 15,
lifecycleScope: CoroutineScope,
onStart: () -> Unit = {},
onTick: (Int) -> Unit = {},
onFinish: () -> Unit
) {
// 1. 防重复:如果正在倒计时,先取消旧任务
if (isCounting.get()) {
//cancelCountDown()
return
}
// 2. 标记为正在倒计时
isCounting.set(true)
// 3. 启动新倒计时
currentJob = flow {
Log.d("CountDownUtil", "倒计时启动,总时长:$total")
for (i in total downTo 1) {
emit(i)
delay(1000) // IO线程等待1秒,无死循环
}
}
.flowOn(Dispatchers.IO) // 上游IO线程,避免阻塞主线程
.onStart {
withContext(Dispatchers.Main) {
onStart.invoke()
}
}
.onEach { remaining ->
// 主线程回调剩余时间
withContext(Dispatchers.Main) {
onTick.invoke(remaining)
}
}
.onCompletion {
// 倒计时结束:重置状态+回调
withContext(Dispatchers.Main) {
isCounting.set(false) // 重置标记
currentJob = null // 置空Job
Log.d("CountDownUtil", "倒计时正常结束")
onFinish.invoke()
}
}
.catch { e ->
// 捕获异常:避免崩溃,重置状态
Log.e("CountDownUtil", "倒计时异常:${e.message}", e)
isCounting.set(false)
currentJob = null
}
.launchIn(lifecycleScope)
}
/**
* 取消倒计时(手动停止)
*/
fun cancelCountDown() {
currentJob?.cancel() // 取消Job
currentJob = null // 置空,避免残留
isCounting.set(false) // 重置标记
Log.d("CountDownUtil", "倒计时被手动取消")
}
}
@@ -0,0 +1,122 @@
package com.sw.inbound.utils
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.flow
//import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.math.abs
import kotlin.to
/**
* 时间格式化工具类
*/
object DateTimeUtils {
/**
* 获取完整中文日期格式(示例:2025年6月11日 星期三)
*/
fun getChineseDateString(date: Date = Date()): String {
return SimpleDateFormat("yyyy年M月d日 EEEE", Locale.CHINA).format(date)
}
/**
* 获取标准时间格式
*/
fun getDateTimeString(date: Date = Date()): String {
return SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA).format(date)
}
/**
* 获取带时间的完整中文格式(示例:2025年6月11日 星期三 14:30
*/
fun getChineseDateTimeString(date: Date = Date()): String {
return SimpleDateFormat("yyyy年M月d日 EEEE HH:mm:ss", Locale.CHINA).format(date)
}
// 使用线程安全的日期格式化(避免 SimpleDateFormat 的线程安全问题)
private val dateFormat by lazy {
SimpleDateFormat("yyyy年M月d日 EEEE", Locale.CHINA)
}
private val timeFormat by lazy {
SimpleDateFormat("HH:mm:ss", Locale.CHINA)
}
fun getChineseDateTimePair(date: Date = Date()): Pair<String, String> {
return dateFormat.format(date) to timeFormat.format(date)
}
// /**
// * 实时时间流(每秒更新)
// * @param intervalMillis 更新间隔(默认1秒)
// */
// fun realTimeChineseDateFlow(intervalMillis: Long = 1000) = flow {
// while (true) {
// FlowCollector.emit(getChineseDateTimePair())
// delay(intervalMillis)
// }
// }
/**
* 解析日期时间字符串
* @param timeString 格式为 "yyyy-MM-dd HH:mm:ss" 的字符串
* @return Date 对象,解析失败返回 null
*/
fun parseDateTime(timeString: String?): Date? {
return try {
if (timeString == null) return null
val format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
format.parse(timeString)
} catch (e: Exception) {
// e.printStackTrace()
// Timber.e(e.message)
null
}
}
/**
* 判断给定时间是否距离当前时间超过72小时
* @param timeInMillis 时间戳(毫秒)
* @return true 表示超过72小时,false 表示未超过
*/
fun isMoreThanHoursFromNow(timeInMillis: Long): Boolean {
val currentTime = System.currentTimeMillis()
val timeDifference = currentTime - timeInMillis
val hoursDifference = timeDifference / (1000 * 60 * 60) // 毫秒转小时
return hoursDifference >= 72
}
/**
* 获取时间间隔描述
*/
fun getTimeAgo(date: Date?): String {
if (date == null) return "未知时间"
val now = Date()
val diffMillis = now.time - date.time
// 如果是未来时间
if (diffMillis < 0) {
val futureHours = abs(diffMillis) / (1000 * 60 * 60)
return if (futureHours < 24) {
"未来 $futureHours 小时"
} else {
val days = futureHours / 24
"未来 $days"
}
}
// 过去时间
val hours = diffMillis / (1000 * 60 * 60)
return when {
hours < 1 -> "刚刚"
hours < 24 -> "${hours}小时前"
else -> {
val days = hours / 24
"${days}天前"
}
}
}
}
@@ -0,0 +1,163 @@
package com.sw.face.collect.utils
import android.content.ContentUris
import android.content.Context
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import androidx.annotation.RequiresApi
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
// import Timber.log.Timber
import java.io.File
import kotlin.io.copyTo
import kotlin.io.outputStream
import kotlin.io.use
import kotlin.text.equals
object FileUtils {
/**
* 从Uri获取File
* example: file:///data/user/0/com.sw.inbound/cache/IMG_17515262353556856678814444882273.jpg
*/
private fun getFileFromUri(context: Context, uri: Uri): File? {
//Timber.d("getFileFromUri uri = ${uri.scheme}")
return when (uri.scheme) {
"file" -> File(uri.path ?: return null)
"content" -> {
try {
val inputStream = context.contentResolver.openInputStream(uri) ?: return null
val cacheDir = context.cacheDir
val file = File.createTempFile(
"upload_${System.currentTimeMillis()}",
".jpg",
cacheDir
)
file.outputStream().use { output ->
inputStream.copyTo(output)
}
file
} catch (e: Exception) {
//Timber.e(e)
null
}
}
else -> null
}
}
/**
* 通过uri生成http请求体
*/
fun genRequestPart(context: Context, imageUri: Uri): MultipartBody.Part? {
//Timber.d("genRequestPart imageUri = $imageUri")
// 1. 从Uri获取文件
val file = getFileFromUri(context, imageUri)
return genRequestPart(file)
}
fun genRequestPart(file: File?): MultipartBody.Part? {
if (file == null) {
//Timber.e("getFileFromUri file is null")
return null
}
// 2. 创建请求体
val requestFile = file
.asRequestBody("application/octet-stream".toMediaTypeOrNull())
val imagePart = MultipartBody.Part.createFormData(
"file",
file.name,
requestFile
)
return imagePart
}
/**
* 通过Uri删除文件
* @param context 上下文
* @param uri 文件Uri
* @return Boolean 是否删除成功
*/
fun deleteFileWithUri(context: Context, uri: Uri): Boolean {
//Timber.d("deleteFileWithUri uri = ${uri.scheme}")
return when {
// 1. 处理 content:// 类型的Uri (MediaStore)
uri.scheme.equals("content", ignoreCase = true) -> {
deleteContentUriFile(context, uri)
}
// 2. 处理 file:// 类型的Uri
uri.scheme.equals("file", ignoreCase = true) -> {
deleteFileUriFile(uri)
}
// 3. 其他情况尝试直接解析路径
else -> {
deleteFileFromPath(uri.path ?: return false)
}
}
}
// 删除Content Uri文件
private fun deleteContentUriFile(context: Context, uri: Uri): Boolean {
//Timber.d("deleteContentUriFile uri = ${uri.scheme}")
return try {
context.contentResolver.delete(uri, null, null) > 0
} catch (e: SecurityException) {
// Android 10+需要特殊处理
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
deleteMediaStoreFile(context, uri)
} else {
false
}
} catch (e: Exception) {
//Timber.e(e)
false
}
}
// Android 10+删除MediaStore文件
@RequiresApi(Build.VERSION_CODES.Q)
private fun deleteMediaStoreFile(context: Context, uri: Uri): Boolean {
//Timber.d("deleteMediaStoreFile uri = ${uri.scheme}")
val contentResolver = context.contentResolver
val projection = arrayOf(MediaStore.MediaColumns._ID)
return try {
contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val id =
cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
val contentUri = ContentUris.withAppendedId(uri, id)
contentResolver.delete(contentUri, null, null) > 0
} else {
false
}
} ?: false
} catch (e: Exception) {
//Timber.e(e)
false
}
}
// 删除File Uri文件
private fun deleteFileUriFile(uri: Uri): Boolean {
//Timber.d("deleteFileUriFile uri = $uri")
return try {
File(uri.path ?: return false).delete()
} catch (e: Exception) {
//Timber.e(e)
false
}
}
// 直接通过路径删除文件
private fun deleteFileFromPath(path: String): Boolean {
//Timber.d("deleteFileFromPath path = $path")
return try {
File(path).delete()
} catch (e: Exception) {
//Timber.e(e)
false
}
}
}
@@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.withContext
// 倒计时Flow扩展函数
fun countDownByFlow(
@@ -20,14 +21,20 @@ fun countDownByFlow(
onStart: (() -> Unit)? = null,
onFinish: (() -> Unit)? = null
): Job {
require(total > 0) { "倒计时总秒数必须大于0" }
return flow {
for (i in total downTo 0) {
for (i in total downTo 1) {
emit(i)
if (i != 0) delay(1000)
// if (i != 0)
delay(1000)
}
}.flowOn(Dispatchers.Main)
.onStart { onStart?.invoke() }
.onCompletion { onFinish?.invoke() }
.onEach { onTick.invoke(it) }
.launchIn(scope)
}
.flowOn(Dispatchers.IO)
// .flowOn(Dispatchers.Main)
.onStart { withContext(Dispatchers.Main) { onStart?.invoke() } }
.onCompletion {
withContext(Dispatchers.Main) { { onFinish?.invoke() } }
}
.onEach { withContext(Dispatchers.Main) { onTick.invoke(it) } }
.launchIn(CoroutineScope(scope.coroutineContext + Dispatchers.Main))
}
@@ -0,0 +1,125 @@
package com.sw.face.collect.utils
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import java.lang.reflect.Type
import kotlin.jvm.java
import kotlin.text.isNullOrEmpty
object GsonUtils {
// 默认的 Gson 实例
private val defaultGson: Gson by lazy {
GsonBuilder()
.setDateFormat("yyyy-MM-dd HH:mm:ss") // 设置日期格式
// .disableHtmlEscaping() // 禁止转义HTML标签
.create()
}
/**
* 获取默认配置的 Gson 实例
*/
fun getGson(): Gson = defaultGson
/**
* 将对象转换为 JSON 字符串
* @param obj 要转换的对象
* @return JSON 字符串
*/
fun toJson(obj: Any?): String {
return if (obj == null) "" else defaultGson.toJson(obj)
}
/**
* 将 JSON 字符串转换为对象
* @param json JSON 字符串
* @param clazz 目标类
* @return 转换后的对象
*/
fun <T> fromJson(json: String?, clazz: Class<T>): T? {
if (json.isNullOrEmpty()) {
return null
}
return try {
defaultGson.fromJson(json, clazz)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 将 JSON 字符串转换为对象 (支持泛型)
* @param json JSON 字符串
* @param type 类型令牌,用于获取泛型类型
* @return 转换后的对象
*/
fun <T> fromJson(json: String?, type: Type): T? {
if (json.isNullOrEmpty()) {
return null
}
return try {
defaultGson.fromJson(json, type)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 将 JSON 字符串转换为 List 对象
* @param json JSON 字符串
* @param clazz List 中的元素类型
* @return 转换后的 List 对象
*/
fun <T> fromJsonList(json: String?, clazz: Class<T>): List<T>? {
if (json.isNullOrEmpty()) {
return null
}
return try {
val type = TypeToken.getParameterized(List::class.java, clazz).type
defaultGson.fromJson<List<T>>(json, type)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 将 JSON 字符串转换为 Map 对象
* @param json JSON 字符串
* @param keyClazz Map 的 key 类型
* @param valueClazz Map 的 value 类型
* @return 转换后的 Map 对象
*/
fun <K, V> fromJsonMap(
json: String?,
keyClazz: Class<K>,
valueClazz: Class<V>
): Map<K, V>? {
if (json.isNullOrEmpty()) {
return null
}
return try {
val type = TypeToken.getParameterized(Map::class.java, keyClazz, valueClazz).type
defaultGson.fromJson<Map<K, V>>(json, type)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 将对象转换为另一种类型的对象
* @param obj 源对象
* @param clazz 目标类型
* @return 转换后的对象
*/
fun <T> convert(obj: Any?, clazz: Class<T>): T? {
if (obj == null) {
return null
}
return fromJson(toJson(obj), clazz)
}
}
@@ -0,0 +1,159 @@
package com.sw.face.collect.utils
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.provider.Settings
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import kotlin.apply
import kotlin.collections.all
import kotlin.collections.any
import kotlin.collections.forEachIndexed
class PermissionHelper private constructor(
private val context: Context,
private val permissions: Array<String>,
internal val requestCode: Int,
private val rationale: String? = null,
private val onGranted: (() -> Unit)?
) {
companion object {
/**
* 创建权限请求构建器
*/
fun with(context: Context, permissions: Array<String>, requestCode: Int): Builder {
return Builder(context, permissions, requestCode)
}
/**
* 检查是否已授予所有权限
*/
fun checkPermissions(context: Context, permissions: Array<String>): Boolean {
return permissions.all { permission ->
ContextCompat.checkSelfPermission(
context,
permission
) == PackageManager.PERMISSION_GRANTED
}
}
/**
* 处理权限请求结果(供BaseActivity/BaseFragment调用)
* @return 是否所有权限都已授予
*/
fun handlePermissionResult(
context: Context,
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray,
permissionHelper: PermissionHelper? = null
): Boolean {
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
permissionHelper?.onGranted?.invoke()
return true
}
val permanentlyDeniedPermissions = mutableListOf<String>()
permissions.forEachIndexed { index, permission ->
if (grantResults[index] != PackageManager.PERMISSION_GRANTED) {
if (!ActivityCompat.shouldShowRequestPermissionRationale(
context as Activity,
permission
)
) {
permanentlyDeniedPermissions.add(permission)
}
}
}
return false
}
/**
* 跳转到应用设置页面
*/
fun openAppSettings(context: Context) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
if (context is Activity) {
context.startActivity(intent)
} else if (context is Fragment) {
context.startActivity(intent)
}
}
}
class Builder(
private val context: Context,
private val permissions: Array<String>,
private val requestCode: Int
) {
private var rationale: String? = null
private var onGranted: (() -> Unit)? = null
/**
* 设置权限说明
*/
fun setRationale(rationale: String): Builder {
this.rationale = rationale
return this
}
/**
* 设置权限授予回调
*/
fun onGranted(callback: () -> Unit): Builder {
this.onGranted = callback
return this
}
/**
* 构建PermissionHelper实例
*/
fun build(): PermissionHelper {
return PermissionHelper(context, permissions, requestCode, rationale, onGranted)
}
}
/**
* 检查并请求权限
*/
fun checkAndRequest() {
if (checkPermissions(context, permissions)) {
onGranted?.invoke()
} else {
requestPermissions()
}
}
private fun requestPermissions() {
val activity = context as? Activity ?: return
val shouldShowRationale = permissions.any { permission ->
ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
}
if (shouldShowRationale && rationale != null) {
AlertDialog.Builder(activity)
.setTitle("权限说明")
.setMessage(rationale)
.setPositiveButton("确定") { _, _ ->
doRequestPermissions(activity)
}
.setNegativeButton("取消", null)
.show()
} else {
doRequestPermissions(activity)
}
}
private fun doRequestPermissions(activity: Activity) {
ActivityCompat.requestPermissions(activity, permissions, requestCode)
}
}
@@ -0,0 +1,15 @@
package com.sw.face.collect.utils
class RegisterCallbackHandler {
private var isCallbackCalled = false
private val lock = Any()
fun setCallback(callback: () -> Unit) {
synchronized(lock) {
if (!isCallbackCalled) {
isCallbackCalled = true
callback()
}
}
}
}
@@ -0,0 +1,114 @@
package com.sw.dualscreen.utils
import android.content.Context
import androidx.core.content.edit
import com.sw.plate.App
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
/**
* 持久化工具
*/
class SPUtil private constructor(context: Context, private val spName: String) {
companion object {
@Volatile
private var instance: SPUtil? = null
fun getInstance(
context: Context = App.getContext(),
spName: String = "default_sp"
): SPUtil {
return instance ?: synchronized(this) {
instance ?: SPUtil(context.applicationContext, spName).also { instance = it }
}
}
}
private val sharedPreferences by lazy {
context.getSharedPreferences(spName, Context.MODE_PRIVATE)
}
// 基础存储方法
fun put(key: String, value: Any?) {
when (value) {
null -> remove(key) // 存入null视为删除
is String -> sharedPreferences.edit { putString(key, value) }
is Int -> sharedPreferences.edit { putInt(key, value) }
is Long -> sharedPreferences.edit { putLong(key, value) }
is Float -> sharedPreferences.edit { putFloat(key, value) }
is Boolean -> sharedPreferences.edit { putBoolean(key, value) }
is Set<*> -> sharedPreferences.edit { putStringSet(key, value as Set<String>) }
else -> throw IllegalArgumentException("Unsupported type: ${value.javaClass.name}")
}
notifyDataChanged(key)
}
@Suppress("UNCHECKED_CAST")
fun <T> get(key: String, defaultValue: T? = null): T? {
return when (defaultValue) {
is String -> sharedPreferences.getString(key, defaultValue) as T
is Int -> sharedPreferences.getInt(key, defaultValue) as T
is Long -> sharedPreferences.getLong(key, defaultValue) as T
is Float -> sharedPreferences.getFloat(key, defaultValue) as T
is Boolean -> sharedPreferences.getBoolean(key, defaultValue) as T
is Set<*> -> sharedPreferences.getStringSet(key, defaultValue as Set<String>) as T
null -> when {
sharedPreferences.contains(key) -> get(key, "") as? T // 尝试作为String获取
else -> null
}
else -> throw IllegalArgumentException("Unsupported type: ${defaultValue.javaClass.name}")
}
}
fun remove(key: String) {
if (sharedPreferences.contains(key)) {
sharedPreferences.edit { remove(key) }
notifyDataChanged(key)
}
}
fun clear() {
sharedPreferences.edit { clear() }
notifyDataChanged(null)
}
fun contains(key: String): Boolean {
return sharedPreferences.contains(key)
}
// 监听变化
private val dataChangeFlow = MutableStateFlow(0)
private fun notifyDataChanged(key: String?) {
dataChangeFlow.value++
}
fun observeKey(key: String): Flow<Any?> {
return dataChangeFlow.map { get(key) }
}
// 属性委托支持
fun int(key: String, default: Int = 0) = SpProperty(key, default)
fun long(key: String, default: Long = 0L) = SpProperty(key, default)
fun float(key: String, default: Float = 0f) = SpProperty(key, default)
fun boolean(key: String, default: Boolean = false) = SpProperty(key, default)
fun string(key: String, default: String = "") = SpProperty(key, default)
fun stringSet(key: String, default: Set<String> = emptySet()) = SpProperty(key, default)
inner class SpProperty<T>(private val key: String, private val defaultValue: T) :
ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return get(key, defaultValue) ?: defaultValue
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
put(key, value)
}
}
}
@@ -0,0 +1,427 @@
package com.sw.face.collect.utils;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Build;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class SoundPoolUtil {
private static final String TAG = SoundPoolUtil.class.getSimpleName();
private static SoundPoolUtil mSound;
private SoundPool mSoundPool;
private boolean isLoadC = false;
private Map<String, Integer> idCache;
private List<Integer> sidCache;
public SoundPoolUtil() {
idCache = new HashMap<>();
sidCache = new ArrayList<>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
AudioAttributes aab = new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build();
mSoundPool = new SoundPool.Builder()
.setMaxStreams(10)
.setAudioAttributes(aab)
.build();
} else {
mSoundPool = new SoundPool(10, AudioManager.STREAM_MUSIC, 8);
}
// mSoundPool = new SoundPool(60, AudioManager.USE_DEFAULT_STREAM_TYPE, 7);
mSoundPool.setOnLoadCompleteListener(new MyOnLoadCompleteListener());
}
public static SoundPoolUtil getInstance() {
synchronized (SoundPoolUtil.class) {
if (mSound == null) {
mSound = new SoundPoolUtil();
}
}
return mSound;
}
private int loadCompleteSize = 0;
private LoadCompletion completionListener;
public void setCompletionListener(LoadCompletion listener) {
this.completionListener = listener;
}
private class MyOnLoadCompleteListener implements SoundPool.OnLoadCompleteListener {
@Override
public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {
loadCompleteSize++;
//L.e("loadSize" + loadSize + "===" + loadCompleteSize);
if (loadSize == loadCompleteSize) {
isLoadC = true;
if (completionListener != null) {
completionListener.onCompletion();
}
}
}
}
/**
* 加载指定资源
*
* @param name
* @param path
*/
public void loadR(String name, String path) {
if (checkSoundPool()) {
if (!idCache.containsKey(name)) {
idCache.put(name, mSoundPool.load(path, 1));
}
}
}
private int loadSize = 0;
/**
* 加载指定路径列表的资源
*
* @param map
*/
public void loadR(Map<String, String> map) {
loadSize = map.size();
Set<Map.Entry<String, String>> entries = map.entrySet();
for (Map.Entry<String, String> entry : entries) {
String key = entry.getKey();
if (checkSoundPool()) {
if (!idCache.containsKey(key)) {
idCache.put(key, mSoundPool.load(entry.getValue(), 1));
}
}
}
}
/**
* 加载指定AssetFileDescriptor的资源
*
* @param name
* @param afd
*/
public void loadRF(String name, AssetFileDescriptor afd) {
if (checkSoundPool()) {
if (!idCache.containsKey(name)) {
idCache.put(name, mSoundPool.load(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength(), 1));
}
}
}
/**
* 加载指定AssetFileDescriptor列表的资源
*
* @param map
*/
public void loadRF(Map<String, AssetFileDescriptor> map) {
Set<Map.Entry<String, AssetFileDescriptor>> entries = map.entrySet();
for (Map.Entry<String, AssetFileDescriptor> entry : entries) {
String key = entry.getKey();
if (checkSoundPool()) {
if (!idCache.containsKey(key)) {
idCache.put(key, mSoundPool.load(entry.getValue().getFileDescriptor(), entry.getValue().getStartOffset(), entry.getValue().getLength(), 1));
}
}
}
}
/**
* 加载指定列表资源
*
* @param context
* @param map
*/
public void loadR(Context context, Map<String, Integer> map) {
loadSize = map.size();
Set<Map.Entry<String, Integer>> entries = map.entrySet();
for (Map.Entry<String, Integer> entry : entries) {
String key = entry.getKey();
if (checkSoundPool()) {
if (!idCache.containsKey(key)) {
idCache.put(key, mSoundPool.load(context, entry.getValue(), 1));
}
}
}
}
/**
* 加载单个音频
*
* @param context
* @param name
* @param res
*/
public void loadR(Context context, String name, int res) {
if (checkSoundPool()) {
if (!idCache.containsKey(name)) {
idCache.put(name, mSoundPool.load(context, res, 1));
}
}
}
/**
* 播放指定音频,并返用于停止、暂停、恢复的StreamId
*
* @param name
* @param times
* @return
*/
public int play(String name, int times) {
//L.e(String.format("play %s times=%s", name, times));
return this.play(name, 1, 1, 1, times, 1);
}
/**
* 播放指定音频,并指定播放次数和频率
*
* @param name
* @param times
* @param rate
* @return
*/
public int play(String name, int times, int rate) {
return this.play(name, 1, 1, 1, times, rate);
}
/**
* 播放指定音频,并指定优先级和播放频率
*
* @param name
* @param property
* @param times
* @param rate
* @return
*/
public int play(String name, int property, int times, int rate) {
return this.play(name, 1, 1, property, times, rate);
}
/**
* 播放指定音频,并指定左右声道、优先级、播放次数、播放频率
*
* @param name
* @param leftVolume
* @param rightVolume
* @param property
* @param times
* @param rate
* @return
*/
public int play(String name, float leftVolume, float rightVolume, int property, int times, int rate) {
int streamId = -1;
if (checkSoundPool()) {
//L.d(TAG, "play: " + name);
if (idCache.containsKey(name) && isLoadC) {
//L.d(TAG, "name:" + idCache.get(name));
streamId = mSoundPool.play(idCache.get(name), leftVolume, rightVolume, property, times, rate);
//L.d(TAG, "streadmId:" + streamId);
sidCache.add(streamId);
}
}
return streamId;
}
/**
* 播放指定列表的音频,并返回并返用于停止、暂停、恢复的StreamId列表
*
* @param names
* @param times
* @return
*/
public List<Integer> play(List<String> names, int times) {
return this.play(names, 1, 1, 1, times, 1);
}
/**
* 播放指定列表的音频,并返回并返用于停止、暂停、恢复的StreamId列表,指定次数和频率
*
* @param names
* @param times
* @param rate
* @return
*/
public List<Integer> play(List<String> names, int times, int rate) {
return this.play(names, 1, 1, 1, times, rate);
}
/**
* 播放指定列表的音频,并返回并返用于停止、暂停、恢复的StreamId列表,指定所有参数
*
* @param names
* @param leftVolume
* @param rightVolume
* @param property
* @param times
* @param rate
* @return
*/
public List<Integer> play(List<String> names, int leftVolume, int rightVolume, int property, int times, int rate) {
List<Integer> streamIds = new ArrayList<>();
if (checkSoundPool()) {
for (String name : names) {
if (idCache.containsKey(name) && isLoadC) {
int a = mSoundPool.play(idCache.get(name), leftVolume, rightVolume, property, times, rate);
streamIds.add(a);
sidCache.add(a);
}
}
}
return streamIds;
}
/**
* 停止指定id音频
*/
public void stop(int r) {
if (checkSoundPool()) {
mSoundPool.stop(r);
}
}
/**
* 停止指定列表音频
*/
public void stopAll() {
if (checkSoundPool()) {
for (int r : sidCache) {
mSoundPool.stop(r);
}
}
}
/**
* 暂停指定音效
*
* @param r
*/
public void pause(int r) {
if (checkSoundPool()) {
mSoundPool.pause(r);
}
}
/**
* 暂停指定列表音频
*
* @param list
*/
public void pause(List<Integer> list) {
if (checkSoundPool()) {
for (int r : list) {
mSoundPool.pause(r);
}
}
}
/**
* 暂停所有音效
*/
public void pauseAll() {
mSoundPool.autoPause();
}
/**
* 恢复指定音频播放
*
* @param r
*/
public void resume(int r) {
if (checkSoundPool()) {
mSoundPool.resume(r);
}
}
/**
* 恢复指定列表的音频
*
* @param list
*/
public void resume(List<Integer> list) {
if (checkSoundPool()) {
for (int r : list) {
mSoundPool.resume(r);
}
}
}
/**
* 恢复所有暂停的音频
*/
public void resumeAll() {
if (checkSoundPool()) {
mSoundPool.autoResume();
}
}
/**
* 卸载指定音频
*
* @param name
*/
public void unLoad(String name) {
if (checkSoundPool()) {
if (idCache.containsKey(name)) {
mSoundPool.unload(idCache.get(name));
idCache.remove(name);
}
}
}
/**
* 卸载指定列表的音频
*
* @param names
*/
public void unLoad(List<String> names) {
if (checkSoundPool()) {
for (String name : names) {
if (idCache.containsKey(name)) {
mSoundPool.unload(idCache.get(name));
idCache.remove(name);
}
}
}
}
/**
* 释放所有资源,如果想继续播放,需要重新加载资源
*/
public void release() {
if (checkSoundPool()) {
mSound = null;
mSoundPool.release();
idCache.clear();
}
}
private boolean checkSoundPool() {
if (mSoundPool != null) {
return true;
}
return false;
}
public interface LoadCompletion {
void onCompletion();
}
}
@@ -0,0 +1,20 @@
package com.sw.face.collect.utils;
import com.sw.dualscreen.utils.SPUtil;
import com.sw.face.collect.MyApp;
public class SpTool {
public static final String LAST_FACE_TIMESTAMP = "faceTimestamp";
public static long getLastFaceTimestamp() {
return SPUtil.Companion.getInstance(MyApp.getContext(), "default_sp").get(LAST_FACE_TIMESTAMP, 0L);
}
public static void setLastFaceTimestamp(long timestamp) {
SPUtil.Companion.getInstance(MyApp.getContext(), "default_sp").put(LAST_FACE_TIMESTAMP, timestamp);
}
}
@@ -0,0 +1,75 @@
package com.sw.face.collect.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.arcsoft.face.FaceInfo
import com.sw.plate.utils.arcface.face.model.CompareResult
import com.sw.plate.utils.arcface.face.model.FacePreviewInfo
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
class FaceRecognizeViewModel: RecognizeViewModel() {
// 2. 新增:识别事件触发标记(即使userId不变,也能通知)
private val _recognitionTriggered = MutableLiveData<RecognitionResult?>()
val recognitionTriggered: LiveData<RecognitionResult?> = _recognitionTriggered
// 辅助:存储当前锁定的trackId(防止重复识别)
private var lockedTrackId: Int? = null
// 防抖时间(可配置,比如3秒)
private val ANTI_SHAKE_TIME = 3000L
// 存储最后一次识别时间,用于防抖
private var lastRecognitionTime = 0L
// 识别结果数据类:包含userId+是否新触发
data class RecognitionResult(
val userId: String,
val isNewRecognition: Boolean = true
)
/**
* 核心:处理识别结果(强制触发通知)
*/
fun handleRecognitionSuccess(faceInfo: FaceInfo, userId: String) {
val currentTime = System.currentTimeMillis()
// val currentTrackId = faceInfo.trackId
val currentTrackId = faceInfo.faceId
// 1. 防抖判断:同一人脸3秒内不重复触发(可根据需求调整)
if (currentTrackId == lockedTrackId && currentTime - lastRecognitionTime < ANTI_SHAKE_TIME) {
// 可选:即使防抖,也强制通知(仅更新事件,不重复执行业务)
_recognitionTriggered.postValue(RecognitionResult(userId, isNewRecognition = false))
return
}
// 2. 重置锁定状态
lockedTrackId = currentTrackId
lastRecognitionTime = currentTime
// 3. 更新userId(触发原有observe
val entity = FaceEntity(userId, null, faceInfo.faceData)
recognizeUserId.postValue(CompareResult(entity, 9.0f))
// 4. 强制触发识别事件通知(核心:即使userId不变,也通知)
_recognitionTriggered.postValue(RecognitionResult(userId))
}
/**
* 重置识别状态(人脸离开/重新进屏时调用)
*/
fun resetRecognitionState() {
lockedTrackId = null
lastRecognitionTime = 0L
// 清空LiveData,为下次识别做准备
recognizeUserId.postValue(null)
_recognitionTriggered.postValue(null)
}
/**
* 手动触发重新识别(比如按钮点击)
*/
fun triggerReRecognition() {
resetRecognitionState()
// 可选:重置虹软SDK跟踪上下文
//faceEngine?.faceEngineReset()
}
}
@@ -0,0 +1,167 @@
package com.sw.face.collect.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sw.face.collect.network.ApiClient
import com.sw.dualscreen.utils.SPUtil
import com.sw.face.collect.base.GlobalKey
import com.sw.face.collect.model.ApiResponse
import com.sw.face.collect.model.DinnerType
import com.sw.face.collect.model.UserFaceModel
import com.sw.face.collect.repository.RemoteRepository
import com.sw.face.collect.utils.SpTool
import com.sw.plate.utils.Base64
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.FaceApi
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class MainViewModel() : ViewModel() {
companion object {
const val PAGE_SIZE = 100
}
private val faceApi: FaceApi = FaceApi()
protected val repository = RemoteRepository(ApiClient.apiService)
private var lastFaceTimestamp = 0L
protected open fun parseResponse(response: ApiResponse<*>): Boolean {
val code = response.code
if (code == "00000" || code == "200" || code == "0") {
return true
}
val message = response.msg
// Timber.d("msg = ${message}, code = $code")
ToastUtils.showToast("${message}(${code})")
return false
}
/**
* 获取人脸数据
*/
fun getUserFaceCache(pageNo: Int = 1, pageSize: Int = PAGE_SIZE) {
var currentPageNo = pageNo
// Timber.tag(TAG).d("getUserFaceCache index = $currentPageNo")
viewModelScope.launch {
//获取全量数据时,时间戳改为0
SpTool.setLastFaceTimestamp(0)
val response = repository.getUserFaceCache(currentPageNo)
if (parseResponse(response)) {
// 获取成功一次后缓存状态
SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true)
withContext(Dispatchers.Default) {
val list: List<UserFaceModel> = response.data ?: emptyList()
if (pageNo == 1 && list.isEmpty()) {
ToastUtils.showToast("查询人脸数据为空")
return@withContext
}
val faceEntity = list.map {
FaceEntity(it.userId, null, Base64.decode(it.faceFeatureStr))
}
faceApi.updateFaceData(currentPageNo, faceEntity)
if (list.size >= pageSize) {
lastFaceTimestamp = list.last().faceUpdateTimestamp ?: 0L
currentPageNo++
getUserFaceCache(currentPageNo)
return@withContext
}
//查询完成所有数据,保存时间戳
if (list.isNotEmpty()) {
//非空最后一条数据的是时间戳,空使用上次list最后一条时间戳
lastFaceTimestamp = list.last().faceUpdateTimestamp ?: 0L
}
SpTool.setLastFaceTimestamp(lastFaceTimestamp)
}
}
}
}
/**
* 获取人脸数据
*/
fun getFaceIncrementList(
pageNo: Int = 1,
pageSize: Int = PAGE_SIZE,
timestamp: Long,
onAllQueryFinished: () -> Unit,
onPageQueryFinished: (List<UserFaceModel>) -> Unit
) {
viewModelScope.launch {
val response = repository.getFaceIncrementList(
pageNum = pageNo.toLong(),
pageSize = pageSize.toLong(),
timestamp = timestamp
)
if (parseResponse(response)) {
withContext(Dispatchers.Default) {
val list: List<UserFaceModel> = response.data ?: emptyList()
onPageQueryFinished(list)
if (pageNo == 1 && list.isEmpty()) {
return@withContext
}
if (list.size >= pageSize) {
lastFaceTimestamp = list.last().faceUpdateTimestamp ?: 0L
getFaceIncrementList(
pageNo = pageNo + 1,
pageSize = pageSize,
timestamp = timestamp,
onAllQueryFinished = onAllQueryFinished,
onPageQueryFinished = onPageQueryFinished
)
return@withContext
}
//查询完成所有数据,保存时间戳
if (list.isNotEmpty()) {
//非空最后一条数据的是时间戳,空使用上次list最后一条时间戳
lastFaceTimestamp = list.last().faceUpdateTimestamp ?: 0L
}
SpTool.setLastFaceTimestamp(lastFaceTimestamp)
onAllQueryFinished()
}
}
}
}
/**
* 获取当前餐点类型
*/
fun getDinnerType(block: (DinnerType) -> Unit) {
viewModelScope.launch {
val response = repository.getDinnerType()
if (parseResponse(response)) {
withContext(Dispatchers.Default) {
response.data?.let {
block(it)
}
}
}
}
}
/**
* 添加人脸数据
*/
fun addUserFace(faceData: String, imageFile: File, block: (Boolean, UserFaceModel?) -> Unit) {
viewModelScope.launch {
val imageResp = repository.uploadImage(imageFile)
val imageUrl = imageResp.data?:""
if (imageUrl.isBlank()) {
block(false, null)
return@launch
}
val faceResp = repository.addUserFace(faceData, imageUrl)
val status = parseResponse(faceResp)
if (status.not()) {
block(false, null)
return@launch
}
block(true, faceResp.data)
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 KiB

+21 -21
View File
@@ -3,25 +3,25 @@
<item
android:width="1920dp"
android:height="1080dp"
android:drawable="@drawable/bg_collect_page" />
<item
android:width="1920dp"
android:height="270dp">
<shape android:shape="rectangle">
<solid android:color="#D00D269F" />
</shape>
</item>
<item
android:width="1920dp"
android:height="810dp"
android:top="270dp">
<shape android:shape="rectangle">
<gradient
android:angle="270"
android:endColor="#99407ACA"
android:startColor="#D00D269F"
android:type="linear"
android:useLevel="false" />
</shape>
</item>
android:drawable="@drawable/bg_collect_page3" />
<!-- <item-->
<!-- android:width="1920dp"-->
<!-- android:height="270dp">-->
<!-- <shape android:shape="rectangle">-->
<!-- <solid android:color="#D00D269F" />-->
<!-- </shape>-->
<!-- </item>-->
<!-- <item-->
<!-- android:width="1920dp"-->
<!-- android:height="810dp"-->
<!-- android:top="270dp">-->
<!-- <shape android:shape="rectangle">-->
<!-- <gradient-->
<!-- android:angle="270"-->
<!-- android:endColor="#99407ACA"-->
<!-- android:startColor="#D00D269F"-->
<!-- android:type="linear"-->
<!-- android:useLevel="false" />-->
<!-- </shape>-->
<!-- </item>-->
</layer-list>
+40
View File
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/ivFlag"
android:layout_width="453dp"
android:layout_height="430dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="180dp"
android:scaleType="fitCenter"
android:src="@drawable/img_init"
android:visibility="invisible"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:layout_gravity="center_horizontal"
android:gravity="center_vertical"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnStart"
android:layout_width="300dp"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="@drawable/bg_collect_btn"
android:text="点击取盘"
android:textColor="@color/white"
android:textSize="40sp"
android:textStyle="normal" />
</LinearLayout>
</LinearLayout>
+85 -26
View File
@@ -65,33 +65,28 @@
</androidx.cardview.widget.CardView>
<!-- <eightbitlab.com.blurview.BlurView-->
<!-- android:id="@+id/target"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="match_parent"-->
<!-- app:blurOverlayColor="#4482CD">-->
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:adjustViewBounds="true"
android:src="@drawable/bg_face_wrap"
android:visibility="visible" />
<!-- </eightbitlab.com.blurview.BlurView>-->
<!-- <com.google.android.material.imageview.ShapeableImageView-->
<!-- android:id="@+id/ivRecImage"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="match_parent"-->
<!-- android:scaleType="centerCrop"-->
<!-- android:visibility="gone"-->
<!-- app:shapeAppearance="@style/round10dpCornerStyle"-->
<!-- tools:src="@mipmap/ic_launcher_round" />-->
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:src="@drawable/bg_face_wrap3"
android:visibility="visible" />
</FrameLayout>
<TextView
android:id="@+id/tvSimilarValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_vertical"
android:textColor="@color/white"
android:textSize="30sp"
android:textStyle="bold"
android:visibility="visible"
tools:text="1.0" />
<TextView
android:id="@+id/tvFaceTip"
android:layout_width="wrap_content"
@@ -104,6 +99,58 @@
android:textStyle="bold"
android:visibility="visible" />
<TextView
android:id="@+id/tvToInit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_vertical"
android:textColor="@color/white"
android:textSize="60sp"
android:textStyle="bold"
android:visibility="visible"
tools:text="30s" />
<LinearLayout
android:id="@+id/llCollectButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="100dp"
android:visibility="gone">
<TextView
android:id="@+id/btnRetry"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:text="再次重试"
android:textColor="#FFCC99"
android:textSize="24sp"
android:layout_marginHorizontal="12dp"
android:layout_marginVertical="10dp"
android:paddingHorizontal="45dp"
android:gravity="center"
android:background="@drawable/bg_collect_btn"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/btnCollect"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:text="新用户采集"
android:textColor="#FFCC99"
android:textSize="24sp"
android:layout_marginHorizontal="12dp"
android:layout_marginVertical="10dp"
android:paddingHorizontal="45dp"
android:gravity="center"
android:background="@drawable/bg_collect_btn"
tools:ignore="HardcodedText" />
</LinearLayout>
<LinearLayout
android:id="@+id/layoutState"
android:layout_width="wrap_content"
@@ -131,9 +178,10 @@
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:layout_marginTop="10dp"
android:gravity="center_vertical"
android:orientation="horizontal">
android:orientation="horizontal"
android:visibility="gone">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnCollectFace"
@@ -167,4 +215,15 @@
android:layout_height="match_parent" />
</FrameLayout>
<TextView
android:id="@+id/btnClear"
android:layout_width="160dp"
android:layout_height="80dp"
android:text="清除数据"
android:textSize="26sp"
android:gravity="center"
android:textColor="@color/white"
android:layout_gravity="start|bottom"/>
</FrameLayout>
@@ -0,0 +1,180 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_collect_shape"
tools:context=".MainActivity">
<!-- <ImageView-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="match_parent"-->
<!-- android:src="@drawable/bg_collect_shape"/>-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="50dp"
android:gravity="center"
android:text="人脸采集"
android:textColor="@color/white"
android:textSize="40sp"
android:textStyle="bold" />
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="1dp"
app:dividerColor="#3391CEFF" />
<TextView
android:id="@+id/tvSimilarValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_vertical"
android:textColor="@color/white"
android:layout_marginTop="20dp"
android:textSize="30sp"
android:textStyle="bold"
android:visibility="visible"
tools:text="1.0" />
<TextView
android:id="@+id/tvFaceTip"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_gravity="center"
android:gravity="center_vertical"
android:text="请正视屏幕"
android:textColor="@color/white"
android:textSize="26sp"
android:textStyle="bold"
android:visibility="visible" />
<TextView
android:id="@+id/tvToInit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_vertical"
android:textColor="@color/white"
android:textSize="30sp"
android:textStyle="bold"
android:visibility="visible"
tools:text="30s" />
<!-- android:layout_width="640dp"-->
<FrameLayout
android:id="@+id/flCameraView"
android:layout_width="852dp"
android:layout_height="480dp"
android:layout_gravity="center_horizontal"
android:layout_marginVertical="50dp"
android:visibility="visible">
<androidx.cardview.widget.CardView
android:id="@+id/flFace"
android:layout_width="640dp"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
app:cardBackgroundColor="@android:color/transparent"
app:cardCornerRadius="10dp"
app:cardElevation="0dp">
<TextureView
android:id="@+id/dual_camera_texture_preview_rgb"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible" />
<com.sw.plate.utils.arcface.FaceRectView
android:id="@+id/dual_camera_face_rect_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.cardview.widget.CardView>
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:src="@drawable/bg_face_wrap3"
android:visibility="visible" />
</FrameLayout>
<LinearLayout
android:id="@+id/layoutState"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone">
<ProgressBar
android:id="@+id/pbCollectLoading"
android:layout_width="36dp"
android:layout_height="36dp"
android:indeterminateTint="@color/white" />
<TextView
android:id="@+id/tvCollectState"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="采集中......"
android:textColor="@color/white"
android:textSize="26sp" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnCollectFace"
android:layout_width="300dp"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="@drawable/bg_collect_btn"
android:text="开始采集"
android:textColor="@color/white"
android:textSize="40sp"
android:textStyle="normal" />
</LinearLayout>
</LinearLayout>
<FrameLayout
android:id="@+id/flRecognizeIr"
android:layout_width="4dp"
android:layout_height="3dp"
android:layout_gravity="bottom">
<TextureView
android:id="@+id/dual_camera_texture_preview_ir"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.sw.plate.utils.arcface.FaceRectView
android:id="@+id/dual_camera_face_rect_view_ir"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</FrameLayout>
@@ -0,0 +1,153 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_collect_shape"
tools:context=".MainActivity">
<!-- <ImageView-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="match_parent"-->
<!-- android:src="@drawable/bg_collect_shape"/>-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="50dp"
android:gravity="center"
android:text="人脸采集"
android:textColor="@color/white"
android:textSize="40sp"
android:textStyle="bold" />
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="1dp"
app:dividerColor="#3391CEFF" />
<!-- android:layout_width="640dp"-->
<FrameLayout
android:id="@+id/flCameraView"
android:layout_width="852dp"
android:layout_height="480dp"
android:layout_gravity="center_horizontal"
android:layout_marginVertical="50dp"
android:visibility="visible">
<androidx.cardview.widget.CardView
android:id="@+id/flFace"
android:layout_width="640dp"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
app:cardBackgroundColor="@android:color/transparent"
app:cardCornerRadius="10dp"
app:cardElevation="0dp">
<TextureView
android:id="@+id/dual_camera_texture_preview_rgb"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible" />
<com.sw.plate.utils.arcface.FaceRectView
android:id="@+id/dual_camera_face_rect_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.cardview.widget.CardView>
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:src="@drawable/bg_face_wrap3"
android:visibility="visible" />
</FrameLayout>
<TextView
android:id="@+id/tvFaceTip"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_gravity="center"
android:gravity="center_vertical"
android:text="请正视摄像头,保持面部居中于取景框内"
android:textColor="@color/white"
android:textSize="26sp"
android:textStyle="bold"
android:visibility="visible" />
<LinearLayout
android:id="@+id/layoutState"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone">
<ProgressBar
android:id="@+id/pbCollectLoading"
android:layout_width="36dp"
android:layout_height="36dp"
android:indeterminateTint="@color/white" />
<TextView
android:id="@+id/tvCollectState"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="采集中......"
android:textColor="@color/white"
android:textSize="26sp" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnCollectFace"
android:layout_width="300dp"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="@drawable/bg_collect_btn"
android:text="开始采集"
android:textColor="@color/white"
android:textSize="40sp"
android:textStyle="normal" />
</LinearLayout>
</LinearLayout>
<FrameLayout
android:id="@+id/flRecognizeIr"
android:layout_width="4dp"
android:layout_height="3dp"
android:layout_gravity="bottom">
<TextureView
android:id="@+id/dual_camera_texture_preview_ir"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.sw.plate.utils.arcface.FaceRectView
android:id="@+id/dual_camera_face_rect_view_ir"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</FrameLayout>
@@ -0,0 +1,178 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_collect_shape"
tools:context=".MainActivity">
<!-- <ImageView-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="match_parent"-->
<!-- android:src="@drawable/bg_collect_shape"/>-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="50dp"
android:gravity="center"
android:text="人脸采集"
android:textColor="@color/white"
android:textSize="40sp"
android:textStyle="bold" />
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="1dp"
app:dividerColor="#3391CEFF" />
<!-- android:layout_width="640dp"-->
<FrameLayout
android:id="@+id/flCameraView"
android:layout_width="852dp"
android:layout_height="480dp"
android:layout_gravity="center_horizontal"
android:layout_marginVertical="50dp"
android:visibility="visible">
<androidx.cardview.widget.CardView
android:id="@+id/flFace"
android:layout_width="640dp"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
app:cardBackgroundColor="@android:color/transparent"
app:cardCornerRadius="10dp"
app:cardElevation="0dp">
<TextureView
android:id="@+id/dual_camera_texture_preview_rgb"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible" />
<com.sw.plate.utils.arcface.FaceRectView
android:id="@+id/dual_camera_face_rect_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.cardview.widget.CardView>
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:src="@drawable/bg_face_wrap3"
android:visibility="visible" />
</FrameLayout>
<TextView
android:id="@+id/tvSimilarValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_vertical"
android:textColor="@color/white"
android:textSize="30sp"
android:textStyle="bold"
android:visibility="visible"
tools:text="1.0" />
<TextView
android:id="@+id/tvFaceTip"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_gravity="center"
android:gravity="center_vertical"
android:text="请正视摄像头,保持面部居中于取景框内"
android:textColor="@color/white"
android:textSize="26sp"
android:textStyle="bold"
android:visibility="visible" />
<TextView
android:id="@+id/tvCountDown"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_vertical"
android:textColor="@color/white"
android:textSize="60sp"
android:textStyle="bold"
android:visibility="visible"
tools:text="30s" />
<LinearLayout
android:id="@+id/layoutState"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone">
<ProgressBar
android:id="@+id/pbCollectLoading"
android:layout_width="36dp"
android:layout_height="36dp"
android:indeterminateTint="@color/white" />
<TextView
android:id="@+id/tvCollectState"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="采集中......"
android:textColor="@color/white"
android:textSize="26sp" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnCollectFace"
android:layout_width="300dp"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="@drawable/bg_collect_btn"
android:text="开始采集"
android:textColor="@color/white"
android:textSize="40sp"
android:textStyle="normal" />
</LinearLayout>
</LinearLayout>
<FrameLayout
android:id="@+id/flRecognizeIr"
android:layout_width="4dp"
android:layout_height="3dp"
android:layout_gravity="bottom">
<TextureView
android:id="@+id/dual_camera_texture_preview_ir"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.sw.plate.utils.arcface.FaceRectView
android:id="@+id/dual_camera_face_rect_view_ir"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</FrameLayout>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+8
View File
@@ -14,6 +14,9 @@ cameraCore = "1.3.0"
kotlinxCoroutinesAndroid = "1.6.4"
lifecycleViewmodelKtx = "2.8.3"
lifecycleRuntimeKtx = "2.8.3"
retrofit = "3.0.0"
okhttp = "4.12.0"
gson = "2.13.1"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -36,6 +39,11 @@ kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutine
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
+1 -1
View File
@@ -56,7 +56,7 @@ dependencies {
// implementation("com.licheedev:android-serialport:2.1.5")
val roomVersion = "2.2.5"
implementation("androidx.room:room-runtime:$roomVersion")
api("androidx.room:room-runtime:$roomVersion")
annotationProcessor("androidx.room:room-compiler:$roomVersion")
implementation("io.reactivex.rxjava2:rxandroid:2.0.1")
@@ -47,6 +47,10 @@ public class FaceRectView extends View {
}
}
public int getRectColor() {
return paint.getColor();
}
public void clearFaceInfo() {
drawInfoList.clear();
postInvalidate();
@@ -652,21 +652,53 @@ public class FaceHelper implements FaceListener {
});
}
private int failCount = 0;
private long startTime = 0;
private void searchFace(final FaceFeature faceFeature, final Integer trackId) {
CompareResult compareResult = FaceServer.getInstance().searchFaceFeature(faceFeature, frEngine);
if (compareResult == null || compareResult.getFaceEntity() == null) {
if (startTime == 0) {
failCount = 0;
startTime = System.currentTimeMillis();
}
if (System.currentTimeMillis() - startTime > 10*1000) {
failCount = 0;
startTime = System.currentTimeMillis();
}
failCount++;
float similar;
if (compareResult != null) {
similar = compareResult.getSimilar();
} else {
similar = 0f;
}
// new Handler(Looper.getMainLooper()).post(()-> {
// try {
// Toast.makeText(App.getContext(),"识别失败"+failCount+"次,similar="+similar, Toast.LENGTH_SHORT).show();
// } catch (Exception e) {
// e.printStackTrace();
// }
// });
Log.d(TAG, "collectFace,searchFace查询失败"+failCount+"次,similar="+similar);
if (failCount >= 2) {
recognizeCallback.onRecognized(null, LivenessInfo.UNKNOWN, false);
return;
}
retryRecognizeDelayed(trackId);
return;
}
compareResult.setTrackId(trackId);
boolean pass = compareResult.getSimilar() > recognizeConfiguration.getSimilarThreshold();
compareResult.setSimilarPass(pass);
Log.d(TAG, "collectFace,searchFace: pass="+pass+",similar="+compareResult.getSimilar()+",threshold="+recognizeConfiguration.getSimilarThreshold());
recognizeCallback.onRecognized(compareResult, getRecognizeInfo(recognizeInfoMap, trackId).getLiveness(), pass);
if (pass) {
setName(trackId, "识别通过");
noticeCurrentStatus("识别通过");
changeRecognizeStatus(trackId, RequestFeatureStatus.SUCCEED);
} else {
noticeCurrentStatus("未通过:NOT_REGISTERED");
noticeCurrentStatus("未通过:NOT_REGISTERED"+compareResult.getSimilar());
retryRecognizeDelayed(trackId);
}
}
@@ -10,6 +10,16 @@ public class CompareResult {
private int compareCode;
private long cost;
private boolean similarPass;
public void setSimilarPass(boolean similarPass) {
this.similarPass = similarPass;
}
public boolean isSimilarPass() {
return similarPass;
}
public CompareResult(FaceEntity faceEntity, float similar) {
this.faceEntity = faceEntity;
this.similar = similar;
@@ -88,4 +88,10 @@ public interface FaceDao {
@Query("UPDATE sqlite_sequence SET seq = 0 WHERE name ='face'")
void resetId();
/**
* @return 删除临时用户人脸
*/
@Query("DELETE from face WHERE user_type = '2'")
int deleteTempUserFaceData();
}
@@ -2,9 +2,11 @@ package com.sw.plate.utils.arcface.facedb.entity;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.PrimaryKey;
import java.util.Arrays;
@@ -42,7 +44,19 @@ public class FaceEntity implements Parcelable {
*/
@ColumnInfo(name = "register_time")
private long registerTime;
/**
* 用户类型:1-普通会员、2-临时用户、或者其它待定类型
*/
@ColumnInfo(name = "user_type")
private String userType;
/**
* 会员编号
*/
@ColumnInfo(name = "cardNo")
private String cardNo;
@Ignore
private int trackId;//人脸追踪ID
public FaceEntity(String userName, String imagePath, byte[] featureData) {
this.userName = userName;
@@ -57,6 +71,8 @@ public class FaceEntity implements Parcelable {
this.imagePath = faceEntity.imagePath;
this.featureData = faceEntity.featureData;
this.registerTime = faceEntity.registerTime;
this.userType = faceEntity.getUserType();
this.cardNo = faceEntity.getCardNo();
}
@@ -66,6 +82,8 @@ public class FaceEntity implements Parcelable {
userName = in.readString();
imagePath = in.readString();
featureData = in.createByteArray();
userType = in.readString();
cardNo = in.readString();
}
public static final Creator<FaceEntity> CREATOR = new Creator<FaceEntity>() {
@@ -120,6 +138,32 @@ public class FaceEntity implements Parcelable {
this.registerTime = registerTime;
}
public int getTrackId() {
return trackId;
}
public void setTrackId(int trackId) {
this.trackId = trackId;
}
public String getUserType() {
return userType;
}
public void setUserType(String userType) {
this.userType = userType;
}
public String getCardNo() {
return cardNo;
}
public void setCardNo(String cardNo) {
this.cardNo = cardNo;
}
@Override
public int describeContents() {
return 0;
@@ -132,6 +176,8 @@ public class FaceEntity implements Parcelable {
dest.writeString(userName);
dest.writeString(imagePath);
dest.writeByteArray(featureData);
dest.writeString(userType);
dest.writeString(cardNo);
}
@Override
@@ -143,16 +189,18 @@ public class FaceEntity implements Parcelable {
return false;
}
FaceEntity that = (FaceEntity) o;
return faceId == that.faceId &&
registerTime == that.registerTime &&
userName.equals(that.userName) &&
imagePath.equals(that.imagePath) &&
Arrays.equals(featureData, that.featureData);
return this.faceId == that.faceId &&
this.registerTime == that.registerTime &&
TextUtils.equals(this.userName, that.userName) &&
TextUtils.equals(this.imagePath, that.imagePath) &&
Arrays.equals(featureData, that.featureData) &&
TextUtils.equals(this.userType, that.userType) &&
TextUtils.equals(this.cardNo, that.cardNo);
}
@Override
public int hashCode() {
int result = Objects.hash(faceId, registerTime, userName, imagePath);
int result = Objects.hash(faceId, registerTime, userName, imagePath, userType, cardNo);
result = 31 * result + Arrays.hashCode(featureData);
return result;
}
@@ -621,4 +621,10 @@ public class FaceServer {
rect.inset(-padding, -padding);
return rect;
}
public void registerFace(Context context, FaceEngine frEngine) {
List<FaceEntity> faceEntityList = FaceDatabase.getInstance(context).faceDao().getAllFaces();
registerFaceFeatureInfoListFromDb(frEngine, faceEntityList);
}
}
@@ -141,7 +141,7 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
/**
* VIDEO模式人脸检测引擎,用于预览帧人脸追踪及图像质量检测
*/
private FaceEngine ftEngine;
public FaceEngine ftEngine;
/**
* 用于特征提取的引擎
*/
@@ -283,8 +283,8 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
);
}
// 填入在设置界面设置好的配置信息
boolean enableLive = !ConfigUtil.getLivenessDetectType(context).equals(context.getString(R.string.value_liveness_type_disable));
// enableLive = false;
// boolean enableLive = !ConfigUtil.getLivenessDetectType(context).equals(context.getString(R.string.value_liveness_type_disable));
boolean enableLive = false;
boolean enableFaceQualityDetect = ConfigUtil.isEnableImageQualityDetect(context);
boolean enableFaceMoveLimit = ConfigUtil.isEnableFaceMoveLimit(context);
boolean enableFaceSizeLimit = ConfigUtil.isEnableFaceSizeLimit(context);
@@ -347,7 +347,14 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
}
public void refreshFaceList() {
FaceServer.getInstance().initFaceList(App.getContext(), frEngine, faceCount -> loadFaceList = true, true);
refreshFaceList(true);
}
public void refreshFaceList(boolean recognize) {
loadFaceList = false;
FaceServer.getInstance().initFaceList(App.getContext(), frEngine, faceCount -> {
Log.i(TAG, "unInitEngine: faceCount=" + faceCount);
loadFaceList = true;
}, recognize);
}
/**
@@ -472,13 +479,20 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
}
}
private String getUserId(CompareResult result) {
if (result != null && result.getFaceEntity() != null) {
return result.getFaceEntity().getUserName();
}
return null;
}
@Override
public void onRecognized(CompareResult compareResult, Integer live, boolean similarPass) {
Disposable disposable = Observable.just(true).observeOn(AndroidSchedulers.mainThread()).subscribe(aBoolean -> {
// TODO: 2026/1/21 测试使用 Observable.just(similarPass)代替Observable.just(true)---
Observable.just(similarPass).observeOn(AndroidSchedulers.mainThread()).subscribe(aBoolean -> {
Log.d(TAG, "collectFace,onRecognized: similarPass=" + similarPass + ",live=" + live + ",userId=" + getUserId(compareResult));
if (similarPass) {
if (recognizeUserId != null) {
recognizeUserId.postValue(compareResult);
}
recognizeUserId.postValue(compareResult);
boolean isAdded = false;
List<CompareResult> compareResults = compareResultList.getValue();
if (compareResults != null && !compareResults.isEmpty()) {
@@ -500,6 +514,8 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
getFaceItemEventMutableLiveData().postValue(new FaceItemEvent(compareResults.size() - 1, EventType.INSERTED));
}
}
} else {
recognizeUserId.postValue(compareResult);
}
});
}
@@ -587,7 +603,7 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
if (recognizeStatus != null) {
if (recognizeStatus == RequestFeatureStatus.FAILED) {
color = RecognizeColor.COLOR_FAILED;
}else if (recognizeStatus == RequestFeatureStatus.SUCCEED) {
} else if (recognizeStatus == RequestFeatureStatus.SUCCEED) {
color = RecognizeColor.COLOR_SUCCESS;
} else if (recognizeStatus == RequestFeatureStatus.TO_RETRY) {
color = RecognizeColor.COLOR_UNKNOWN;
@@ -665,4 +681,12 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
String[] size = ConfigUtil.getPreviewSize(App.getContext()).split("x");
return new Point(Integer.parseInt(size[0]), Integer.parseInt(size[1]));
}
public void registerFace() {
FaceServer.getInstance().registerFace(App.getContext(), frEngine);
}
public void clearAllFaces() {
FaceServer.getInstance().clearAllFaces();
}
}