Author SHA1 Message Date
lvmeng 3cbf64ec14 refactor(api): 重构人脸数据添加接口参数结构
- 将 addUserFace 接口从表单参数改为请求体参数
- 新增 FeatureBody 数据类封装人脸特征和图片URL
- 更新 RemoteRepository 中的人脸数据添加方法实现
- 优化参数传递方式,提高代码可读性
- 移除 @FormUrlEncoded 注解,改用 @Body 注解
- 调整方法参数命名,统一使用 imageUrl 命名规范
2026-07-01 15:57:34 +08:00
lvmeng d1e9c4d34f refactor(config): 更新环境配置URL映射关系
- 将TEST_BASE_URL替换为LOCAL_BASE_URL作为本地开发地址
- 将UAT_BASE_URL替换为TEST_BASE_URL作为测试环境地址
- 更新环境切换对话框中的单选按钮映射逻辑
- 修改应用启动时的设备ID判断逻辑和默认环境配置
- 移除部分临时测试代码并调整注释状态
2026-06-30 16:58:11 +08:00
lvmeng 38a00e8d34 refactor(face): 更新FaceEntity实体类并调整引擎激活逻辑
- 为FaceEntity添加默认构造函数并初始化注册时间
- 为FaceEntity的所有构造函数添加@Ignore注解
- 补充FaceEntity的Parcel序列化和反序列化逻辑
- 完善FaceEntity的getter和setter方法实现
- 更新equals和hashCode方法以包含新增字段
- 将人脸引擎激活从userViewModel切换到netViewModelV2
- 添加lint配置文件忽略通知权限检查警告
2026-06-30 10:31:06 +08:00
lvmeng 95b4f5e8f2 feat(network): 添加API V2版本支持并重构网络层
- 新增ApiServiceV2接口定义,支持人脸缓存、增量数据、设备配置等功能
- 添加NetViewModelV2视图模型,实现新版本API的数据处理逻辑
- 创建RemoteRepositoryV2仓库,封装API V2的网络请求操作
- 新增UserFaceModelV2数据模型,优化人脸特征数据结构
- 添加DeviceConfigV2设备配置模型,支持更多设备参数
- 在ApiClient中注册apiServiceV2实例
- 更新BaseActivity使用NetViewModelV2替代UserViewModel
- 修改DeviceInitActivity中的人脸数据获取和设备配置逻辑
- 重构FaceDao数据库访问对象,优化用户类型查询和删除操作
- 更新FaceEntity实体类,增加更多用户相关字段
- 移除BaseViewModel中的旧版网络基础功能代码
- 优化人脸数据同步和存储流程,提升性能表现
2026-06-30 10:08:10 +08:00
mazengfeiandClaude Sonnet 4.6 e8735db27b fix: 优化人脸采集日志及提示界面交互
- LoginByFaceActivity: 新增人脸采集关键节点调试日志
- PlcSettingActivity: 添加点击提示区域关闭提示布局功能,格式化代码
- activity_plc_setting.xml: 给提示 TextView 添加 id,字体调整为 32sp

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-06-01 18:10:16 +08:00
mazengfei e1e4bc5123 feat(plc): 添加餐盘平台复位功能和无餐盘提示界面
- 在PLC设置页面添加餐盘平台复位按钮
- 实现平台复位功能,先执行平台上升再返回营业状态
- 添加无餐盘提示界面,包含图片和文字提示
- 修改启动方法支持显示提示界面参数传递
- 在人脸识别登录页面检测到无餐盘时自动显示提示界面
2026-04-16 18:06:47 +08:00
mazengfei 18f45e7821 修改uat环境域名端口 2026-04-15 16:53:45 +08:00
mazengfei 1826ac0f60 fix(login): 修复人脸登录后餐盘状态判断逻辑
- 当检测到餐盘已空时跳转到餐盘设置页面
- 当检测到剩余餐盘时继续执行初始化操作
- 添加餐盘状态检查的日志记录功能
2026-04-14 15:02:36 +08:00
mazengfei d81381ae6f fix(login): 修复人脸识别登录后的流程处理
- 移除出盘完成日志的重复打印
- 添加餐盘已空时的页面跳转功能
2026-04-14 14:59:09 +08:00
mazengfei f35411a8a0 refactor(login): 优化人脸识别登录页面的串口操作实现
- 使用正确的实例引用调用PlcHelper的openSerialPort方法
- 在activity销毁时添加串口资源释放操作
- 改进代码格式化以提高可读性
- 确保串口资源在页面关闭时得到正确清理
2026-04-14 14:13:54 +08:00
mazengfei 93b46e38d4 refactor(login): 优化餐盘出盘及剩余状态提示逻辑
- 出盘后改为直接查询剩余餐盘状态以决定提示音播放
- result为false时播放无盘提示音并跳转首页
- result为true时播放出盘成功音效,继续查询剩余餐盘
- 新增checkRemainingPlate方法,查询餐盘状态并播放相应提示音
- 错误时跳过播放,确保流程顺畅跳转首页
- 删除之前注释的旧版餐盘检测逻辑代码
2026-04-14 14:00:17 +08:00
mazengfei ba1a364ee8 临时版本 2026-04-14 11:50:39 +08:00
mazengfei 50b1023a44 fix(login): 修复人脸识别登录界面返回时串口连接超时问题
- 每次回到识别界面时重新打开串口连接
- 防止PLC SDK内部连接超时导致出盘失败
- 添加串口开启成功和失败的日志记录
2026-04-13 18:17:21 +08:00
mazengfeiandClaude Sonnet 4.6 385ada4a0d fix(face): 修复短时间内再次识别无法触发的问题
新增 RecognizeViewModel.resetFaceState(),在 resetRecognizeState() 时
同时清空粘性 LiveData 和 FaceHelper 内部 recognizeInfoMap,
防止旧 trackId 状态残留导致识别流程无法重新触发。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 15:57:50 +08:00
mazengfei ae708eeeed 优化 2026-04-08 15:41:42 +08:00
mazengfeiandClaude Sonnet 4.6 2886e7d4a3 fix: 优化等待弹窗交互、人脸数据刷新及相机暂停逻辑
- BaseActivity: 等待对话框允许点击外部区域关闭
- LoginByFaceActivity: 恢复暂停相机时的预览帧处理
- PlcSettingActivity: 人脸数据更新成功后刷新人脸列表

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 10:41:25 +08:00
lvmeng 66a0c399ec feat(face): 添加人脸识别距离检测和环境切换时的人脸数据重置功能
- 在FaceHelper中添加人脸宽度小于200像素时的过滤逻辑,避免远距离识别人脸
- 注释掉LoginByFaceActivity中的重复识别状态判断逻辑
- 在PlcSettingActivity中添加协程支持用于异步处理人脸数据清除操作
- 实现环境切换时清空所有人脸数据并重新获取用户人脸缓存的功能
- 修改UserViewModel中回调逻辑,当第一页无数据时返回false而非true
2026-04-02 19:11:51 +08:00
27 changed files with 1128 additions and 497 deletions
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<lint>
<!-- Glide库中的NotificationTarget触发了此检查,但本kiosk应用不使用通知功能 -->
<issue id="NotificationPermission" severity="ignore" />
</lint>
@@ -38,8 +38,8 @@ object GlobalData {
/**
* 具体业务 BaseUrl
*/
const val TEST_BASE_URL: String = "http://192.168.1.201:14801"
const val UAT_BASE_URL: String = "https://dev.yixiong-tech.com:8081"
const val LOCAL_BASE_URL: String = "http://192.168.10.101:24801"
const val TEST_BASE_URL: String = "https://dev.yixiong-tech.com:8081"
const val PROD_BASE_URL: String = "https://api.dm.yixiong-tech.com:8443"
/**
@@ -42,12 +42,15 @@ class MyApp : App() {
} else {
GlobalData.appBaseUrl =
if ("14d2dad5-7d33-3ced-8a59-f14cb1ef2d21" == deviceId)
GlobalData.TEST_BASE_URL
GlobalData.LOCAL_BASE_URL
else
GlobalData.PROD_BASE_URL
// GlobalData.PROD_BASE_URL
GlobalData.TEST_BASE_URL
}
// TODO: 临时测试----------------
// GlobalData.appBaseUrl = GlobalData.TEST_BASE_URL
// GlobalData.deviceId = "79814f20-f253-397c-9da7-8430ffa5ef00"
// if ("2987f0c5-5754-33e9-b00a-251db5e2e55f" == deviceId) {
// GlobalData.deviceId = "79814f20-f253-397c-9da7-8430ffa5ef00"
// }
@@ -55,6 +58,7 @@ class MyApp : App() {
GlobalData.appVersion = AppUtil.getAppVersionCode(this).toString()
GlobalData.globalEquipmentCode = "202501171634"
// GlobalData.appBaseUrl = "http://192.168.1.201:14801"
}
}
@@ -25,11 +25,10 @@ import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import com.sw.platecabinet.R
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.ext.clickWithCoroutines
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.utils.IntervalExecutor
import com.sw.platecabinet.utils.SpTool
import com.sw.platecabinet.view.CustomLoadingDialog
import com.sw.platecabinet.viewmodel.UserViewModel
import com.sw.platecabinet.viewmodel.NetViewModelV2
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@@ -48,10 +47,8 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
// private val permissionHelpers = mutableMapOf<Int, PermissionHelper>()
// protected var keyEventHelper: ScanGunKeyEventHelper? = null
// 用户对应的viewModel
protected val userViewModel by viewModels<UserViewModel>()
public val recognizeViewModel by viewModels<RecognizeViewModel>()
public val netViewModelV2 by viewModels<NetViewModelV2>()
private var startTime: Long = 0
@@ -256,6 +253,7 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
mDialogWaiting = CustomLoadingDialog(this, view, R.style.MyDialog)
mDialogWaiting!!.show()
mDialogWaiting!!.setCancelable(true)
mDialogWaiting!!.setCanceledOnTouchOutside(true)
}
}
@@ -297,84 +295,23 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
// private val dealyMillis = 10 * 60 * 1000L
private val initialDelay = 1 * 1000L
private val dealyMillis = 10 * 1000L
fun startFaceTask(block: (List<String?>) -> Unit) {
fun startFaceTask() {
faceTaskJob =
intervalExecutor.startIntervalTaskWithInitialDelay(initialDelay, dealyMillis) {
getFaceIncrementList(action = block)
getFaceIncrementList()
}
}
private var faceTimestamp = 0L
//private var taskPageNo = 1
private fun getFaceIncrementList(pageNo: Int = 1, action: (List<String?>) -> Unit) {
private fun getFaceIncrementList(pageNo: Int = 1) {
val timestamp = SpTool.lastFaceTimestamp
userViewModel.getFaceIncrementList(
netViewModelV2.getFaceIncrementList(
pageNo = pageNo,
timestamp = timestamp
) { list ->
runOnUiThread {
if (pageNo == 1 && list.isEmpty()) {
return@runOnUiThread
}
updateFaceData(list)
val userIdList = list.map { it.userId }
action(userIdList)
if (list.isNotEmpty()) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0
}
SpTool.lastFaceTimestamp = faceTimestamp
}
}
}
private fun updateFaceData(list: List<UserFaceModel>) {
lifecycleScope.launch(Dispatchers.IO) {
val faceList = mutableListOf<FaceEntity>()
val faceDao = FaceDatabase.getInstance(this@BaseActivity).faceDao()
try {
list.forEach { model ->
if (model.faceDeleted == true) {
//删除数据
faceDao.deleteFaceById(model.userId)
} else {
// 获取该用户所有已存特征,判断当前特征是否已存在
val existList = faceDao.queryAllByUserName(model.userId)
val featureMatches =
existList.map { Base64.encode(it.featureData) == model.faceFeatureStr }
Timber.d("userId=${model.userId}, 已存记录数=${existList.size}, 各条特征是否与服务器一致=$featureMatches")
val alreadyExists = featureMatches.any { it }
if (!alreadyExists) {
//当前特征不存在,保存
runCatching { getNewFaceEntity(model) }
.onSuccess { faceList.add(it) }
.onFailure { Timber.e(it, "人脸特征解码失败: userId=${model.userId}") }
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
try {
if (faceList.isNotEmpty()) {
FaceDatabase.getInstance(this@BaseActivity).faceDao().insert(faceList)
}
) {
recognizeViewModel.refreshFaceList()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private fun getNewFaceEntity(model: UserFaceModel): FaceEntity {
return FaceEntity(
model.userId,
null,
Base64.decode(model.faceFeatureStr)
).also {
//1-会员、2-临时用户
it.userType = if (model.member) "1" else "2"
it.cardNo = model.cardNo
}
}
@@ -3,7 +3,6 @@ package com.sw.platecabinet.activity
import android.content.Intent
import androidx.lifecycle.lifecycleScope
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.FaceApi
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.databinding.ActivityDeviceInitBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
@@ -32,7 +31,7 @@ class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
//val count = FaceApi().queryFaceCount()
//if (count == 0) {
if (SpTool.firstGetFace) {
userViewModel.getUserFaceCache { status, msg ->
netViewModelV2.getUserFaceCache { status, msg ->
SpTool.firstGetFace = false
getDeviceConfig()
}
@@ -44,7 +43,7 @@ class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
}
private fun getDeviceConfig() {
userViewModel.getDeviceConfig { deviceConfig ->
netViewModelV2.getDeviceConfig(onSuccess = { deviceConfig ->
runOnUiThread {
if (deviceConfig == null) {
hideWaitingDialog()
@@ -61,7 +60,11 @@ class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
goLoginActivity()
}, 500)
}
}, onFailure = { errMsg ->
runOnUiThread {
ToastUtils.showToast(errMsg ?: "接口异常")
}
})
}
override fun registerDataChange() {
@@ -20,7 +20,6 @@ import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.arcsoft.face.ErrorInfo
import com.bumptech.glide.Glide
import com.google.gson.Gson
import com.sw.inbound.utils.DateTimeUtils
import com.sw.plate.utils.Base64
import com.sw.plate.utils.ToastUtils
@@ -33,7 +32,6 @@ 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.constants.RecognizeColor
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
@@ -49,12 +47,11 @@ import com.sw.platecabinet.dialog.CustomDialog
import com.sw.platecabinet.ext.clickWithDebounce
import com.sw.platecabinet.ext.gone
import com.sw.platecabinet.ext.visible
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.model.response.UserFaceModelV2
import com.sw.platecabinet.utils.BitmapSaver
import com.sw.platecabinet.utils.CountDownUtil
import com.sw.platecabinet.utils.Debouncer
import com.sw.platecabinet.utils.LogFileUtil
import com.sw.platecabinet.utils.PlateUtils
import com.sw.platecabinet.utils.RegisterCallbackHandler
import com.sw.platecabinet.utils.SoundPoolUtil
import com.sw.platecabinet.utils.mego.PlcCallback
@@ -93,9 +90,10 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
}
override fun initialize() {
userViewModel.activeEngine()
netViewModelV2.activeEngine()
PlcHelper.getInstance(this).openSerialPort(object : PlcCallback<String> {
PlcHelper.getInstance(this@LoginByFaceActivity)
.openSerialPort(object : PlcCallback<String> {
override fun onSuccess(result: String) {
ToastUtils.showToast("打开串口 → 成功: ${result}")
}
@@ -113,9 +111,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
EventBus.getDefault().register(this)
//开启人脸增量数据定时任务
startFaceTask {
loadLogInfo("collectFace增量接口数据用户id:${Gson().toJson(it)}")
}
startFaceTask()
val soundMap = hashMapOf<String, Int>()
soundMap[COLLECT_SUCCESS] = R.raw.collect_success
@@ -221,6 +217,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
private fun isFitData(userFaceInfo: UserFaceInfo?): Boolean {
if (userFaceInfo == null) return false
val headBmp = userFaceInfo.headBmp
loadLogInfo("collectFace,isFitData:w=${headBmp?.width},height=${headBmp?.height}")
if (headBmp == null) return false
// if (headBmp.width <= 200 || headBmp.height <= 200) {
// loadLogInfo("collectFace,isFitData:拿到人脸提取特征数据照片过小-----------")
@@ -234,9 +231,12 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
recognizeViewModel.prepareRegister()
recognizeViewModel.setOnRegisterFinishedCallback { facePreviewInfo, userFaceInfo ->
registerHandler?.setCallback {
loadLogInfo("collectFace,registerHandler?.setCallback")
if (!isCollectFace || !isCollecting.get()) {
loadLogInfo("collectFace,isCollectFace=${isCollectFace},isCollecting=${isCollecting.get()}")
return@setCallback
}
loadLogInfo("collectFace,userFaceInfo==null:${userFaceInfo==null}")
if (this.userFaceInfo == null && isFitData(userFaceInfo)) {
val headBmp = userFaceInfo.headBmp ?: run {
loadLogInfo("collectFace,isFitData:headBmp 为 null,跳过")
@@ -295,7 +295,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
}
recognizeViewModel.recognizeUserId.observe(this) { result: CompareResult? ->
loadLogInfo("collectFace,recognizeUserId observe userId = ${result?.faceEntity?.userName}")
loadLogInfo("collectFace,recognizeUserId observe userId=${result?.faceEntity?.userName},similar=${result?.similar},userType=${result?.faceEntity?.userType}")
loadFaceRecognizeResult3(result)
}
@@ -319,7 +319,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
*/
private val faceSuccessThreshold by lazy { ConfigUtil.getRecognizeThreshold(this) }
private val openPlateDebouncer = Debouncer(5000)
private val openPlateDebouncer = Debouncer(3000)
private val isCollecting = AtomicBoolean(false)
private fun loadFaceRecognizeResult3(result: CompareResult?) {
@@ -371,7 +371,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
currentUserId = userId
// 防抖,防止重复开闸
openPlateDebouncer.debounce {
openPlateDebouncer.debounce("openPlateDebouncer") {
openPlate(userId)
}
}
@@ -400,52 +400,70 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
text = "新用户采集"
}
binding.ivCollectPhoto.gone()
openPlateDebouncer.reset()
plateEnable = true
resumeCamera()
}
private var plateEnable = false
/**
* 打开餐盘柜:先获取 PLC 状态,有盘则出盘,无盘则播放提示音
* 打开餐盘柜:执行出盘,出盘后查询剩余餐盘状态并播放对应提示音
*/
private fun openPlate(userId: String?) {
loadLogInfo("collectFace,openPlate: userId=$userId,plateEnable=$plateEnable")
if (plateEnable.not()) return
loadLogInfo("collectFace,openPlate: userId=$userId")
showWaitingDialog("检测餐盘中,请稍候……")
PlcHelper.getInstance(this).getStatus(object : PlcCallback<PlcStatus> {
override fun onSuccess(result: PlcStatus) {
hideWaitingDialog()
if (result.isEmptyPlate) {
// 无餐盘,播放提示音并返回首页
loadLogInfo("collectFace,openPlate: 暂无餐盘,isEmptyPlate=trueisCollectFace=$isCollectFace")
PlcHelper.getInstance(this@LoginByFaceActivity)
.outPlate(object : PlcCallback<String> {
override fun onSuccess(result: String) {
plateEnable = false
recognizeCountDownUtil.cancelCountDown()
// result 为 false 时表示出盘后无餐盘,直接播放无盘提示音
if (result.equals("false", ignoreCase = true)) {
loadLogInfo("collectFace,openPlate: result=false,餐盘已空")
SoundPoolUtil.getInstance().play(NO_PLATE_REMIND, 0)
goInitActivity()
return
}
// 有餐盘,播放识别成功音效并调用出盘
loadLogInfo("collectFace,openPlate: 检测到餐盘,开始出盘")
loadLogInfo("collectFace,openPlate: 出盘完成,result=$result,开始查询剩余餐盘")
// result 为 true,出盘成功,再查询是否还有剩余餐盘
SoundPoolUtil.getInstance().play(RECOGNIZE_SUCCESS, 0)
plateEnable = false
recognizeCountDownUtil.cancelCountDown()
PlcHelper.getInstance(this@LoginByFaceActivity)
.outPlate(object : PlcCallback<String> {
override fun onSuccess(result: String) {
loadLogInfo("collectFace,openPlate: 出盘成功,result=$result")
goInitActivity()
checkRemainingPlate()
}
override fun onError(message: String) {
ToastUtils.showToast("出盘失败,message=$message")
loadLogInfo("collectFace,openPlate: 出盘失败,message=$message")
goInitActivity()
}
})
}
override fun onError(message: String) {
hideWaitingDialog()
// 状态获取失败,保底播放无盘提示
loadLogInfo("collectFace,openPlate: 获取PLC状态失败,message=$message")
/**
* 出盘成功后查询剩余餐盘状态,无盘则播放提示音
*/
private fun checkRemainingPlate() {
PlcHelper.getInstance(this@LoginByFaceActivity)
.getStatus(object : PlcCallback<PlcStatus> {
override fun onSuccess(result: PlcStatus) {
if (isFinishing || isDestroyed) return
loadLogInfo("collectFace,checkRemainingPlate: isEmptyPlate=${result.isEmptyPlate}")
if (result.isEmptyPlate) {
// 餐盘已空,播放无盘提示音
SoundPoolUtil.getInstance().play(NO_PLATE_REMIND, 0)
//跳转到餐盘设置页面
PlcSettingActivity.start(this@LoginByFaceActivity, showTips = true)
} else {
loadLogInfo("collectFace,checkRemainingPlate: 剩余餐盘,继续使用")
goInitActivity()
}
}
override fun onError(message: String) {
if (isFinishing || isDestroyed) return
// 状态查询失败,不影响主流程,直接跳转
loadLogInfo("collectFace,checkRemainingPlate: 获取PLC状态失败,message=$message")
goInitActivity()
}
})
@@ -467,6 +485,8 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
recognizeViewModel.destroy()
PlcHelper.getInstance(this@LoginByFaceActivity).release()
EventBus.getDefault().unregister(this)
super.onDestroy()
}
@@ -598,9 +618,10 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
binding.btnCollect.tag = ""
binding.tvSimilarValue.text = ""
retryFailCount = 0
// 人脸离开时,发送空帧清除 ViewModel 内部识别缓存,防止再次进入时使用旧数据
recognizeViewModel.onPreviewFrame(emptyFrame, true)
if (binding.llCollectButton.isVisible) {
leftDebouncer.debounce { leftCountDown() }
leftDebouncer.debounce("leftDebouncer") { leftCountDown() }
}
}
recognizeViewModel.clearLeftFace(facePreviewInfoList)
@@ -724,6 +745,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
// private var tempUserId: String = ""
override fun onResume() {
super.onResume()
plateEnable = true
try {
if (isFirstOpen) {
isFirstOpen = false
@@ -764,6 +786,9 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
plateEnable = true
collectCount = 0
// tempUserId = "${System.currentTimeMillis()}"
openPlateDebouncer.reset()
// 清空上次识别结果和 FaceHelper 内部状态,防止短时间内再次识别无法触发
recognizeViewModel.resetFaceState()
resumeCamera()
registerHandler = RegisterCallbackHandler()
@@ -772,13 +797,13 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
lifecycleScope = lifecycleScope,
onTick = { remaining ->
loadLogInfo("resetRecognizeState剩余时间:$remaining")
if (judgeRecognizeState()) return@startCountDown
// if (judgeRecognizeState()) return@startCountDown
loadLogInfo("initCountTime onTick = $remaining")
binding.tvToInit.text = "$remaining"
},
onFinish = {
loadLogInfo("resetRecognizeState倒计时结束")
if (judgeRecognizeState()) return@startCountDown
// if (judgeRecognizeState()) return@startCountDown
if (facePreviewInfoList.isNullOrEmpty()) {
goInitActivity()
} else {
@@ -799,23 +824,23 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
private fun pauseCamera() {
isRecognition = false
// recognizeViewModel.onPreviewFrame(emptyFrame, true)
recognizeViewModel.onPreviewFrame(emptyFrame, true)
}
private fun judgeRecognizeState(): Boolean {
val rectColor = binding.dualCameraFaceRectView.rectColor
if (rectColor == RecognizeColor.COLOR_SUCCESS && currentUserId.isNullOrBlank()
.not() && facePreviewInfoList.isNullOrEmpty().not()
) {
//人未离开,再次识别
loadLogInfo("userId=$currentUserId")
openPlateDebouncer.debounce {
openPlate(currentUserId)
}
return true
}
return false
}
// private fun judgeRecognizeState(): Boolean {
// val rectColor = binding.dualCameraFaceRectView.rectColor
// if (rectColor == RecognizeColor.COLOR_SUCCESS && currentUserId.isNullOrBlank()
// .not() && facePreviewInfoList.isNullOrEmpty().not()
// ) {
// //人未离开,再次识别
// loadLogInfo("userId=$currentUserId")
// openPlateDebouncer.debounce {
// openPlate(currentUserId)
// }
// return true
// }
// return false
// }
fun goInitActivity() {
val intent = Intent(context, InitActivity::class.java)
@@ -852,13 +877,13 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
ToastUtils.showToast("获取人脸照片失败")
return
}
uploadFaceDebouncer.debounce {
uploadFaceDebouncer.debounce("uploadFaceDebouncer") {
addUserFace(faceData, imageFile)
}
}
private fun addUserFace(faceData: String, imageFile: File) {
userViewModel.addUserFace(faceData, imageFile) { status, faceModel ->
netViewModelV2.addUserFace(faceData, imageFile) { status, faceModel ->
loadLogInfo("collectFace,saveFaceInfo: ----------12,人脸数据发送完成时间:${DateTimeUtils.getDateTimeString()},结果:$status-----------")
if (status.not()) {
ToastUtils.showToast("新增数据失败")
@@ -914,18 +939,11 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
private var currentUserId: String? = null
private fun saveFaceInfo(faceModel: UserFaceModel, block: () -> Unit = {}) {
private fun saveFaceInfo(faceModel: UserFaceModelV2, block: () -> Unit = {}) {
try {
loadLogInfo("collectFace,saveFaceInfo: ----------8,保存人脸取特征数据-----------")
val faceDao = FaceDatabase.getInstance(this).faceDao()
val faceEntity = FaceEntity(
faceModel.userId,
null,
Base64.decode(faceModel.faceFeatureStr)
).also {
//1-会员、2-临时用户
it.userType = if (faceModel.member) "1" else "2"
}
val faceEntity = faceModel.toFaceEntity()
val faceId = faceDao.insert(faceEntity)
faceEntity.faceId = faceId
loadLogInfo("collectFace,saveFaceInfo: ----------10,人脸数据保存完成-----------")
@@ -3,7 +3,9 @@ package com.sw.platecabinet.activity
import android.content.Context
import android.content.Intent
import android.view.inputmethod.InputMethodManager
import androidx.lifecycle.lifecycleScope
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.faceserver.FaceServer
import com.sw.platecabinet.databinding.ActivityPlcSettingBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.dialog.EnvSwitchDialog
@@ -11,6 +13,9 @@ import com.sw.platecabinet.ext.clickWithDebounce
import com.sw.platecabinet.utils.mego.PlcCallback
import com.sw.platecabinet.utils.mego.PlcHelper
import com.sw.platecabinet.utils.mego.PlcStatus
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* PLC 控制设置界面
@@ -19,14 +24,21 @@ import com.sw.platecabinet.utils.mego.PlcStatus
class PlcSettingActivity : BaseActivity<ActivityPlcSettingBinding>() {
companion object {
/** Intent Extra 键:是否显示 tips 界面 */
private const val EXTRA_SHOW_TIPS = "extra_show_tips"
/**
* 启动 PlcSettingActivity
* @param showTips 是否显示 tips 界面(暂无餐盘提示)
*/
fun start(context: Context) {
context.startActivity(Intent(context, PlcSettingActivity::class.java))
fun start(context: Context, showTips: Boolean = false) {
val intent = Intent(context, PlcSettingActivity::class.java)
intent.putExtra(EXTRA_SHOW_TIPS, showTips)
context.startActivity(intent)
}
}
override fun inflateViewBinding(): ActivityPlcSettingBinding {
return ActivityPlcSettingBinding.inflate(layoutInflater)
}
@@ -36,14 +48,40 @@ class PlcSettingActivity : BaseActivity<ActivityPlcSettingBinding>() {
}
override fun initialize() {
// 根据传入参数控制 tips 界面显示
val showTips = intent.getBooleanExtra(EXTRA_SHOW_TIPS, false)
binding.layoutTips.visibility =
if (showTips) android.view.View.VISIBLE else android.view.View.GONE
initConnectButtons()
initOutPlateButton()
initPlatformButtons()
initPlatformResetButton()
initAlarmButton()
initParamButtons()
initStatusButton()
binding.btnSwitchEnv.setOnClickListener {
EnvSwitchDialog(this).show()
EnvSwitchDialog(context = this, onEnvChanged = {
showWaitingDialog("重置人脸数据中……")
lifecycleScope.launch {
withContext(Dispatchers.IO) {
FaceServer.getInstance().clearAllFaces()
}
}
netViewModelV2.getUserFaceCache(pageNo = 1, callback = { state, errMsg ->
hideWaitingDialog()
if (state) {
recognizeViewModel.refreshFaceList()
ToastUtils.showToast("人脸数据已更新")
} else {
ToastUtils.showToast(errMsg)
}
})
}).show()
}
binding.tvTips.clickWithDebounce {
binding.layoutTips.visibility = android.view.View.GONE
}
}
@@ -161,6 +199,38 @@ class PlcSettingActivity : BaseActivity<ActivityPlcSettingBinding>() {
// ==================== 故障处理 ====================
/**
* 初始化平台复位按钮:先执行平台上升,成功后自动执行返回营业
*/
private fun initPlatformResetButton() {
binding.btnPlatformReset.clickWithDebounce {
showWaitingDialog("正在复位平台……")
PlcHelper.getInstance(this).platformUp(object : PlcCallback<String> {
override fun onSuccess(result: String) {
// 平台上升完成,继续执行返回营业
PlcHelper.getInstance(this@PlcSettingActivity)
.backToBusiness(object : PlcCallback<String> {
override fun onSuccess(result: String) {
hideWaitingDialog()
ToastUtils.showToast("平台复位成功")
finish()
}
override fun onError(message: String) {
hideWaitingDialog()
ToastUtils.showToast("返回营业失败:$message")
}
})
}
override fun onError(message: String) {
hideWaitingDialog()
ToastUtils.showToast("平台上升失败:$message")
}
})
}
}
/**
* 初始化故障复位按钮
*/
@@ -39,8 +39,8 @@ class EnvSwitchDialog(
override fun initView() {
// 根据当前 appBaseUrl 预选对应 RadioButton,不匹配则不选
when (GlobalData.appBaseUrl) {
GlobalData.TEST_BASE_URL -> binding.rbTest.isChecked = true
GlobalData.UAT_BASE_URL -> binding.rbUat.isChecked = true
GlobalData.LOCAL_BASE_URL -> binding.rbTest.isChecked = true
GlobalData.TEST_BASE_URL -> binding.rbUat.isChecked = true
GlobalData.PROD_BASE_URL -> binding.rbProd.isChecked = true
}
@@ -55,8 +55,8 @@ class EnvSwitchDialog(
return@setOnClickListener
}
val newUrl = when (binding.rgEnv.checkedRadioButtonId) {
binding.rbTest.id -> GlobalData.TEST_BASE_URL
binding.rbUat.id -> GlobalData.UAT_BASE_URL
binding.rbTest.id -> GlobalData.LOCAL_BASE_URL
binding.rbUat.id -> GlobalData.TEST_BASE_URL
binding.rbProd.id -> GlobalData.PROD_BASE_URL
else -> return@setOnClickListener
}
@@ -1,5 +1,7 @@
package com.sw.platecabinet.model
import com.google.gson.annotations.SerializedName
/**
* 错误信息 code == 200 成功
*/
@@ -19,6 +21,27 @@ data class DeviceConfig(
var arcsoftActiveKey: String? = null
)
data class DeviceConfigV2(
/** 业务服务器 BASE URL */
val appPackageUrl: String? = null,
/** 食堂/餐厅名称 */
val canteenName: String? = null,
/** 食堂/餐厅 ID(后续所有业务 API 的 restId */
val canteenId: String? = null,
/** 虹软人脸 SDK App ID */
val arcsoftAppId: String? = null,
/** 虹软人脸 SDK Key */
val arcsoftSdkKey: String? = null,
/** 虹软人脸 SDK Active Key */
val arcsoftActiveKey: String? = null,
/** MQTT 客户端服务器 IP */
val clientServerIp: String? = null,
/** 智慧食堂服务器 IP(MQTT 端口) */
val zhstServerIp: String? = null
)
data class CodeMsg(
val code: String? = "",
val msg: String? = ""
@@ -0,0 +1,6 @@
package com.sw.platecabinet.model.request
data class FeatureBody(
val url:String,
val featureChar:String
)
@@ -9,19 +9,6 @@ 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? = "",
// @SerializedName("updateTimeStamp")
// val updateTimeStamp: Long = 0,
val userId: String? = "",
val faceFeatureStr: String? = "",
val faceUpdateTimestamp: Long? = null,
@@ -32,10 +19,10 @@ data class UserFaceModel(
/**
* 会员编号
*/
val cardNo: String,
val cardNo: String? = null,
/**
* 是否会员
*/
val member: Boolean
val member: Boolean? = null
) : Parcelable
@@ -0,0 +1,44 @@
package com.sw.platecabinet.model.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import kotlinx.parcelize.Parcelize
/**
* 用户人脸信息
*/
@Parcelize
data class UserFaceModelV2(
val userFaceId: String? = null,
val userId: String? = null,
val faceFeature: String? = null,
val faceFeatureStr: String? = null,
val faceFeatureString: String? = null,
val faceUpdateTimestamp: Long? = null,
val cardNo: String? = null,
val member: Boolean? = false,
val faceDeleted: Boolean? = false,
val personType: String? = null
) : Parcelable {
fun resolveFeatureStr(): String? {
return faceFeature?.takeIf { it.isNotEmpty() }
?: faceFeatureStr?.takeIf { it.isNotEmpty() }
?: faceFeatureString?.takeIf { it.isNotEmpty() }
}
fun toFaceEntity(): FaceEntity {
val featureBase64 = resolveFeatureStr()
return FaceEntity(
userId,
android.util.Base64.decode(featureBase64, android.util.Base64.DEFAULT),
personType,
cardNo,
userId,
userFaceId,
member ?: false,
faceUpdateTimestamp ?: 0L
)
}
}
@@ -3,6 +3,7 @@ package com.sw.platecabinet.network
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.MyApp
import com.sw.platecabinet.network.api.ApiService
import com.sw.platecabinet.network.api.ApiServiceV2
import com.sw.platecabinet.network.interceptor.RequestInterceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
@@ -37,4 +38,8 @@ object ApiClient {
val apiService: ApiService by lazy {
retrofit.create(ApiService::class.java)
}
val apiServiceV2: ApiServiceV2 by lazy {
retrofit.create(ApiServiceV2::class.java)
}
}
@@ -0,0 +1,65 @@
package com.sw.platecabinet.network.api
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.model.DeviceConfigV2
import com.sw.platecabinet.model.request.FeatureBody
import com.sw.platecabinet.model.response.ApiResponse
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.model.response.UserFaceModelV2
import okhttp3.MultipartBody
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.Url
interface ApiServiceV2 {
/**
* 获取人脸缓存数据
*/
@POST
suspend fun getUserFaceCache(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/common/face/page",
@Body param: Map<String, Int>
): ApiResponse<List<UserFaceModelV2>>
/**
* 获取人脸增量数据
*/
@POST
suspend fun getFaceIncrementList(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/common/face/increment",
@Body param: Map<String, Long>
): ApiResponse<List<UserFaceModelV2>?>
/**
* 获取设备配置数据
*/
@GET
suspend fun getDeviceConfig(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/pickup/device/config"
): ApiResponse<DeviceConfigV2?>
/**
* 添加人脸数据
*/
@POST
suspend fun addUserFace(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/user/add-by-face",
@Body faceData: FeatureBody
): ApiResponse<UserFaceModelV2?>
/**
* 上传图片
*/
@Multipart
@POST
suspend fun uploadImage(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/upload",
@Part file: MultipartBody.Part,
): ApiResponse<String?>
}
@@ -0,0 +1,72 @@
package com.sw.platecabinet.repository
import com.sw.platecabinet.model.DeviceConfigV2
import com.sw.platecabinet.model.request.FeatureBody
import com.sw.platecabinet.model.response.ApiResponse
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.model.response.UserFaceModelV2
import com.sw.platecabinet.network.api.ApiServiceV2
import com.sw.platecabinet.utils.FileUtils
import java.io.File
/**
* 远程数据处理
*/
class RemoteRepositoryV2 constructor(
private val apiService: ApiServiceV2
) : BaseRepository() {
/**
* 获取人脸数据
*/
suspend fun getUserFaceCache(
pageNum: Int,
pageSize: Int = 100,
): ApiResponse<List<UserFaceModelV2>> {
return safeApiCall {
apiService.getUserFaceCache(
param = mapOf(
"pageNum" to pageNum, "pageSize" to pageSize
)
)
}
}
suspend fun getFaceIncrementList(
pageNum: Long, pageSize: Long = 100L, timestamp: Long
): ApiResponse<List<UserFaceModelV2>?> {
return safeApiCall {
apiService.getFaceIncrementList(
param = mapOf(
"pageNum" to pageNum, "pageSize" to pageSize, "timestamp" to timestamp
)
)
}
}
suspend fun getDeviceConfig(): ApiResponse<DeviceConfigV2?> {
return safeApiCall { apiService.getDeviceConfig() }
}
suspend fun addUserFace(faceData: String, imageUrl: String): ApiResponse<UserFaceModelV2?> {
return safeApiCall {
apiService.addUserFace(
faceData = FeatureBody(
url = imageUrl, featureChar = faceData
)
)
}
}
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)
}
}
}
}
@@ -1,6 +1,11 @@
package com.sw.platecabinet.utils
import timber.log.Timber
class Debouncer(private val delayMillis: Long) {
companion object {
const val TAG = "Debouncer"
}
private var lastActionTime = 0L
/**
@@ -8,13 +13,15 @@ class Debouncer(private val delayMillis: Long) {
* @param action 要执行的操作
* @return Boolean 是否执行了操作 (true=已执行, false=被防抖)
*/
fun debounce(action: () -> Unit): Boolean {
fun debounce(tag:String = TAG, action: () -> Unit): Boolean {
val currentTime = System.currentTimeMillis()
if (currentTime - lastActionTime >= delayMillis) {
Timber.tag(tag).d("collectFace,recognizeUserId防止重复操作:执行action回调")
lastActionTime = currentTime
action()
return true
}
Timber.tag(tag).d("collectFace,recognizeUserId防止重复操作-----------------------")
return false
}
@@ -19,10 +19,15 @@ import java.io.StringReader;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class LogFileUtil {
private static boolean isSaveLog = true;
public static final String FILE_STR_PATH = App.getContext().getExternalCacheDir().getAbsolutePath();
// 使用内部存储,不会被系统缓存清理策略影响
public static final String FILE_STR_PATH = new File(App.getContext().getFilesDir(), "logs").getAbsolutePath();
// 单线程执行器,保证日志串行写入,避免多线程并发竞争
private static final ExecutorService LOG_EXECUTOR = Executors.newSingleThreadExecutor();
public static final int SIZETYPE_B = 1;//获取文件大小单位为B的double值
public static final int SIZETYPE_KB = 2;//获取文件大小单位为KB的double值
@@ -610,10 +615,8 @@ public class LogFileUtil {
if (!isSaveLog) {
return;
}
new Thread() {
@Override
public void run() {
super.run();
// 提交到单线程队列,串行执行,避免并发写入竞争
LOG_EXECUTOR.execute(() -> {
StringBuilder sb = new StringBuilder();
for (String log : logs) {
sb.append("===")
@@ -625,9 +628,7 @@ public class LogFileUtil {
}
String fileName = "log" + AppUtil.formatDateGetDay(System.currentTimeMillis()) + ".txt";
LogFileUtil.saveStrFile(sb.toString(), fileName, FILE_STR_PATH, true);
}
}.start();
});
}
private static final String[][] MIME_MapTable = {
@@ -1,74 +1,74 @@
package com.sw.platecabinet.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.model.response.ApiResponse
import com.sw.platecabinet.network.ApiClient
import com.sw.platecabinet.repository.RemoteRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
abstract class BaseViewModel() : ViewModel() {
protected val repository = RemoteRepository(ApiClient.apiService)
// 显示进度条
private val _showLoading = MutableStateFlow<Boolean>(false)
val showLoading: StateFlow<Boolean> = _showLoading
/**
* 带进度条的请求
*/
protected fun launchWithLoading(block: suspend () -> Unit) {
viewModelScope.launch {
try {
_showLoading.value = true
block()
} catch (e: Exception) {
// 错误处理可被子类重写
handleError(e)
} finally {
_showLoading.value = false
}
}
}
/**
* 不带进度条的请求
*/
protected fun launch(block: suspend () -> Unit) {
viewModelScope.launch() {
try {
block()
} catch (e: Exception) {
// 错误处理可被子类重写
handleError(e)
}
}
}
protected open fun parseResponse(response: ApiResponse<*>): Boolean {
if (response.isSuccess()) {
return true
}
Timber.d("msg = ${response.msg}, code = ${response.code}")
ToastUtils.showToast("${response.msg}(${response.code})")
return false
}
fun handleError(exception: Exception) {
Timber.d("handleError ${exception.message}")
}
// fun parseEquipmentInfo(equipmentInfo: EquipmentInfo) {
//// GlobalData.appBaseUrl = equipmentInfo.appPackageUrl!!
//// GlobalData.appBaseUrl="https://vip.shuziweidao.com"
//// GlobalData.sdkKey = equipmentInfo.arcsoftSdkKey!!
//// GlobalData.appId = equipmentInfo.arcsoftAppId!!
//// GlobalData.activeKey = equipmentInfo.arcsoftActiveKey!!
//// GlobalData.restId = equipmentInfo.canteenId!!
//package com.sw.platecabinet.viewmodel
//
//import androidx.lifecycle.ViewModel
//import androidx.lifecycle.viewModelScope
//import com.sw.plate.utils.ToastUtils
//import com.sw.platecabinet.model.response.ApiResponse
//import com.sw.platecabinet.network.ApiClient
//import com.sw.platecabinet.repository.RemoteRepository
//import kotlinx.coroutines.flow.MutableStateFlow
//import kotlinx.coroutines.flow.StateFlow
//import kotlinx.coroutines.launch
//import timber.log.Timber
//
//abstract class BaseViewModel() : ViewModel() {
// protected val repository = RemoteRepository(ApiClient.apiService)
//
// // 显示进度条
// private val _showLoading = MutableStateFlow<Boolean>(false)
// val showLoading: StateFlow<Boolean> = _showLoading
//
// /**
// * 带进度条的请求
// */
// protected fun launchWithLoading(block: suspend () -> Unit) {
// viewModelScope.launch {
// try {
// _showLoading.value = true
// block()
// } catch (e: Exception) {
// // 错误处理可被子类重写
// handleError(e)
// } finally {
// _showLoading.value = false
// }
// }
// }
//
// /**
// * 不带进度条的请求
// */
// protected fun launch(block: suspend () -> Unit) {
// viewModelScope.launch() {
// try {
// block()
// } catch (e: Exception) {
// // 错误处理可被子类重写
// handleError(e)
// }
// }
// }
//
// protected open fun parseResponse(response: ApiResponse<*>): Boolean {
// if (response.isSuccess()) {
// return true
// }
// Timber.d("msg = ${response.msg}, code = ${response.code}")
// ToastUtils.showToast("${response.msg}(${response.code})")
// return false
// }
//
//
// fun handleError(exception: Exception) {
// Timber.d("handleError ${exception.message}")
// }
//
//// fun parseEquipmentInfo(equipmentInfo: EquipmentInfo) {
////// GlobalData.appBaseUrl = equipmentInfo.appPackageUrl!!
////// GlobalData.appBaseUrl="https://vip.shuziweidao.com"
////// GlobalData.sdkKey = equipmentInfo.arcsoftSdkKey!!
////// GlobalData.appId = equipmentInfo.arcsoftAppId!!
////// GlobalData.activeKey = equipmentInfo.arcsoftActiveKey!!
////// GlobalData.restId = equipmentInfo.canteenId!!
//// }
//}
}
@@ -0,0 +1,249 @@
package com.sw.platecabinet.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.arcsoft.face.ErrorInfo
import com.sw.inbound.utils.SPUtil
import com.sw.plate.App
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.FaceDatabase
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.GlobalKey
import com.sw.platecabinet.model.DeviceConfigV2
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.model.response.UserFaceModelV2
import com.sw.platecabinet.network.ApiClient
import com.sw.platecabinet.repository.RemoteRepositoryV2
import com.sw.platecabinet.utils.SpTool
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
/**
* 用户viewmodel
*/
class NetViewModelV2 : ViewModel() {
companion object {
const val PAGE_SIZE = 100
}
private val faceApi: FaceApi = FaceApi()
private val repository = RemoteRepositoryV2(ApiClient.apiServiceV2)
//fun getUserFace(
// pageNo: Int,
// pageSize: Int,
// callback: (Boolean, List<UserFaceModelV2>) -> Unit
//) {
// viewModelScope.launch {
// val response = repository.getUserFaceCache(pageNum = pageNo, pageSize = pageSize)
// if (response.isSuccess()) callback(true, response.data ?: emptyList())
// else callback(false, emptyList())
// }
//}
/**
* 获取人脸数据
*/
fun getUserFaceCache(
pageNo: Int = 1,
pageSize: Int = PAGE_SIZE,
callback: (Boolean, String?) -> Unit = { _, _ -> }
) {
var currentPageNo = pageNo
viewModelScope.launch {
val response = repository.getUserFaceCache(pageNo, pageSize)
if (response.isSuccess().not()) {
callback(false, response.msg)
return@launch
}
// 获取成功一次后缓存状态
SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true)
val list = response.data ?: emptyList()
if (pageNo == 1 && list.isEmpty()) {
callback(false, "暂无数据")
return@launch
}
val faceEntityList = list.mapNotNull { face ->
val featureBase64 = face.resolveFeatureStr()
if (featureBase64.isNullOrBlank()) null
else {
val featureData = base64ToByteArray(featureBase64)
if (featureData == null) null
else face.toFaceEntity()
}
}
withContext(Dispatchers.Default) {
if (currentPageNo == 1) {
//第一页数据删除本地数据库
faceApi.clearFaceData()
}
faceApi.updateFaceData(pageNo, faceEntityList)
if (list.size >= pageSize) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0
currentPageNo++
getUserFaceCache(
pageNo = currentPageNo, callback = callback
)
return@withContext
}
if (list.isNotEmpty()) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0
}
SpTool.lastFaceTimestamp = faceTimestamp
callback(true, "查询完成")
}
}
}
private var faceTimestamp = 0L
/**
* 激活人脸识别引擎
*/
fun activeEngine() {
Timber.d("activeEngine")
faceApi.activeEngine(
App.getContext(),
GlobalData.appId,
GlobalData.sdkKey,
GlobalData.activeKey,
object : FaceApi.ActiveCallback {
override fun onSuccess(activeCode: Int) {
Timber.d("activeEngine activeCode = $activeCode")
// ThreadUtils.launch {
viewModelScope.launch(Dispatchers.Main) {
when (activeCode) {
ErrorInfo.MOK -> {
ToastUtils.showToast("激活引擎成功")
}
ErrorInfo.MERR_ASF_ALREADY_ACTIVATED -> {
// ToastUtils.showToast("引擎已激活,无需再次激活")
}
else -> {
ToastUtils.showToast("激活引擎失败($activeCode)")
}
}
}
}
override fun onFail(e: Exception?) {
viewModelScope.launch(Dispatchers.Main) {
ToastUtils.showToast("激活引擎异常,${e?.message}")
}
}
})
}
fun getDeviceConfig(onSuccess: (DeviceConfigV2?) -> Unit, onFailure: (String?) -> Unit) {
// Timber.tag(TAG).d("getDeviceConfig")
viewModelScope.launch {
val response = repository.getDeviceConfig()
if (response.isSuccess()) {
onSuccess(response.data)
} else {
onFailure(response.msg)
}
}
}
/**
* 获取人脸数据
*/
fun getFaceIncrementList(
pageNo: Int = 1,
pageSize: Int = PAGE_SIZE,
timestamp: Long,
refreshFaceList: () -> Unit
) {
viewModelScope.launch {
val response = repository.getFaceIncrementList(
pageNum = pageNo.toLong(),
pageSize = pageSize.toLong(),
timestamp = timestamp
)
if (response.isSuccess().not()) {
return@launch
}
withContext(Dispatchers.IO) {
val list = response.data ?: emptyList()
updateFaceData(list)
if (list.isNotEmpty()) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0
}
SpTool.lastFaceTimestamp = faceTimestamp
try {
refreshFaceList()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
private fun updateFaceData(list: List<UserFaceModelV2>) {
try {
list.forEach { model ->
if (model.faceDeleted == true) {
//删除数据
faceApi.deleteByUserName(model.userId)
} else {
// 获取该用户所有已存特征,判断当前特征是否已存在
val existList = faceApi.queryAllByUserName(model.userId)
val featureMatches =
existList.map { Base64.encode(it.featureData) == model.faceFeatureStr }
Timber.d("userId=${model.userId}, 已存记录数=${existList.size}, 各条特征是否与服务器一致=$featureMatches")
val alreadyExists = featureMatches.any { it }
if (!alreadyExists) {
faceApi.insert(model.toFaceEntity())
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
/**
* 添加人脸数据
*/
fun addUserFace(faceData: String, imageFile: File, block: (Boolean, UserFaceModelV2?) -> 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)
if (faceResp.isSuccess().not()) {
block(false, null)
return@launch
}
block(true, faceResp.data)
}
}
private fun base64ToByteArray(base64: String?): ByteArray? {
base64 ?: return null
return try {
Base64.decode(base64)
} catch (e: Exception) {
null
}
}
}
@@ -1,201 +1,201 @@
package com.sw.platecabinet.viewmodel
import androidx.lifecycle.viewModelScope
import com.arcsoft.face.ErrorInfo
import com.sw.inbound.utils.SPUtil
import com.sw.plate.App
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 com.sw.platecabinet.GlobalData
import com.sw.platecabinet.GlobalKey
import com.sw.platecabinet.model.DeviceConfig
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.utils.SpTool
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
/**
* 用户viewmodel
*/
class UserViewModel : BaseViewModel() {
companion object {
const val PAGE_SIZE = 100
}
private val faceApi: FaceApi = FaceApi()
fun getUserFace(
pageNo: Int,
pageSize: Int,
callback: (List<UserFaceModel>) -> Unit
) {
launchWithLoading {
val response = repository.getUserFaceCache(pageNum = pageNo, pageSize = pageSize)
val status = parseResponse(response)
if (status) callback(response.data ?: emptyList())
else emptyList<UserFaceModel>()
}
}
/**
* 获取人脸数据
*/
fun getUserFaceCache(
pageNo: Int = 1,
pageSize: Int = PAGE_SIZE,
callback: (Boolean, String?) -> Unit = { statue, msg -> }
) {
var currentPageNo = pageNo
launchWithLoading {
val response = repository.getUserFaceCache(pageNum = pageNo, pageSize = pageSize)
val status = parseResponse(response)
if (status.not()) {
callback(false, response.msg)
return@launchWithLoading
}
// 获取成功一次后缓存状态
SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true)
val list: List<UserFaceModel> = response.data ?: emptyList()
if (pageNo == 1 && list.isEmpty()) {
callback(true, "暂无数据")
return@launchWithLoading
}
val faceEntityList = list.map {
FaceEntity(it.userId, null, Base64.decode(it.faceFeatureStr)).apply {
userType = if (it.member) "1" else "2"
}
}
withContext(Dispatchers.Default) {
if (currentPageNo == 1) {
//第一页数据删除本地数据库
faceApi.clearFaceData()
}
faceApi.updateFaceData(pageNo, faceEntityList)
if (list.size >= pageSize) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0
currentPageNo++
getUserFaceCache(
pageNo = currentPageNo,
callback = callback
)
return@withContext
}
if (list.isNotEmpty()) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0
}
SpTool.lastFaceTimestamp = faceTimestamp
callback(true, "查询完成")
}
}
}
private var faceTimestamp = 0L
/**
* 激活人脸识别引擎
*/
fun activeEngine() {
Timber.d("activeEngine")
faceApi.activeEngine(
App.getContext(),
GlobalData.appId,
GlobalData.sdkKey,
GlobalData.activeKey,
object : FaceApi.ActiveCallback {
override fun onSuccess(activeCode: Int) {
Timber.d("activeEngine activeCode = $activeCode")
// ThreadUtils.launch {
viewModelScope.launch(Dispatchers.Main) {
when (activeCode) {
ErrorInfo.MOK -> {
ToastUtils.showToast("激活引擎成功")
}
ErrorInfo.MERR_ASF_ALREADY_ACTIVATED -> {
// ToastUtils.showToast("引擎已激活,无需再次激活")
}
else -> {
ToastUtils.showToast("激活引擎失败($activeCode)")
}
}
}
}
override fun onFail(e: Exception?) {
viewModelScope.launch(Dispatchers.Main) {
ToastUtils.showToast("激活引擎异常,${e?.message}")
}
}
})
}
fun getDeviceConfig(block: (DeviceConfig?) -> Unit) {
// Timber.tag(TAG).d("getDeviceConfig")
launch {
val response = repository.getDeviceConfig()
if (parseResponse(response)) {
block(response.data)
} else {
block(null)
}
}
}
/**
* 获取人脸数据
*/
fun getFaceIncrementList(
pageNo: Int = 1,
pageSize: Int = PAGE_SIZE,
timestamp: Long,
block: (List<UserFaceModel>) -> Unit
) {
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()
block(list)
}
}
}
}
/**
* 添加人脸数据
*/
fun addUserFace(faceData: String, imageFile: File, block: (Boolean, UserFaceModel?) -> Unit) {
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)
}
}
}
//package com.sw.platecabinet.viewmodel
//
//import androidx.lifecycle.viewModelScope
//import com.arcsoft.face.ErrorInfo
//import com.sw.inbound.utils.SPUtil
//import com.sw.plate.App
//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 com.sw.platecabinet.GlobalData
//import com.sw.platecabinet.GlobalKey
//import com.sw.platecabinet.model.DeviceConfig
//import com.sw.platecabinet.model.response.UserFaceModel
//import com.sw.platecabinet.utils.SpTool
//import kotlinx.coroutines.Dispatchers
//import kotlinx.coroutines.launch
//import kotlinx.coroutines.withContext
//import timber.log.Timber
//import java.io.File
//
///**
// * 用户viewmodel
// */
//class UserViewModel : BaseViewModel() {
//
// companion object {
// const val PAGE_SIZE = 100
// }
//
// private val faceApi: FaceApi = FaceApi()
//
//
// fun getUserFace(
// pageNo: Int,
// pageSize: Int,
// callback: (List<UserFaceModel>) -> Unit
// ) {
// launchWithLoading {
// val response = repository.getUserFaceCache(pageNum = pageNo, pageSize = pageSize)
// val status = parseResponse(response)
// if (status) callback(response.data ?: emptyList())
// else emptyList<UserFaceModel>()
// }
// }
//
// /**
// * 获取人脸数据
// */
// fun getUserFaceCache(
// pageNo: Int = 1,
// pageSize: Int = PAGE_SIZE,
// callback: (Boolean, String?) -> Unit = { statue, msg -> }
// ) {
// var currentPageNo = pageNo
// launchWithLoading {
// val response = repository.getUserFaceCache(pageNum = pageNo, pageSize = pageSize)
// val status = parseResponse(response)
// if (status.not()) {
// callback(false, response.msg)
// return@launchWithLoading
// }
// // 获取成功一次后缓存状态
// SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true)
// val list: List<UserFaceModel> = response.data ?: emptyList()
// if (pageNo == 1 && list.isEmpty()) {
// callback(false, "暂无数据")
// return@launchWithLoading
// }
// val faceEntityList = list.map {
// FaceEntity(it.userId, null, Base64.decode(it.faceFeatureStr)).apply {
// userType = if (it.member == true) "1" else "2"
// }
// }
// withContext(Dispatchers.Default) {
// if (currentPageNo == 1) {
// //第一页数据删除本地数据库
// faceApi.clearFaceData()
// }
// faceApi.updateFaceData(pageNo, faceEntityList)
// if (list.size >= pageSize) {
// faceTimestamp = list.last().faceUpdateTimestamp ?: 0
// currentPageNo++
// getUserFaceCache(
// pageNo = currentPageNo,
// callback = callback
// )
// return@withContext
// }
// if (list.isNotEmpty()) {
// faceTimestamp = list.last().faceUpdateTimestamp ?: 0
// }
// SpTool.lastFaceTimestamp = faceTimestamp
// callback(true, "查询完成")
// }
// }
// }
//
// private var faceTimestamp = 0L
//
//
// /**
// * 激活人脸识别引擎
// */
// fun activeEngine() {
// Timber.d("activeEngine")
//
// faceApi.activeEngine(
// App.getContext(),
// GlobalData.appId,
// GlobalData.sdkKey,
// GlobalData.activeKey,
// object : FaceApi.ActiveCallback {
// override fun onSuccess(activeCode: Int) {
// Timber.d("activeEngine activeCode = $activeCode")
//// ThreadUtils.launch {
// viewModelScope.launch(Dispatchers.Main) {
// when (activeCode) {
// ErrorInfo.MOK -> {
// ToastUtils.showToast("激活引擎成功")
// }
//
// ErrorInfo.MERR_ASF_ALREADY_ACTIVATED -> {
//// ToastUtils.showToast("引擎已激活,无需再次激活")
// }
//
// else -> {
// ToastUtils.showToast("激活引擎失败($activeCode)")
// }
// }
// }
// }
//
// override fun onFail(e: Exception?) {
// viewModelScope.launch(Dispatchers.Main) {
// ToastUtils.showToast("激活引擎异常,${e?.message}")
// }
// }
// })
// }
//
//
// fun getDeviceConfig(block: (DeviceConfig?) -> Unit) {
//// Timber.tag(TAG).d("getDeviceConfig")
// launch {
// val response = repository.getDeviceConfig()
// if (parseResponse(response)) {
// block(response.data)
// } else {
// block(null)
// }
// }
// }
//
//
// /**
// * 获取人脸数据
// */
// fun getFaceIncrementList(
// pageNo: Int = 1,
// pageSize: Int = PAGE_SIZE,
// timestamp: Long,
// block: (List<UserFaceModel>) -> Unit
// ) {
// 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()
// block(list)
// }
// }
// }
// }
//
// /**
// * 添加人脸数据
// */
// fun addUserFace(faceData: String, imageFile: File, block: (Boolean, UserFaceModel?) -> Unit) {
// 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)
// }
// }
//
//}
@@ -11,6 +11,29 @@
android:id="@+id/includeHeader"
layout="@layout/item_title_time" />
<RelativeLayout
android:id="@+id/layout_tips"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@mipmap/img_error_tips"
tools:ignore="ContentDescription" />
<TextView android:id="@+id/tv_tips"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="144dp"
android:text="暂无餐盘 请联系管理人员"
android:textColor="#fff0c8b4"
android:textSize="32sp" />
</RelativeLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
@@ -143,6 +166,17 @@
android:textColor="#FFCC99"
android:textSize="22sp" />
<TextView
android:id="@+id/btnPlatformReset"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="12dp"
android:background="@drawable/btn_outline"
android:gravity="center"
android:text="餐盘平台复位"
android:textColor="#FFCC99"
android:textSize="22sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

@@ -32,7 +32,7 @@ public class FaceApi {
*/
public void updateFaceData(int index, List<FaceEntity> list) {
Log.d(TAG, "updateFaceData: index = " + index + ", listSize = " + list.size());
FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao();
FaceDao faceDao = getFaceDao();
//if (index == 1) {
// faceDao.deleteAll();
// faceDao.resetId();
@@ -41,7 +41,7 @@ public class FaceApi {
}
public void clearFaceData() {
FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao();
FaceDao faceDao = getFaceDao();
faceDao.deleteAll();
faceDao.resetId();
}
@@ -88,29 +88,33 @@ public class FaceApi {
}
public void deleteByUserName(String userName) {
FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao();
faceDao.deleteFaceById(userName);
getFaceDao().deleteFaceById(userName);
}
public Long insert(FaceEntity entity) {
if (entity == null) {
return 0L;
}
FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao();
return faceDao.insert(entity);
return getFaceDao().insert(entity);
}
public FaceEntity queryByUserName(String userName) {
if (TextUtils.isEmpty(userName)) {
return null;
}
FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao();
return faceDao.queryByUserName(userName);
return getFaceDao().queryByUserName(userName);
}
public int queryFaceCount() {
FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao();
return faceDao.getFaceCount();
return getFaceDao().getFaceCount();
}
public List<FaceEntity> queryAllByUserName(String userName) {
return getFaceDao().queryAllByUserName(userName);
}
public FaceDao getFaceDao() {
return FaceDatabase.getInstance(App.getContext()).faceDao();
}
}
@@ -343,6 +343,14 @@ public class FaceHelper implements FaceListener {
refreshTrackId(faceInfoList);
if (faceInfoList.isEmpty()) {
return facePreviewInfoList;
} else {
FaceInfo currentFaceInfo = faceInfoList.get(0);
// L.e("currentFaceInfo width=" + currentFaceInfo.getRect().width() +
// "---height" + currentFaceInfo.getRect().height());
if (currentFaceInfo.getRect().width() < 200) {//距离远(人脸小),不识别
clearLeftFace(facePreviewInfoList);
return facePreviewInfoList;
}
}
if (!onlyDetectLiveness) {
code = ftEngine.process(rgbNv21, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, faceInfoList,
@@ -104,13 +104,13 @@ public interface FaceDao {
/**
* @return 删除临时用户人脸
*/
@Query("DELETE from face WHERE user_type = '2'")
int deleteTempUserFaceData();
@Query("DELETE from face WHERE user_type = :userType")
int deleteUserFaceData(int userType);
/**
* 查询用户人脸数
* @param userType 1-会员,2-临时用户
*/
@Query("SELECT COUNT(1) FROM face WHERE user_type = :userType")
int getFaceCount(String userType);
int getFaceCountByUserType(int userType);
}
@@ -52,12 +52,39 @@ public class FaceEntity implements Parcelable {
/**
* 会员编号
*/
@ColumnInfo(name = "cardNo")
@ColumnInfo(name = "card_no")
private String cardNo;
@Ignore
private int trackId;//人脸追踪ID
@ColumnInfo(name = "user_idd")
private String userId;
@ColumnInfo(name = "user_face_id")
private String userFaceId;
@ColumnInfo(name = "member")
private boolean member;
@ColumnInfo(name = "face_update_timestamp")
private long faceUpdateTimestamp;
public FaceEntity() {
registerTime = System.currentTimeMillis();
}
@Ignore
public FaceEntity(String userName, byte[] featureData, String userType, String cardNo, String userId, String userFaceId, boolean member, long faceUpdateTimestamp) {
this.userName = userName;
this.featureData = featureData;
this.userType = userType;
this.cardNo = cardNo;
this.userId = userId;
this.userFaceId = userFaceId;
this.member = member;
this.faceUpdateTimestamp = faceUpdateTimestamp;
registerTime = System.currentTimeMillis();
}
@Ignore
public FaceEntity(String userName, String imagePath, byte[] featureData) {
this.userName = userName;
this.imagePath = imagePath;
@@ -65,6 +92,7 @@ public class FaceEntity implements Parcelable {
registerTime = System.currentTimeMillis();
}
@Ignore
public FaceEntity(FaceEntity faceEntity) {
this.faceId = faceEntity.faceId;
this.userName = faceEntity.userName;
@@ -73,9 +101,14 @@ public class FaceEntity implements Parcelable {
this.registerTime = faceEntity.registerTime;
this.userType = faceEntity.getUserType();
this.cardNo = faceEntity.getCardNo();
this.userId = faceEntity.getUserId();
this.userFaceId = faceEntity.getUserFaceId();
this.member = faceEntity.isMember();
this.faceUpdateTimestamp = faceEntity.getFaceUpdateTimestamp();
}
@Ignore
protected FaceEntity(Parcel in) {
faceId = in.readLong();
registerTime = in.readLong();
@@ -84,6 +117,10 @@ public class FaceEntity implements Parcelable {
featureData = in.createByteArray();
userType = in.readString();
cardNo = in.readString();
userId = in.readString();
userFaceId = in.readString();
member = in.readByte() != 0;
faceUpdateTimestamp = in.readLong();
}
public static final Creator<FaceEntity> CREATOR = new Creator<FaceEntity>() {
@@ -162,7 +199,37 @@ public class FaceEntity implements Parcelable {
this.cardNo = cardNo;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getUserFaceId() {
return userFaceId;
}
public void setUserFaceId(String userFaceId) {
this.userFaceId = userFaceId;
}
public boolean isMember() {
return member;
}
public void setMember(boolean member) {
this.member = member;
}
public long getFaceUpdateTimestamp() {
return faceUpdateTimestamp;
}
public void setFaceUpdateTimestamp(long faceUpdateTimestamp) {
this.faceUpdateTimestamp = faceUpdateTimestamp;
}
@Override
public int describeContents() {
@@ -178,6 +245,10 @@ public class FaceEntity implements Parcelable {
dest.writeByteArray(featureData);
dest.writeString(userType);
dest.writeString(cardNo);
dest.writeString(userId);
dest.writeString(userFaceId);
dest.writeByte((byte) (member ? 1 : 0));
dest.writeLong(faceUpdateTimestamp);
}
@Override
@@ -195,12 +266,16 @@ public class FaceEntity implements Parcelable {
TextUtils.equals(this.imagePath, that.imagePath) &&
Arrays.equals(featureData, that.featureData) &&
TextUtils.equals(this.userType, that.userType) &&
TextUtils.equals(this.cardNo, that.cardNo);
TextUtils.equals(this.cardNo, that.cardNo) &&
TextUtils.equals(this.userId, that.userId) &&
TextUtils.equals(this.userFaceId, that.userFaceId) &&
this.member == that.member &&
this.faceUpdateTimestamp == that.faceUpdateTimestamp;
}
@Override
public int hashCode() {
int result = Objects.hash(faceId, registerTime, userName, imagePath, userType, cardNo);
int result = Objects.hash(faceId, registerTime, userName, imagePath, userType, cardNo, userId, userFaceId, member, faceUpdateTimestamp);
result = 31 * result + Arrays.hashCode(featureData);
return result;
}
@@ -397,6 +397,20 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
}
}
/**
* 重置人脸识别状态:清空上次识别结果及 FaceHelper 内部的 recognizeInfoMap
* 使下一帧进入时能重新触发识别流程。
* 适用场景:短时间内再次识别、点击重试按钮等需要重新开始识别的时机。
*/
public void resetFaceState() {
// 清空粘性 LiveData,防止旧结果被重新投递给 observer
recognizeUserId.postValue(null);
// 清空 FaceHelper 内部所有人脸状态,让 trackId 对应的状态回到 TO_RETRY
if (faceHelper != null) {
faceHelper.clearFacePreviewInfoList();
}
}
/**
* 释放操作
*/