Author SHA1 Message Date
mazengfei 86c3c07e42 隐藏测试代码 2026-03-12 11:10:26 +08:00
lvmengandClaude Sonnet 4.6 aa96455f66 refactor(face): 优化人脸特征存储与增量同步逻辑
- FaceDao 新增 queryAllByUserName 查询用户全部特征记录
- 改为查询所有记录后做去重判断,支持同一用户保存多条特征数据
- 将 Thread 替换为 lifecycleScope.launch(Dispatchers.IO) 协程
- 提取 getNewFaceEntity 方法消除重复代码
- 新增特征比对调试日志

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 16:02:13 +08:00
lvmengandClaude Sonnet 4.6 786f2a123c feat(face): 人脸库为空时自动进入新用户采集模式
- 新增 newUserCollect() 方法,人脸数据库记录为零时自动显示采集界面
- onResume 中异步查询人脸数量,据此决定进入采集模式或正常识别模式
- 修复 isFirstOpen 依赖 1000ms 延迟的竞态问题,改为立即置 false
- 顺带修改 FunActivity 按钮文案为「清除并拉取全量数据」

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 15:01:39 +08:00
lvmeng f2a6de55b9 优化默认值 2026-03-10 10:47:24 +08:00
lvmeng e51864d400 优化默认值 2026-03-10 10:45:16 +08:00
lvmeng e63d3f7624 修改会员字段名称 2026-03-10 08:32:18 +08:00
lvmengandClaude Sonnet 4.6 0a110020b9 refactor(face): 重构倒计时工具,统一使用 CountDownUtil 实例化方案
- CountDownUtil 由 object 单例改为 class,各调用方持有独立实例,互不干扰
- 修复 onCompletion 取消时误触发 onFinish 的 bug
- LoginByFaceActivity 新增 recognizeCountDownUtil / leftCountDownUtil 两个实例
- 移除 CountDownManager 及其所有调用,消除两套倒计时并存的混乱

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 15:50:36 +08:00
lvmeng f99538890c 代码优化 2026-03-06 15:30:25 +08:00
lvmeng 6d8ca7c83f 人脸增量接口改为间隔10秒执行 2026-03-05 16:21:45 +08:00
mazengfei c6bcdc03ba 增量时间戳为0不再限制 2026-03-05 12:38:48 +08:00
lvmeng 3b88b011a3 全量人脸数据改为首次打开app获取 2026-03-05 09:43:41 +08:00
mazengfei b96f9872cb 优化 2026-03-04 14:56:16 +08:00
lvmengandClaude Sonnet 4.6 eef13ff56e refactor(activity): 将 refreshFaceList 移至 insert 判断块外,确保删除操作后也触发刷新
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 09:15:00 +08:00
lvmengandClaude Sonnet 4.6 6c937c5f5f fix(activity): 修复 queryUserCount 在主线程执行 Room 查询导致崩溃的问题
- DB 查询改用 withContext(Dispatchers.IO) 切换至 IO 线程
- clearFace/clearFaceAndGetAll 操作完成后自动刷新用户计数
- 删除未使用的 CoroutineDispatcher import

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 09:07:48 +08:00
lvmeng 513f4dbaea 增加查询用户数量 2026-03-04 09:03:32 +08:00
lvmengandClaude Sonnet 4.6 037816c303 fix(activity): 修复协程冗余 start() 调用并收敛方法可见性
- 移除 lifecycleScope.launch 后的 .start(),launch 已自动启动协程
- createBlackNV21Frame 和 emptyFrame 加 private 修饰符,收敛对外可见性

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 10:20:37 +08:00
lvmengandClaude Sonnet 4.6 dca9496bee refactor(activity): 全面优化 LoginByFaceActivity 代码质量
- 移除静态 instance 引用,消除潜在内存泄漏风险
- Observer {} 显式包装统一改为 Kotlin lambda 写法
- 匿名 Debouncer 改为成员变量 openDebouncer(修复防抖失效 bug)
- Thread.start() 替换为 lifecycleScope.launch(Dispatchers.IO)
- Arrays.fill() 替换为 Kotlin ByteArray.fill(),移除 Java import
- 清理死字段:regDebouncer、faceUseSwitch、faceFailThreshold、tempUserId
- Java getter 语法统一改为 Kotlin 属性访问
- 移除 protected override、无效 handleLoginSuccess override 等冗余代码
- 顶部添加 @file:Suppress("DEPRECATION") 统一处理废弃 API 警告

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 10:18:39 +08:00
lvmengandClaude Sonnet 4.6 08f1963054 refactor(network): 优化 ApiClient 代码,清理死代码并统一懒加载初始化
- 移除未使用的 DEVICE_BASE_URL 中间变量,直接引用 GlobalData.appBaseUrl
- okHttpClient 改为 private,收敛可见性
- retrofit 改为 by lazy,与 apiService 保持一致
- 简化日志拦截器 lambda 写法
- 清除所有注释掉的废弃代码

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 09:19:49 +08:00
13 changed files with 351 additions and 340 deletions
@@ -14,6 +14,7 @@ import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.google.gson.Gson import com.google.gson.Gson
import com.sw.inbound.utils.DateTimeUtils import com.sw.inbound.utils.DateTimeUtils
@@ -417,8 +418,8 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
// private val initialDelay = 5 * 60 * 1000L // private val initialDelay = 5 * 60 * 1000L
// private val dealyMillis = 10 * 60 * 1000L // private val dealyMillis = 10 * 60 * 1000L
private val initialDelay = 5 * 60 * 1000L private val initialDelay = 1 * 1000L
private val dealyMillis = 5 * 60 * 1000L private val dealyMillis = 10 * 1000L
fun startFaceTask(block: (List<String?>) -> Unit) { fun startFaceTask(block: (List<String?>) -> Unit) {
faceTaskJob = faceTaskJob =
intervalExecutor.startIntervalTaskWithInitialDelay(initialDelay, dealyMillis) { intervalExecutor.startIntervalTaskWithInitialDelay(initialDelay, dealyMillis) {
@@ -431,9 +432,9 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
//private var taskPageNo = 1 //private var taskPageNo = 1
private fun getFaceIncrementList(pageNo: Int = 1, action:(List<String?>)-> Unit) { private fun getFaceIncrementList(pageNo: Int = 1, action:(List<String?>)-> Unit) {
val timestamp = SpTool.lastFaceTimestamp val timestamp = SpTool.lastFaceTimestamp
if (timestamp == 0L) { // if (timestamp == 0L) {
return // return
} // }
settingViewModel.getFaceIncrementList( settingViewModel.getFaceIncrementList(
pageNo = pageNo, pageNo = pageNo,
timestamp = timestamp timestamp = timestamp
@@ -441,6 +442,12 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
runOnUiThread { runOnUiThread {
if (pageNo == 1 && list.isEmpty()) { if (pageNo == 1 && list.isEmpty()) {
//未查询到增量数据 //未查询到增量数据
// Thread{
// val faceDao = FaceDatabase.getInstance(this).faceDao()
// if (faceDao.getFaceCount() == 0) {
// FaceEntity("-1", null, byteArrayOf(-1)).also { it.userType = "-1" }
// }
// }.start()
return@runOnUiThread return@runOnUiThread
} }
updateFaceData(list) updateFaceData(list)
@@ -462,28 +469,26 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
public val recognizeViewModel by viewModels<RecognizeViewModel>() public val recognizeViewModel by viewModels<RecognizeViewModel>()
private fun updateFaceData(list: List<UserFaceModel>) { private fun updateFaceData(list: List<UserFaceModel>) {
Thread { lifecycleScope.launch(Dispatchers.IO) {
val faceList = mutableListOf<FaceEntity>() val faceList = mutableListOf<FaceEntity>()
val faceDao = FaceDatabase.getInstance(this).faceDao() val faceDao = FaceDatabase.getInstance(this@BaseActivity).faceDao()
try { try {
list.forEach { model -> list.forEach { model ->
if (model.faceDeleted == true) { if (model.faceDeleted == true) {
//删除数据 //删除数据
faceDao.deleteFaceById(model.userId) faceDao.deleteFaceById(model.userId)
// if (faceDao.getFaceCount() == 0) {
// FaceEntity("-1", null, byteArrayOf(-1)).also { it.userType = "-1" }
// }
} else { } else {
val faceData = faceDao.queryByUserName(model.userId) // 获取该用户所有已存特征,判断当前特征是否已存在
if (faceData == null) { val existList = faceDao.queryAllByUserName(model.userId)
//保存数据 val featureMatches = existList.map { Base64.encode(it.featureData) == model.faceFeatureStr }
val faceEntity = FaceEntity( Timber.d("userId=${model.userId}, 已存记录数=${existList.size}, 各条特征是否与服务器一致=$featureMatches")
model.userId, val alreadyExists = featureMatches.any { it }
null, if (!alreadyExists) {
Base64.decode(model.faceFeatureStr) //当前特征不存在,保存
).also { faceList.add(getNewFaceEntity(model))
//1-会员、2-临时用户
it.userType = if (model.isMember) "1" else "2"
it.cardNo = model.cardNo
}
faceList.add(faceEntity)
} }
} }
} }
@@ -492,13 +497,25 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
} }
try { try {
if (faceList.isNotEmpty()) { if (faceList.isNotEmpty()) {
FaceDatabase.getInstance(this).faceDao().insert(faceList) FaceDatabase.getInstance(this@BaseActivity).faceDao().insert(faceList)
recognizeViewModel.refreshFaceList()
} }
recognizeViewModel.refreshFaceList()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
}.start() }
}
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
}
} }
fun initPlateList() { fun initPlateList() {
@@ -518,6 +535,10 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
val faceDao = FaceDatabase.getInstance(App.getContext()).faceDao() val faceDao = FaceDatabase.getInstance(App.getContext()).faceDao()
faceDao.deleteAll() faceDao.deleteAll()
faceDao.resetId() faceDao.resetId()
// faceDao.insert(
// FaceEntity("-1", null, byteArrayOf(-1)).also { it.userType = "-1" }
// )
recognizeViewModel.refreshFaceList()
runOnUiThread { block() } runOnUiThread { block() }
}.start() }.start()
} }
@@ -7,6 +7,7 @@ import com.sw.plate.utils.arcface.FaceApi
import com.sw.platecabinet.GlobalData import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.databinding.ActivityDeviceInitBinding import com.sw.platecabinet.databinding.ActivityDeviceInitBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.utils.SpTool
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -48,9 +49,11 @@ class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
showWaitingDialog("加载中……") showWaitingDialog("加载中……")
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
val count = FaceApi().queryFaceCount() //val count = FaceApi().queryFaceCount()
if (count == 0) { //if (count == 0) {
if (SpTool.firstGetFace) {
userViewModel.getUserFaceCache { status, msg -> userViewModel.getUserFaceCache { status, msg ->
SpTool.firstGetFace = false
getDeviceConfig() getDeviceConfig()
} }
} else { } else {
@@ -1,7 +1,10 @@
package com.sw.platecabinet.activity package com.sw.platecabinet.activity
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.sw.plate.utils.Base64
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.facedb.FaceDatabase
import com.sw.platecabinet.MyApp import com.sw.platecabinet.MyApp
import com.sw.platecabinet.databinding.ActivityFunBinding import com.sw.platecabinet.databinding.ActivityFunBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding import com.sw.platecabinet.databinding.ItemTitleTimeBinding
@@ -9,11 +12,12 @@ import com.sw.platecabinet.dialog.CustomDialog
import com.sw.platecabinet.ext.clickWithDebounce import com.sw.platecabinet.ext.clickWithDebounce
import com.sw.platecabinet.utils.PlateUtils import com.sw.platecabinet.utils.PlateUtils
import com.sw.platecabinet.utils.SpTool import com.sw.platecabinet.utils.SpTool
import kotlinx.coroutines.CoroutineDispatcher import com.sw.platecabinet.viewmodel.UserViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.getValue
class FunActivity : BaseActivity<ActivityFunBinding>() { class FunActivity : BaseActivity<ActivityFunBinding>() {
@@ -32,6 +36,28 @@ class FunActivity : BaseActivity<ActivityFunBinding>() {
} }
} }
binding.btnPutPlate.clickWithDebounce { putPlate() } binding.btnPutPlate.clickWithDebounce { putPlate() }
queryUserCount()
binding.btnTestFeature.clickWithDebounce {
testFeature()
}
}
private fun testFeature() {
userViewModel.getUserFace(1, 1) {list ->
lifecycleScope.launch {
withContext(Dispatchers.IO) {
if (list.isEmpty()) return@withContext
val userId = list[0].userId
val base64Feature = list[0].faceFeatureStr
val faceDao = FaceDatabase.getInstance(this@FunActivity).faceDao()
val entityList = faceDao.queryAllByUserName(userId)
val resultList = entityList.map { base64Feature == Base64.encode(it.featureData) }
val isEqual = resultList.any { true }
ToastUtils.showToast("比较结果:$isEqual")
}
}
}
} }
private fun clearFace() { private fun clearFace() {
@@ -42,6 +68,7 @@ class FunActivity : BaseActivity<ActivityFunBinding>() {
{ {
hideWaitingDialog() hideWaitingDialog()
ToastUtils.showToast("已清除") ToastUtils.showToast("已清除")
queryUserCount()
}, 500 }, 500
) )
} }
@@ -57,6 +84,7 @@ class FunActivity : BaseActivity<ActivityFunBinding>() {
ToastUtils.showToast( ToastUtils.showToast(
if (status) "请求成功,$msg" else "请求失败,$msg" if (status) "请求成功,$msg" else "请求失败,$msg"
) )
queryUserCount()
} }
} }
} }
@@ -111,4 +139,14 @@ class FunActivity : BaseActivity<ActivityFunBinding>() {
} }
private fun queryUserCount() {
lifecycleScope.launch {
val faceDao = FaceDatabase.getInstance(this@FunActivity).faceDao()
val memberUserCount = withContext(Dispatchers.IO) { faceDao.getFaceCount("1") }
val tempUserCount = withContext(Dispatchers.IO) { faceDao.getFaceCount("2") }
binding.tvMemberUserCount.text = "会员用户:${memberUserCount}"
binding.tvTempUserCount.text = "临时用户:${tempUserCount}"
}
}
} }
@@ -1,3 +1,5 @@
@file:Suppress("DEPRECATION")
package com.sw.platecabinet.activity package com.sw.platecabinet.activity
import android.Manifest import android.Manifest
@@ -10,10 +12,8 @@ import android.util.DisplayMetrics
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
import android.widget.ImageView
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.arcsoft.face.ErrorInfo import com.arcsoft.face.ErrorInfo
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
@@ -49,7 +49,6 @@ import com.sw.platecabinet.ext.visible
import com.sw.platecabinet.model.response.EquipmentUserInfo import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.UserFaceModel import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.utils.BitmapSaver import com.sw.platecabinet.utils.BitmapSaver
import com.sw.platecabinet.utils.CountDownManager
import com.sw.platecabinet.utils.CountDownUtil import com.sw.platecabinet.utils.CountDownUtil
import com.sw.platecabinet.utils.Debouncer import com.sw.platecabinet.utils.Debouncer
import com.sw.platecabinet.utils.LogFileUtil import com.sw.platecabinet.utils.LogFileUtil
@@ -58,12 +57,14 @@ import com.sw.platecabinet.utils.PlateUtils
import com.sw.platecabinet.utils.RegisterCallbackHandler import com.sw.platecabinet.utils.RegisterCallbackHandler
import com.sw.platecabinet.utils.SoundPoolUtil import com.sw.platecabinet.utils.SoundPoolUtil
import com.sw.platecabinet.utils.SpTool import com.sw.platecabinet.utils.SpTool
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.util.Arrays
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
/** /**
@@ -89,22 +90,13 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
companion object { companion object {
private const val TAG = "LoginByFaceActivity" private const val TAG = "LoginByFaceActivity"
private var instance: LoginByFaceActivity? = null
private const val COLLECT_SUCCESS = "collect_success" private const val COLLECT_SUCCESS = "collect_success"
private const val RECOGNIZE_SUCCESS = "recognize_success" private const val RECOGNIZE_SUCCESS = "recognize_success"
private const val NO_PLATE_REMIND = "no_plate_remind"; private const val NO_PLATE_REMIND = "no_plate_remind"
fun goInitActivity() {
instance?.goInitActivity()
}
} }
// private var logList: MutableList<String> = mutableListOf()
// private val logAdapter by lazy { LogAdapter(logList) }
override fun initialize() { override fun initialize() {
instance = this
userViewModel.activeEngine() userViewModel.activeEngine()
initArcViewModel() initArcViewModel()
@@ -174,8 +166,14 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
private var rgbFaceRectTransformer: FaceRectTransformer? = null private var rgbFaceRectTransformer: FaceRectTransformer? = null
private val livenessType = LivenessType.RGB private val livenessType = LivenessType.RGB
private var openRectInfoDraw = false private var openRectInfoDraw = false
private val regDebouncer = Debouncer(3000)
private val leftDebouncer = Debouncer(30*1000) private val leftDebouncer = Debouncer(30 * 1000)
/** 识别超时倒计时(15秒无操作跳回初始页) */
private val recognizeCountDownUtil = CountDownUtil()
/** 用户离开画面后的倒计时(llCollectButton 显示状态下无人脸时触发) */
private val leftCountDownUtil = CountDownUtil()
private var registerHandler: RegisterCallbackHandler? = null private var registerHandler: RegisterCallbackHandler? = null
@@ -213,7 +211,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
} }
} }
} }
recognizeViewModel.ftInitCode.observe(this, Observer { ftInitCode: Int? -> recognizeViewModel.ftInitCode.observe(this) { ftInitCode: Int? ->
if (ftInitCode != ErrorInfo.MOK) { if (ftInitCode != ErrorInfo.MOK) {
val error: String? = getString( val error: String? = getString(
R.string.specific_engine_init_failed, "ftEngine", R.string.specific_engine_init_failed, "ftEngine",
@@ -222,8 +220,8 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
Timber.e("ftInitCode observe = $error") Timber.e("ftInitCode observe = $error")
ToastUtils.showToast(error) ToastUtils.showToast(error)
} }
}) }
recognizeViewModel.frInitCode.observe(this, Observer { frInitCode: Int? -> recognizeViewModel.frInitCode.observe(this) { frInitCode: Int? ->
if (frInitCode != ErrorInfo.MOK) { if (frInitCode != ErrorInfo.MOK) {
val error: String? = getString( val error: String? = getString(
R.string.specific_engine_init_failed, "frEngine", R.string.specific_engine_init_failed, "frEngine",
@@ -232,8 +230,8 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
Timber.e("frInitCode observe = $error") Timber.e("frInitCode observe = $error")
ToastUtils.showToast(error) ToastUtils.showToast(error)
} }
}) }
recognizeViewModel.flInitCode.observe(this, Observer { flInitCode: Int? -> recognizeViewModel.flInitCode.observe(this) { flInitCode: Int? ->
if (flInitCode != ErrorInfo.MOK) { if (flInitCode != ErrorInfo.MOK) {
val error: String? = getString( val error: String? = getString(
R.string.specific_engine_init_failed, "flEngine", R.string.specific_engine_init_failed, "flEngine",
@@ -242,31 +240,26 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
Timber.e("flInitCode observe = $error") Timber.e("flInitCode observe = $error")
ToastUtils.showToast(error) ToastUtils.showToast(error)
} }
}) }
recognizeViewModel.recognizeConfiguration recognizeViewModel.recognizeConfiguration
.observe(this, Observer { recognizeConfiguration: RecognizeConfiguration? -> .observe(this) { recognizeConfiguration: RecognizeConfiguration? ->
Timber.i("recognizeConfiguration: observe = ${recognizeConfiguration.toString()}") Timber.i("recognizeConfiguration: observe = ${recognizeConfiguration.toString()}")
}) }
recognizeViewModel.recognizeNotice.observe(this, Observer { notice: String? -> recognizeViewModel.recognizeNotice.observe(this) { notice: String? ->
Timber.i("recognizeNotice observe notice = $notice") Timber.i("recognizeNotice observe notice = $notice")
}) }
recognizeViewModel.recognizeUserId.observe(this, Observer { result: CompareResult? -> recognizeViewModel.recognizeUserId.observe(this) { result: CompareResult? ->
loadLogInfo("collectFace,recognizeUserId observe userId = ${result?.faceEntity?.userName}") loadLogInfo("collectFace,recognizeUserId observe userId = ${result?.faceEntity?.userName}")
loadFaceRecognizeResult3(result) loadFaceRecognizeResult3(result)
}) }
recognizeViewModel.drawRectInfoText.observe(this, Observer { info -> recognizeViewModel.drawRectInfoText.observe(this) { info ->
Timber.i("drawRectInfoText observe info = $info") Timber.i("drawRectInfoText observe info = $info")
}) }
} }
/**
* 人脸识别开关,true-loadFaceRecognizeResult2false-loadFaceRecognizeResult
*/
private var faceUseSwitch = true
/** /**
* 识别失败重试次数 * 识别失败重试次数
*/ */
@@ -277,11 +270,6 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
*/ */
private val maxFailCount = 5 private val maxFailCount = 5
/**
* 人脸失败阈值
*/
private val faceFailThreshold = 0.3
/** /**
* 配置阈值,默认值为0.8 * 配置阈值,默认值为0.8
*/ */
@@ -295,8 +283,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
binding.tvSimilarValue.text = "$similar${retryFailCount}" binding.tvSimilarValue.text = "$similar${retryFailCount}"
loadLogInfo( loadLogInfo(
"collectFace,recognizeUserId 阈值 success=$faceSuccessThreshold, " + "collectFace,recognizeUserId 阈值 success=$faceSuccessThreshold, similar=$similar, retry=$retryFailCount"
"fail=$faceFailThreshold, similar=$similar, retry=$retryFailCount"
) )
if (similar >= faceSuccessThreshold) { if (similar >= faceSuccessThreshold) {
val userId = result.faceEntity?.userName val userId = result.faceEntity?.userName
@@ -309,7 +296,6 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
return return
} }
// if (similar <= faceFailThreshold || retryFailCount >= maxFailCount) {
if (retryFailCount >= maxFailCount) { if (retryFailCount >= maxFailCount) {
loadLogInfo( loadLogInfo(
"collectFace,recognizeUserId 识别失败,开始采集 similar=$similar, " + "collectFace,recognizeUserId 识别失败,开始采集 similar=$similar, " +
@@ -351,7 +337,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
} }
private fun recognizeFail() { private fun recognizeFail() {
CountDownManager.cancelCountDown() recognizeCountDownUtil.cancelCountDown()
binding.tvToInit.text = "" binding.tvToInit.text = ""
binding.tvSimilarValue.text = "" binding.tvSimilarValue.text = ""
binding.llCollectButton.visible() binding.llCollectButton.visible()
@@ -363,6 +349,20 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
binding.ivCollectPhoto.visible() binding.ivCollectPhoto.visible()
} }
private fun newUserCollect() {
recognizeCountDownUtil.cancelCountDown()
binding.tvToInit.text = ""
binding.tvSimilarValue.text = ""
binding.llCollectButton.visible()
binding.btnRetry.gone()
binding.btnCollect.run {
visible()
text = "新用户采集"
}
binding.ivCollectPhoto.gone()
resumeCamera()
}
/** /**
* 查询餐盘数据 * 查询餐盘数据
*/ */
@@ -404,12 +404,12 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
item.updateTime = DateTimeUtils.getDateTimeString() item.updateTime = DateTimeUtils.getDateTimeString()
item.plateNumber = null item.plateNumber = null
plateEnable = false plateEnable = false
Debouncer(5000).debounce { openDebouncer.debounce {
//打开柜子 //打开柜子
PlateUtils.open(this, item.equipmentBoxCode, false) { PlateUtils.open(this, item.equipmentBoxCode, false) {
goInitActivity() goInitActivity()
} }
CountDownManager.cancelCountDown() recognizeCountDownUtil.cancelCountDown()
} }
currentPlateInfo = item currentPlateInfo = item
MyApp.plateList = items MyApp.plateList = items
@@ -417,21 +417,19 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
} }
} }
// private val debouncer by lazy { Debouncer(3000) } private val openDebouncer by lazy { Debouncer(5000) }
private fun initArcView() { private fun initArcView() {
//在布局结束后才做初始化操作 //在布局结束后才做初始化操作
binding.dualCameraTexturePreviewRgb.getViewTreeObserver().addOnGlobalLayoutListener(this) binding.dualCameraTexturePreviewRgb.viewTreeObserver.addOnGlobalLayoutListener(this)
recognizeViewModel.getCompareResultList().getValue() val list = recognizeViewModel.getCompareResultList().getValue()
loadLogInfo("initArcView:list.size=${list?.size}")
} }
override fun onDestroy() { override fun onDestroy() {
loadLogInfo("onDestroy") loadLogInfo("onDestroy")
instance = null rgbCameraHelper?.release()
if (rgbCameraHelper != null) { rgbCameraHelper = null
rgbCameraHelper!!.release()
rgbCameraHelper = null
}
recognizeViewModel.destroy() recognizeViewModel.destroy()
@@ -474,7 +472,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
layoutParams.width = (measuredHeight * ratio).toInt() layoutParams.width = (measuredHeight * ratio).toInt()
} }
if (scale < 1f) { if (scale < 1f) {
val rgbParam = rgbPreview.getLayoutParams() val rgbParam = rgbPreview.layoutParams
layoutParams.width = (rgbParam.width * scale).toInt() layoutParams.width = (rgbParam.width * scale).toInt()
layoutParams.height = (rgbParam.height * scale).toInt() layoutParams.height = (rgbParam.height * scale).toInt()
} else { } else {
@@ -510,8 +508,8 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
isMirror: Boolean isMirror: Boolean
) { ) {
loadLogInfo("initRgbCamera Rgb onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation") loadLogInfo("initRgbCamera Rgb onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation")
runOnUiThread({ runOnUiThread {
val previewSizeRgb = camera.getParameters().getPreviewSize() val previewSizeRgb = camera.parameters.previewSize
val layoutParams = adjustPreviewViewSize( val layoutParams = adjustPreviewViewSize(
binding.dualCameraTexturePreviewRgb, binding.dualCameraTexturePreviewRgb,
binding.dualCameraTexturePreviewRgb, binding.dualCameraFaceRectView, binding.dualCameraTexturePreviewRgb, binding.dualCameraFaceRectView,
@@ -545,7 +543,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
recognizeViewModel.onRgbCameraOpened(camera) recognizeViewModel.onRgbCameraOpened(camera)
recognizeViewModel.setRgbFaceRectTransformer(rgbFaceRectTransformer) recognizeViewModel.setRgbFaceRectTransformer(rgbFaceRectTransformer)
}) }
} }
@RequiresApi(api = Build.VERSION_CODES.Q) @RequiresApi(api = Build.VERSION_CODES.Q)
@@ -607,8 +605,8 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
.previewOn(binding.dualCameraTexturePreviewRgb) .previewOn(binding.dualCameraTexturePreviewRgb)
.cameraListener(cameraListener) .cameraListener(cameraListener)
.build() .build()
rgbCameraHelper!!.init() rgbCameraHelper?.init()
rgbCameraHelper!!.start() rgbCameraHelper?.start()
} }
/** /**
@@ -626,15 +624,15 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
} }
} }
fun openRectInfoDraw(view: View?) { // fun openRectInfoDraw(view: View?) {
openRectInfoDraw = !openRectInfoDraw // openRectInfoDraw = !openRectInfoDraw
recognizeViewModel.setDrawRectInfoTextValue(openRectInfoDraw) // recognizeViewModel.setDrawRectInfoTextValue(openRectInfoDraw)
} // }
override fun onGlobalLayout() { override fun onGlobalLayout() {
loadLogInfo("onGlobalLayout") loadLogInfo("onGlobalLayout")
binding.dualCameraTexturePreviewRgb.getViewTreeObserver().removeOnGlobalLayoutListener(this) binding.dualCameraTexturePreviewRgb.viewTreeObserver.removeOnGlobalLayoutListener(this)
// 请求权限 // 请求权限
checkCameraPermission() checkCameraPermission()
} }
@@ -663,38 +661,52 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
* @param height 图像高度(如 1080 * @param height 图像高度(如 1080
* @return 合法的全黑帧字节数组 * @return 合法的全黑帧字节数组
*/ */
fun createBlackNV21Frame(width: Int, height: Int): ByteArray { private fun createBlackNV21Frame(width: Int, height: Int): ByteArray {
val frameSize = width * height val frameSize = width * height
val nv21Data = ByteArray(frameSize * 3 / 2) // NV21 格式长度 = 宽×高×1.5 val nv21Data = ByteArray(frameSize * 3 / 2) // NV21 格式长度 = 宽×高×1.5
// Y 分量(亮度)设为 0(全黑),UV 分量设为 128(默认值) // Y 分量(亮度)设为 0(全黑),UV 分量设为 128(默认值)
// 1. 填充 Y 分量(前 frameSize 个字节) // 1. 填充 Y 分量(前 frameSize 个字节)
Arrays.fill(nv21Data, 0, frameSize, 0.toByte()) // Arrays.fill(nv21Data, 0, frameSize, 0.toByte())
nv21Data.fill(0, 0, frameSize)
// 2. 填充 UV 分量(后 frameSize/2 个字节) // 2. 填充 UV 分量(后 frameSize/2 个字节)
Arrays.fill(nv21Data, frameSize, nv21Data.size, 128.toByte()) // Arrays.fill(nv21Data, frameSize, nv21Data.size, 128.toByte())
nv21Data.fill(128.toByte(), frameSize, nv21Data.size)
return nv21Data return nv21Data
} }
// 调用示例:构造 1920×1080 的全黑帧(长度 1382400 // 调用示例:构造 1920×1080 的全黑帧(长度 1382400
val emptyFrame = createBlackNV21Frame(1920, 1080) // private val emptyFrame = createBlackNV21Frame(1920, 1080)
private val emptyFrame = createBlackNV21Frame(1280, 720)
// 传递“空帧” recognizeViewModel.onPreviewFrame(emptyFrame, true) // 传递“空帧” recognizeViewModel.onPreviewFrame(emptyFrame, true)
/** // /**
* 临时用户id // * 临时用户id
*/ // */
private var tempUserId: String = "" // private var tempUserId: String = ""
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
try { try {
if (isFirstOpen) { if (isFirstOpen) {
isFirstOpen = false
goInitActivity() goInitActivity()
binding.root.postDelayed({
isFirstOpen = false
}, 1000)
return return
} }
resetRecognizeState()
lifecycleScope.launch {
try {
val faceDao = FaceDatabase.getInstance(this@LoginByFaceActivity).faceDao()
val faceCount = withContext(Dispatchers.IO) { faceDao.faceCount }
if (faceCount == 0) {
newUserCollect()
} else {
resetRecognizeState()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@@ -714,12 +726,13 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
isCollectFace = false isCollectFace = false
plateEnable = true plateEnable = true
collectCount = 0 collectCount = 0
tempUserId = "${System.currentTimeMillis()}" // tempUserId = "${System.currentTimeMillis()}"
resumeCamera() resumeCamera()
registerHandler = RegisterCallbackHandler() registerHandler = RegisterCallbackHandler()
CountDownManager.startCountDown( recognizeCountDownUtil.startCountDown(
totalSeconds = 15, total = 15,
lifecycleScope = lifecycleScope,
onTick = { remaining -> onTick = { remaining ->
loadLogInfo("resetRecognizeState剩余时间:$remaining") loadLogInfo("resetRecognizeState剩余时间:$remaining")
if (judgeRecognizeState()) return@startCountDown if (judgeRecognizeState()) return@startCountDown
@@ -741,10 +754,10 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
private var isFirstOpen = true private var isFirstOpen = true
protected override fun onPause() { override fun onPause() {
pauseCamera() pauseCamera()
super.onPause() super.onPause()
CountDownManager.cancelCountDown() recognizeCountDownUtil.cancelCountDown()
} }
private fun pauseCamera() { private fun pauseCamera() {
@@ -752,14 +765,11 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
// recognizeViewModel.onPreviewFrame(emptyFrame, true) // recognizeViewModel.onPreviewFrame(emptyFrame, true)
} }
override fun handleLoginSuccess(equipmentUserInfo: EquipmentUserInfo, isAdmin: Boolean) {
super.handleLoginSuccess(equipmentUserInfo, isAdmin)
// goInitActivity()
}
private fun judgeRecognizeState(): Boolean { private fun judgeRecognizeState(): Boolean {
val rectColor = binding.dualCameraFaceRectView.rectColor val rectColor = binding.dualCameraFaceRectView.rectColor
if (rectColor == RecognizeColor.COLOR_SUCCESS && currentUserId.isNullOrBlank().not() && facePreviewInfoList.isNullOrEmpty().not()) { if (rectColor == RecognizeColor.COLOR_SUCCESS && currentUserId.isNullOrBlank()
.not() && facePreviewInfoList.isNullOrEmpty().not()
) {
//人未离开,再次识别 //人未离开,再次识别
loadLogInfo("userId=$currentUserId") loadLogInfo("userId=$currentUserId")
openPlateDebouncer.debounce { openPlateDebouncer.debounce {
@@ -823,14 +833,15 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
loadLogInfo("collectFace,sendFaceData: ----------12,发送人脸数据成功但接口返回为null,faceData=${faceData}") loadLogInfo("collectFace,sendFaceData: ----------12,发送人脸数据成功但接口返回为null,faceData=${faceData}")
return@addUserFace return@addUserFace
} }
Thread { lifecycleScope.launch(Dispatchers.IO) {
saveFaceInfo(faceModel) { saveFaceInfo(faceModel) {
runOnUiThread { runOnUiThread {
Glide.with(this@LoginByFaceActivity).load(imageFile).into(binding.ivCollectPhoto) Glide.with(this@LoginByFaceActivity).load(imageFile)
.into(binding.ivCollectPhoto)
} }
} }
uploadFaceDebouncer.reset() uploadFaceDebouncer.reset()
}.start() }
} }
} }
@@ -866,7 +877,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
private var currentUserId: String? = null private var currentUserId: String? = null
private fun saveFaceInfo(faceModel: UserFaceModel, block:()-> Unit= {}) { private fun saveFaceInfo(faceModel: UserFaceModel, block: () -> Unit = {}) {
try { try {
loadLogInfo("collectFace,saveFaceInfo: ----------8,保存人脸取特征数据-----------") loadLogInfo("collectFace,saveFaceInfo: ----------8,保存人脸取特征数据-----------")
val faceDao = FaceDatabase.getInstance(this).faceDao() val faceDao = FaceDatabase.getInstance(this).faceDao()
@@ -876,13 +887,18 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
Base64.decode(faceModel.faceFeatureStr) Base64.decode(faceModel.faceFeatureStr)
).also { ).also {
//1-会员、2-临时用户 //1-会员、2-临时用户
it.userType = if (faceModel.isMember) "1" else "2" it.userType = if (faceModel.member) "1" else "2"
} }
faceDao.insert(faceEntity) val faceId = faceDao.insert(faceEntity)
faceEntity.faceId = faceId
loadLogInfo("collectFace,saveFaceInfo: ----------10,人脸数据保存完成-----------") loadLogInfo("collectFace,saveFaceInfo: ----------10,人脸数据保存完成-----------")
recognizeViewModel.refreshFaceList() recognizeViewModel.refreshFaceList()
loadLogInfo("collectFace,saveFaceInfo: ----------11,刷新人脸数据-----------") loadLogInfo("collectFace,saveFaceInfo: ----------11,刷新人脸数据-----------")
runOnUiThread { recognizeViewModel.onPreviewFrame(emptyFrame, true) } runOnUiThread {
//采集完成,不再识别,直接提示成功
isRecognition = false
recognizeSuccess("${System.currentTimeMillis()}")
}
loadLogInfo("---------------------------------") loadLogInfo("---------------------------------")
collectCount = 0 collectCount = 0
block() block()
@@ -899,22 +915,20 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
private var isCollectFace = false private var isCollectFace = false
private fun leftCountDown() { private fun leftCountDown() {
CountDownUtil.startCountDown(total = 15, lifecycleScope = lifecycleScope, onStart = { leftCountDownUtil.startCountDown(total = 15, lifecycleScope = lifecycleScope, onStart = {
loadLogInfo("onPreview-----------leftCountDown-------------") loadLogInfo("onPreview-----------leftCountDown-------------")
}, onTick = { }, onTick = {
loadLogInfo("leftCountDown.onTick:$it") loadLogInfo("leftCountDown.onTick:$it")
if (facePreviewInfoList.isNullOrEmpty().not()) { if (facePreviewInfoList.isNullOrEmpty().not()) {
loadLogInfo("leftCountDown.onTick-cancel()") loadLogInfo("leftCountDown.onTick-cancel()")
CountDownUtil.cancelCountDown() leftCountDownUtil.cancelCountDown()
return@startCountDown return@startCountDown
} }
}, onFinish = { }, onFinish = {
if (facePreviewInfoList.isNullOrEmpty().not()) { if (facePreviewInfoList.isNullOrEmpty().not()) {
loadLogInfo("leftCountDown.onFinish-cancel()") loadLogInfo("leftCountDown.onFinish-人脸未离开,不跳转")
CountDownUtil.cancelCountDown()
return@startCountDown return@startCountDown
} }
CountDownUtil.cancelCountDown()
goInitActivity() goInitActivity()
}) })
} }
@@ -2,7 +2,6 @@ package com.sw.platecabinet.model.response
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
/** /**
@@ -38,5 +37,5 @@ data class UserFaceModel(
/** /**
* 是否会员 * 是否会员
*/ */
val isMember: Boolean val member: Boolean
) : Parcelable ) : Parcelable
@@ -12,37 +12,29 @@ import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
object ApiClient { object ApiClient {
//private const val BASE_URL = "https://vip.shuziweidao.com"
// private const val DEVICE_BASE_URL = "http://device.shuziweidao.com:8889/"
private var DEVICE_BASE_URL = GlobalData.appBaseUrl
// private const val BASE_URL = "http://192.168.1.8:9092"
// private const val TIME_OUT = 30L // 超时时间(秒)
private const val TIME_OUT = 60L // 超时时间(秒)
val okHttpClient = OkHttpClient.Builder() private const val TIME_OUT = 60L // 超时时间(秒)
private val okHttpClient = OkHttpClient.Builder()
.connectTimeout(TIME_OUT, TimeUnit.SECONDS) .connectTimeout(TIME_OUT, TimeUnit.SECONDS)
.readTimeout(TIME_OUT, TimeUnit.SECONDS) .readTimeout(TIME_OUT, TimeUnit.SECONDS)
.writeTimeout(TIME_OUT, TimeUnit.SECONDS) .writeTimeout(TIME_OUT, TimeUnit.SECONDS)
.addNetworkInterceptor(HttpLoggingInterceptor(logger = { .addNetworkInterceptor(HttpLoggingInterceptor { Timber.d("okhttp ==>${it}") }.apply {
Timber.d("okhttp logger ==>${it}") level = if (MyApp.DEBUG) HttpLoggingInterceptor.Level.BODY
}).apply { else HttpLoggingInterceptor.Level.NONE
level = if (MyApp.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
}) })
.addInterceptor(RequestInterceptor()) .addInterceptor(RequestInterceptor())
.build() .build()
private val retrofit = Retrofit.Builder() private val retrofit by lazy {
.baseUrl(DEVICE_BASE_URL) Retrofit.Builder()
.client(okHttpClient) .baseUrl(GlobalData.appBaseUrl)
.addConverterFactory(GsonConverterFactory.create()) .client(okHttpClient)
// .addCallAdapterFactory(CoroutineCallAdapterFactory()) // 协程适配器 .addConverterFactory(GsonConverterFactory.create())
.build() .build()
}
val apiService: ApiService by lazy { val apiService: ApiService by lazy {
retrofit.create(ApiService::class.java) retrofit.create(ApiService::class.java)
} }
} }
@@ -1,135 +0,0 @@
package com.sw.platecabinet.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
}
}
@@ -1,33 +1,26 @@
package com.sw.platecabinet.utils package com.sw.platecabinet.utils
import android.util.Log
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import android.util.Log
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlin.ranges.downTo
/** /**
* 人脸识别防抖倒计时工具类(单例+防重复+无死循环 * 基于协程的倒计时工具类(实例化使用,各调用方持有独立实例,互不干扰
* 修复:onCompletion 区分正常结束与取消,手动取消时不触发 onFinish
*/ */
object CountDownUtil { class CountDownUtil {
// 原子布尔值:标记是否正在倒计时(防重复启动)
private val isCounting = AtomicBoolean(false)
// 当前倒计时Job(用于取消) private val isCounting = AtomicBoolean(false)
private var currentJob: Job? = null private var currentJob: Job? = null
/** /**
* 启动/重启防抖倒计时(同一时间仅一个倒计时运行 * 启动倒计时(若已在运行则忽略,需重启请先调用 cancelCountDown
* @param totalSeconds 倒计时总秒数(如15 * @param total 总秒数
* @param lifecycleScope Activity/FragmentlifecycleScope(必须) * @param lifecycleScope Activity/FragmentlifecycleScope
* @param onStart 启动回调(主线程)
* @param onTick 每秒回调剩余秒数(主线程) * @param onTick 每秒回调剩余秒数(主线程)
* @param onFinish 倒计时结束回调(主线程) * @param onFinish 正常结束回调(主线程,手动取消时不触发
*/ */
fun startCountDown( fun startCountDown(
total: Int = 15, total: Int = 15,
@@ -36,46 +29,37 @@ object CountDownUtil {
onTick: (Int) -> Unit = {}, onTick: (Int) -> Unit = {},
onFinish: () -> Unit onFinish: () -> Unit
) { ) {
// 1. 防重复:如果正在倒计时,先取消旧任务 if (isCounting.get()) return
if (isCounting.get()) {
//cancelCountDown()
return
}
// 2. 标记为正在倒计时
isCounting.set(true) isCounting.set(true)
// 3. 启动新倒计时
currentJob = flow { currentJob = flow {
Log.d("CountDownUtil", "倒计时启动,总时长:$total")
for (i in total downTo 1) { for (i in total downTo 1) {
emit(i) emit(i)
delay(1000) // IO线程等待1秒,无死循环 delay(1000L)
} }
} }
.flowOn(Dispatchers.IO) // 上游IO线程,避免阻塞主线程 .flowOn(Dispatchers.IO)
.onStart { .onStart {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) { onStart() }
onStart.invoke()
}
} }
.onEach { remaining -> .onEach { remaining ->
// 主线程回调剩余时间 withContext(Dispatchers.Main) { onTick(remaining) }
withContext(Dispatchers.Main) {
onTick.invoke(remaining)
}
} }
.onCompletion { .onCompletion { cause ->
// 倒计时结束:重置状态+回调
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
isCounting.set(false) // 重置标记 isCounting.set(false)
currentJob = null // 置空Job currentJob = null
Log.d("CountDownUtil", "倒计时正常结束") // cause == null 为正常结束,有异常(含 CancellationException)时不触发 onFinish
onFinish.invoke() if (cause == null) {
Log.d("CountDownUtil", "倒计时正常结束")
onFinish()
} else {
Log.d("CountDownUtil", "倒计时被取消:${cause.message}")
}
} }
} }
.catch { e -> .catch { e ->
// 捕获异常:避免崩溃,重置状态
Log.e("CountDownUtil", "倒计时异常:${e.message}", e) Log.e("CountDownUtil", "倒计时异常:${e.message}", e)
isCounting.set(false) isCounting.set(false)
currentJob = null currentJob = null
@@ -84,12 +68,12 @@ object CountDownUtil {
} }
/** /**
* 取消倒计时(手动停止 * 取消倒计时(不触发 onFinish
*/ */
fun cancelCountDown() { fun cancelCountDown() {
currentJob?.cancel() // 取消Job currentJob?.cancel()
currentJob = null // 置空,避免残留 currentJob = null
isCounting.set(false) // 重置标记 isCounting.set(false)
Log.d("CountDownUtil", "倒计时被手动取消") Log.d("CountDownUtil", "倒计时取消")
} }
} }
@@ -6,10 +6,14 @@ import com.sw.inbound.utils.SPUtil
import com.sw.inbound.utils.SPUtil.Companion.getInstance import com.sw.inbound.utils.SPUtil.Companion.getInstance
import com.sw.platecabinet.MyApp import com.sw.platecabinet.MyApp
import com.sw.platecabinet.model.response.EquipmentUserInfo import com.sw.platecabinet.model.response.EquipmentUserInfo
import kotlin.text.get
object SpTool { object SpTool {
const val LAST_FACE_TIMESTAMP: String = "faceTimestamp" const val LAST_FACE_TIMESTAMP: String = "faceTimestamp"
const val PLATE_BOX_DATA: String = "plateBoxBind" const val PLATE_BOX_DATA: String = "plateBoxBind"
const val IS_FIRST_GET_FACE = "isFirstGetFace"
private var spUtil: SPUtil? = null private var spUtil: SPUtil? = null
init { init {
@@ -39,4 +43,10 @@ object SpTool {
return GsonUtils.fromJsonList(jsonData, EquipmentUserInfo::class.java) return GsonUtils.fromJsonList(jsonData, EquipmentUserInfo::class.java)
} }
var firstGetFace: Boolean
get() = spUtil?.get(IS_FIRST_GET_FACE, true)?:true
set(value) {
spUtil?.boolean(IS_FIRST_GET_FACE, value)
}
} }
@@ -9,16 +9,12 @@ import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.FaceApi import com.sw.plate.utils.arcface.FaceApi
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import com.sw.platecabinet.GlobalData import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.GlobalData.globalEquipmentCode
import com.sw.platecabinet.GlobalKey import com.sw.platecabinet.GlobalKey
import com.sw.platecabinet.model.DeviceConfig import com.sw.platecabinet.model.DeviceConfig
import com.sw.platecabinet.model.request.LoginParam import com.sw.platecabinet.model.request.LoginParam
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.UserFaceModel import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.utils.SpTool import com.sw.platecabinet.utils.SpTool
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
@@ -55,6 +51,19 @@ class UserViewModel : BaseViewModel() {
// } // }
// } // }
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>()
}
}
/** /**
* 获取人脸数据 * 获取人脸数据
*/ */
@@ -79,9 +88,15 @@ class UserViewModel : BaseViewModel() {
return@launchWithLoading return@launchWithLoading
} }
val faceEntityList = list.map { val faceEntityList = list.map {
FaceEntity(it.userId, null, Base64.decode(it.faceFeatureStr)) FaceEntity(it.userId, null, Base64.decode(it.faceFeatureStr)).apply {
userType = if (it.member) "1" else "2"
}
} }
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
if (currentPageNo == 1) {
//第一页数据删除本地数据库
faceApi.clearFaceData()
}
faceApi.updateFaceData(pageNo, faceEntityList) faceApi.updateFaceData(pageNo, faceEntityList)
if (list.size >= pageSize) { if (list.size >= pageSize) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0 faceTimestamp = list.last().faceUpdateTimestamp ?: 0
@@ -103,6 +118,7 @@ class UserViewModel : BaseViewModel() {
private var faceTimestamp = 0L private var faceTimestamp = 0L
/** /**
* 激活人脸识别引擎 * 激活人脸识别引擎
*/ */
+49 -2
View File
@@ -3,12 +3,45 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@drawable/bg" android:background="@drawable/bg"
android:orientation="vertical"> android:orientation="vertical"
xmlns:tools="http://schemas.android.com/tools">
<include <include
android:id="@+id/includeHeader" android:id="@+id/includeHeader"
layout="@layout/item_title_time" /> layout="@layout/item_title_time" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:gravity="center_vertical"
android:layout_marginTop="30dp">
<TextView
android:id="@+id/tvMemberUserCount"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:textSize="16sp"
android:textColor="@color/white"
android:gravity="center"
tools:text="会员用户:100人"/>
<View
android:layout_width="1dp"
android:layout_height="30dp"
android:background="@color/white"/>
<TextView
android:id="@+id/tvTempUserCount"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:textSize="16sp"
android:textColor="@color/white"
android:gravity="center"
tools:text="临时用户:100人"/>
</LinearLayout>
<TextView <TextView
android:id="@+id/btnClearFace" android:id="@+id/btnClearFace"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -32,7 +65,7 @@
android:layout_marginHorizontal="50dp" android:layout_marginHorizontal="50dp"
android:paddingHorizontal="30dp" android:paddingHorizontal="30dp"
android:background="@drawable/btn_outline" android:background="@drawable/btn_outline"
android:text="清除并重新拉取数据" android:text="清除并拉取全量数据"
android:textColor="#FFCC99" /> android:textColor="#FFCC99" />
<TextView <TextView
@@ -60,4 +93,18 @@
android:background="@drawable/btn_outline" android:background="@drawable/btn_outline"
android:text="模拟放置餐盘" android:text="模拟放置餐盘"
android:textColor="#FFCC99" /> android:textColor="#FFCC99" />
<TextView
android:id="@+id/btnTestFeature"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="30dp"
android:textSize="24sp"
android:gravity="center"
android:layout_marginHorizontal="50dp"
android:paddingHorizontal="30dp"
android:background="@drawable/btn_outline"
android:text="人脸特征比较"
android:textColor="#FFCC99"
android:visibility="gone"/>
</LinearLayout> </LinearLayout>
@@ -40,6 +40,12 @@ public class FaceApi {
faceDao.insert(list); faceDao.insert(list);
} }
public void clearFaceData() {
FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao();
faceDao.deleteAll();
faceDao.resetId();
}
/** /**
* 激活arcsoft 人脸 * 激活arcsoft 人脸
* *
@@ -92,9 +92,25 @@ public interface FaceDao {
@Query("SELECT * FROM face WHERE user_name = :userName limit 1") @Query("SELECT * FROM face WHERE user_name = :userName limit 1")
FaceEntity queryByUserName(String userName); FaceEntity queryByUserName(String userName);
/**
* 查询指定用户的所有人脸记录(同一用户可能存有多条特征数据)
*
* @param userName 用户ID
* @return 该用户所有人脸记录列表
*/
@Query("SELECT * FROM face WHERE user_name = :userName")
List<FaceEntity> queryAllByUserName(String userName);
/** /**
* @return 删除临时用户人脸 * @return 删除临时用户人脸
*/ */
@Query("DELETE from face WHERE user_type = '2'") @Query("DELETE from face WHERE user_type = '2'")
int deleteTempUserFaceData(); int deleteTempUserFaceData();
/**
* 查询用户人脸数
* @param userType 1-会员,2-临时用户
*/
@Query("SELECT COUNT(1) FROM face WHERE user_type = :userType")
int getFaceCount(String userType);
} }