- FaceDao新增queryByUserFaceId接口,实现按userFaceId精确查询 - FaceApi新增queryByUserFaceId方法,封装Dao查询逻辑 - LoginByFaceActivity插入人脸时,先按userFaceId查询,存在则更新避免重复 - NetViewModelV2同步人脸数据时,优先用userFaceId判重,减少重复入库 - 修正FaceEntity中userId字段数据库列名错误(user_id替代user_idd) - 增加注释说明及代码格式优化,提升代码可读性和健壮性
1075 lines
43 KiB
Kotlin
1075 lines
43 KiB
Kotlin
@file:Suppress("DEPRECATION")
|
||
|
||
package com.sw.platecabinet.activity
|
||
|
||
import android.Manifest
|
||
import android.content.Intent
|
||
import android.graphics.Bitmap
|
||
import android.graphics.Point
|
||
import android.hardware.Camera
|
||
import android.net.Uri
|
||
import android.os.Build
|
||
import android.provider.Settings
|
||
import android.view.View
|
||
import android.view.ViewGroup
|
||
import android.view.ViewTreeObserver
|
||
import android.widget.FrameLayout
|
||
import androidx.annotation.RequiresApi
|
||
import androidx.core.content.ContextCompat
|
||
import androidx.core.view.isVisible
|
||
import androidx.lifecycle.lifecycleScope
|
||
import com.arcsoft.face.ErrorInfo
|
||
import com.bumptech.glide.Glide
|
||
import com.sw.inbound.utils.DateTimeUtils
|
||
import com.sw.plate.utils.Base64
|
||
import com.sw.plate.utils.ToastUtils
|
||
import com.sw.plate.utils.arcface.ConfigUtil
|
||
import com.sw.plate.utils.arcface.ErrorCodeUtil
|
||
import com.sw.plate.utils.arcface.FaceRectTransformer
|
||
import com.sw.plate.utils.arcface.FaceRectView
|
||
import com.sw.plate.utils.arcface.FaceRectView.DrawInfo
|
||
import com.sw.plate.utils.arcface.PreviewConfig
|
||
import com.sw.plate.utils.arcface.camera.CameraListener
|
||
import com.sw.plate.utils.arcface.camera.DualCameraHelper
|
||
import com.sw.plate.utils.arcface.face.constants.LivenessType
|
||
import com.sw.plate.utils.arcface.face.model.CompareResult
|
||
import com.sw.plate.utils.arcface.face.model.FacePreviewInfo
|
||
import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration
|
||
import com.sw.plate.utils.arcface.facedb.FaceDatabase
|
||
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
|
||
import com.sw.plate.utils.arcface.model.UserFaceInfo
|
||
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
|
||
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel.REGISTER_STATUS_READY
|
||
import com.sw.platecabinet.R
|
||
import com.sw.platecabinet.databinding.ActivityLoginFaceBinding
|
||
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
|
||
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.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.RegisterCallbackHandler
|
||
import com.sw.platecabinet.utils.SoundPoolUtil
|
||
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
|
||
import org.greenrobot.eventbus.EventBus
|
||
import org.greenrobot.eventbus.Subscribe
|
||
import org.greenrobot.eventbus.ThreadMode
|
||
import timber.log.Timber
|
||
import java.io.File
|
||
import java.util.concurrent.atomic.AtomicBoolean
|
||
|
||
/**
|
||
* 人脸识别
|
||
*/
|
||
class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
|
||
ViewTreeObserver.OnGlobalLayoutListener {
|
||
|
||
override fun inflateViewBinding(): ActivityLoginFaceBinding {
|
||
return ActivityLoginFaceBinding.inflate(layoutInflater)
|
||
}
|
||
|
||
override fun inflateTitleBinding(): ItemTitleTimeBinding {
|
||
return binding.includeHeader
|
||
}
|
||
|
||
companion object {
|
||
private const val TAG = "LoginByFaceActivity"
|
||
|
||
private const val COLLECT_SUCCESS = "collect_success"
|
||
private const val RECOGNIZE_SUCCESS = "recognize_success"
|
||
private const val NO_PLATE_REMIND = "no_plate_remind"
|
||
}
|
||
|
||
override fun initialize() {
|
||
netViewModelV2.activeEngine()
|
||
|
||
PlcHelper.getInstance(this@LoginByFaceActivity)
|
||
.openSerialPort(object : PlcCallback<String> {
|
||
override fun onSuccess(result: String) {
|
||
ToastUtils.showToast("打开串口 → 成功: ${result}")
|
||
}
|
||
|
||
override fun onError(message: String) {
|
||
ToastUtils.showToast("打开串口 → 失败: $message")
|
||
}
|
||
})
|
||
|
||
initArcViewModel()
|
||
initArcView()
|
||
openRectInfoDraw = true
|
||
recognizeViewModel.setDrawRectInfoTextValue(true)
|
||
|
||
EventBus.getDefault().register(this)
|
||
|
||
//开启人脸增量数据定时任务
|
||
startFaceTask()
|
||
|
||
val soundMap = hashMapOf<String, Int>()
|
||
soundMap[COLLECT_SUCCESS] = R.raw.collect_success
|
||
soundMap[RECOGNIZE_SUCCESS] = R.raw.recognize_success
|
||
soundMap[NO_PLATE_REMIND] = R.raw.no_plate_remind
|
||
SoundPoolUtil.getInstance().loadR(this, soundMap)
|
||
|
||
binding.btnCollect.clickWithDebounce {
|
||
//先重置,后采集
|
||
resetRecognizeState()
|
||
//采集数据按钮
|
||
retryFailCount = 0
|
||
isCollectFace = true
|
||
isCollecting.set(true)
|
||
collectFace()
|
||
}
|
||
|
||
binding.btnRetry.clickWithDebounce {
|
||
//重新识别按钮
|
||
resetRecognizeState()
|
||
}
|
||
}
|
||
// 检查相机权限
|
||
|
||
private fun checkCameraPermission() {
|
||
Timber.i("checkCameraPermission")
|
||
val hasCameraPermission = ContextCompat.checkSelfPermission(
|
||
this,
|
||
Manifest.permission.CAMERA
|
||
) == android.content.pm.PackageManager.PERMISSION_GRANTED
|
||
|
||
if (hasCameraPermission) {
|
||
// 权限已授予
|
||
loadLogInfo("checkCameraPermission 权限已允许")
|
||
recognizeViewModel.init()
|
||
initRgbCamera()
|
||
resumeCamera()
|
||
} else {
|
||
// 无权限,请求相机权限
|
||
requestCameraPermission()
|
||
}
|
||
}
|
||
|
||
private fun requestCameraPermission() {
|
||
requestPermission(Manifest.permission.CAMERA) { isGranted ->
|
||
if (isGranted) {
|
||
// 权限已授予
|
||
loadLogInfo("checkCameraPermission 权限已允许")
|
||
recognizeViewModel.init()
|
||
initRgbCamera()
|
||
resumeCamera()
|
||
} else {
|
||
// 权限被拒绝
|
||
loadLogInfo("checkCameraPermission 权限被拒绝")
|
||
CustomDialog(
|
||
activity = this,
|
||
content = "请在设置中开启相机权限",
|
||
onConfirm = {
|
||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||
data = Uri.fromParts("package", context.packageName, null)
|
||
}
|
||
startActivity(intent) {
|
||
checkCameraPermission()
|
||
}
|
||
}, onCancel = {
|
||
|
||
}).show()
|
||
}
|
||
}
|
||
}
|
||
|
||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||
fun getFaceEntity(insertEntity: FaceEntity) {
|
||
Timber.tag("performSync").e("-time=%s", insertEntity.registerTime)
|
||
recognizeViewModel.addFace(insertEntity)
|
||
Timber.tag("performSync over").e("-time=%s", insertEntity.registerTime)
|
||
}
|
||
|
||
override fun onLeftDoubleClick() {
|
||
finish()
|
||
}
|
||
|
||
override fun onRightDoubleClick() {
|
||
PlcSettingActivity.start(this)
|
||
}
|
||
|
||
private var isRecognition = false
|
||
private var rgbCameraHelper: DualCameraHelper? = null
|
||
private var rgbFaceRectTransformer: FaceRectTransformer? = null
|
||
private val livenessType = LivenessType.RGB
|
||
private var openRectInfoDraw = false
|
||
|
||
private val leftDebouncer = Debouncer(30 * 1000)
|
||
|
||
/** 识别超时倒计时(15秒无操作跳回初始页) */
|
||
private val recognizeCountDownUtil = CountDownUtil()
|
||
|
||
/** 用户离开画面后的倒计时(llCollectButton 显示状态下无人脸时触发) */
|
||
private val leftCountDownUtil = CountDownUtil()
|
||
|
||
private var registerHandler: RegisterCallbackHandler? = null
|
||
|
||
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:拿到人脸提取特征数据照片过小-----------")
|
||
// return false
|
||
// }
|
||
return true
|
||
}
|
||
|
||
private fun initArcViewModel() {
|
||
recognizeViewModel.setLiveType(livenessType)
|
||
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,跳过")
|
||
return@setCallback
|
||
}
|
||
if (headBmp.width <= 200 || headBmp.height <= 200) {
|
||
loadLogInfo("collectFace,isFitData:拿到人脸提取特征数据照片过小w:${headBmp.width},h:${headBmp.height}-----------")
|
||
binding.llCollectButton.visible()
|
||
binding.btnRetry.gone()
|
||
binding.btnCollect.text = "重新采集"
|
||
return@setCallback
|
||
}
|
||
recognizeViewModel.updateRegisterStatus(RecognizeViewModel.REGISTER_STATUS_DONE)
|
||
this.userFaceInfo = userFaceInfo
|
||
getFaceDataBlock()
|
||
}
|
||
}
|
||
}
|
||
recognizeViewModel.ftInitCode.observe(this) { ftInitCode: Int? ->
|
||
if (ftInitCode != null && ftInitCode != ErrorInfo.MOK) {
|
||
val error: String? = getString(
|
||
R.string.specific_engine_init_failed, "ftEngine",
|
||
ftInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(ftInitCode)
|
||
)
|
||
Timber.e("ftInitCode observe = $error")
|
||
ToastUtils.showToast(error)
|
||
}
|
||
}
|
||
recognizeViewModel.frInitCode.observe(this) { frInitCode: Int? ->
|
||
if (frInitCode != null && frInitCode != ErrorInfo.MOK) {
|
||
val error: String? = getString(
|
||
R.string.specific_engine_init_failed, "frEngine",
|
||
frInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(frInitCode)
|
||
)
|
||
Timber.e("frInitCode observe = $error")
|
||
ToastUtils.showToast(error)
|
||
}
|
||
}
|
||
recognizeViewModel.flInitCode.observe(this) { flInitCode: Int? ->
|
||
if (flInitCode != null && flInitCode != ErrorInfo.MOK) {
|
||
val error: String? = getString(
|
||
R.string.specific_engine_init_failed, "flEngine",
|
||
flInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(flInitCode)
|
||
)
|
||
Timber.e("flInitCode observe = $error")
|
||
ToastUtils.showToast(error)
|
||
}
|
||
}
|
||
|
||
recognizeViewModel.recognizeConfiguration
|
||
.observe(this) { recognizeConfiguration: RecognizeConfiguration? ->
|
||
Timber.i("recognizeConfiguration: observe = ${recognizeConfiguration.toString()}")
|
||
}
|
||
recognizeViewModel.recognizeNotice.observe(this) { notice: String? ->
|
||
Timber.i("recognizeNotice observe notice = $notice")
|
||
}
|
||
|
||
recognizeViewModel.recognizeUserId.observe(this) { result: CompareResult? ->
|
||
loadLogInfo("collectFace,recognizeUserId observe userId=${result?.faceEntity?.userName},similar=${result?.similar},userType=${result?.faceEntity?.userType}")
|
||
loadFaceRecognizeResult3(result)
|
||
}
|
||
|
||
recognizeViewModel.drawRectInfoText.observe(this) { info ->
|
||
Timber.i("drawRectInfoText observe info = $info")
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 识别失败重试次数
|
||
*/
|
||
private var retryFailCount = 0
|
||
|
||
/**
|
||
* 识别失败最大次数
|
||
*/
|
||
private val maxFailCount = 5
|
||
|
||
/**
|
||
* 配置阈值,默认值为0.8
|
||
*/
|
||
private val faceSuccessThreshold by lazy { ConfigUtil.getRecognizeThreshold(this) }
|
||
|
||
private val openPlateDebouncer = Debouncer(3000)
|
||
private val isCollecting = AtomicBoolean(false)
|
||
|
||
private fun loadFaceRecognizeResult3(result: CompareResult?) {
|
||
val similar = result?.similar ?: return
|
||
|
||
binding.tvSimilarValue.text = "$similar,${retryFailCount}次"
|
||
loadLogInfo(
|
||
"collectFace,recognizeUserId 阈值 success=$faceSuccessThreshold, similar=$similar, retry=$retryFailCount"
|
||
)
|
||
if (similar >= faceSuccessThreshold) {
|
||
val userId = result.faceEntity?.userName
|
||
recognizeSuccess(userId)
|
||
return
|
||
}
|
||
|
||
if (isCollectFace || isCollecting.get()) {
|
||
loadLogInfo("collectFace,recognizeUserId 正在采集中,忽略失败结果 similar=$similar")
|
||
return
|
||
}
|
||
|
||
if (retryFailCount >= maxFailCount) {
|
||
loadLogInfo(
|
||
"collectFace,recognizeUserId 识别失败,开始采集 similar=$similar, " +
|
||
"retry=$retryFailCount, time=${DateTimeUtils.getDateTimeString()}"
|
||
)
|
||
if (!isCollecting.compareAndSet(false, true)) {
|
||
loadLogInfo("collectFace 已在采集中,忽略重复触发")
|
||
return
|
||
}
|
||
|
||
recognizeFail()
|
||
return
|
||
}
|
||
|
||
retryFailCount++
|
||
loadLogInfo("collectFace,recognizeUserId 中间态重试,retryFailCount=$retryFailCount")
|
||
}
|
||
|
||
private fun recognizeSuccess(userId: String?) {
|
||
if (userId.isNullOrBlank()) {
|
||
loadLogInfo("collectFace,recognizeUserId 识别成功但 userId 为空,忽略")
|
||
return
|
||
}
|
||
loadLogInfo("collectFace,recognizeUserId 识别成功 userId=$userId, time=${DateTimeUtils.getDateTimeString()}")
|
||
// 成功后必须重置状态
|
||
retryFailCount = 0
|
||
isCollecting.set(false)
|
||
isCollectFace = false
|
||
currentUserId = userId
|
||
|
||
// 防抖,防止重复开闸
|
||
openPlateDebouncer.debounce("openPlateDebouncer") {
|
||
openPlate(userId)
|
||
}
|
||
}
|
||
|
||
private fun recognizeFail() {
|
||
recognizeCountDownUtil.cancelCountDown()
|
||
binding.tvToInit.text = ""
|
||
binding.tvSimilarValue.text = ""
|
||
binding.llCollectButton.visible()
|
||
binding.btnRetry.visible()
|
||
binding.btnCollect.run {
|
||
visible()
|
||
text = "新用户采集"
|
||
}
|
||
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()
|
||
openPlateDebouncer.reset()
|
||
plateEnable = true
|
||
resumeCamera()
|
||
}
|
||
|
||
private var plateEnable = false
|
||
|
||
/**
|
||
* 打开餐盘柜:执行出盘,出盘后查询剩余餐盘状态并播放对应提示音
|
||
*/
|
||
private fun openPlate(userId: String?) {
|
||
loadLogInfo("collectFace,openPlate: userId=$userId,plateEnable=$plateEnable")
|
||
if (plateEnable.not()) return
|
||
|
||
PlcHelper.getInstance(this@LoginByFaceActivity)
|
||
.outPlate(object : PlcCallback<String> {
|
||
override fun onSuccess(result: String) {
|
||
loadLogInfo("collectFace,openPlate: 出盘完成,result=$result,开始查询剩余餐盘")
|
||
onOutPlateSuccess(result, RECOGNIZE_SUCCESS)
|
||
}
|
||
|
||
override fun onError(message: String) {
|
||
onOutPlateError(message)
|
||
}
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 新用户注册采集成功后立即出盘(与上传后台并行,互不阻塞)。
|
||
* 故意不经过 recognizeSuccess:避免使用临时 userId、避免与识别成功的防抖语义混淆。
|
||
*/
|
||
private fun outPlateForRegister() {
|
||
loadLogInfo("collectFace,outPlateForRegister: 新用户采集成功,立即出盘,plateEnable=$plateEnable")
|
||
if (plateEnable.not()) return
|
||
|
||
PlcHelper.getInstance(this@LoginByFaceActivity)
|
||
.outPlate(object : PlcCallback<String> {
|
||
override fun onSuccess(result: String) {
|
||
loadLogInfo("collectFace,outPlateForRegister: 出盘完成,result=$result")
|
||
onOutPlateSuccess(result, COLLECT_SUCCESS)
|
||
}
|
||
|
||
override fun onError(message: String) {
|
||
onOutPlateError(message)
|
||
}
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 出盘成功后的统一处理:
|
||
* 1. 置 plateEnable=false、取消识别倒计时,防止重复出盘;
|
||
* 2. result 为 "false" 表示出盘后已无餐盘,播放无盘提示音并跳转;
|
||
* 3. 否则播放提示音并查询剩余餐盘状态。
|
||
*
|
||
* @param result PLC 返回结果("false" 表示出盘后无餐盘)
|
||
* @param successSound 出盘成功播放的提示音 key(识别成功 / 采集成功)
|
||
*/
|
||
private fun onOutPlateSuccess(result: String, successSound: String) {
|
||
plateEnable = false
|
||
recognizeCountDownUtil.cancelCountDown()
|
||
if (result.equals("false", ignoreCase = true)) {
|
||
loadLogInfo("collectFace,onOutPlateSuccess: result=false,餐盘已空")
|
||
SoundPoolUtil.getInstance().play(NO_PLATE_REMIND, 0)
|
||
goInitActivity()
|
||
return
|
||
}
|
||
SoundPoolUtil.getInstance().play(successSound, 0)
|
||
// 已注册用户:出盘成功且有盘时,上报取餐盘时刻(P-09a)
|
||
if (successSound == RECOGNIZE_SUCCESS) {
|
||
reportPlatePickup(currentUserId, "recognize")
|
||
}
|
||
checkRemainingPlate()
|
||
}
|
||
|
||
/**
|
||
* 出盘失败后的统一处理:提示失败并跳转回初始页
|
||
*/
|
||
private fun onOutPlateError(message: String) {
|
||
ToastUtils.showToast("出盘失败,message=$message")
|
||
loadLogInfo("collectFace,onOutPlateError: 出盘失败,message=$message")
|
||
goInitActivity()
|
||
}
|
||
|
||
/**
|
||
* 上报取餐盘时刻(P-09a):记录用户开始就餐时刻,是后续就餐记录(P-09)的强前置。
|
||
*
|
||
* 调用时机:
|
||
* - 已注册用户:识别成功且出盘成功(有盘)后,userId 取自 [currentUserId](来源 "recognize");
|
||
* - 新用户:addUserFace 注册成功拿到真实 userId 后(出盘已由 outPlateForRegister 并行触发,来源 "collect")。
|
||
*
|
||
* 上报失败仅记录日志,不影响吐盘主流程。
|
||
*
|
||
* @param userId 用户 id(餐盘号 = userId),为空或非数字时跳过
|
||
* @param source 调用来源标识,仅用于日志区分
|
||
*/
|
||
private fun reportPlatePickup(userId: String?, source: String) {
|
||
val id = userId?.toLongOrNull()
|
||
if (id == null) {
|
||
loadLogInfo("collectFace,reportPlatePickup[$source]: userId 为空或非数字,跳过上报,userId=$userId")
|
||
return
|
||
}
|
||
loadLogInfo("collectFace,reportPlatePickup[$source]: 上报取餐盘时刻,userId=$id")
|
||
netViewModelV2.platePickup(id) { success, msg ->
|
||
loadLogInfo("collectFace,reportPlatePickup[$source]: 上报完成,success=$success,msg=$msg")
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 出盘成功后查询剩余餐盘状态,无盘则播放提示音
|
||
*/
|
||
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()
|
||
}
|
||
})
|
||
}
|
||
|
||
private val openDebouncer by lazy { Debouncer(5000) }
|
||
|
||
private fun initArcView() {
|
||
//在布局结束后才做初始化操作
|
||
binding.dualCameraTexturePreviewRgb.viewTreeObserver.addOnGlobalLayoutListener(this)
|
||
val list = recognizeViewModel.getCompareResultList().getValue()
|
||
loadLogInfo("initArcView:list.size=${list?.size}")
|
||
}
|
||
|
||
override fun onDestroy() {
|
||
loadLogInfo("onDestroy")
|
||
rgbCameraHelper?.release()
|
||
rgbCameraHelper = null
|
||
|
||
recognizeViewModel.destroy()
|
||
|
||
PlcHelper.getInstance(this@LoginByFaceActivity).release()
|
||
|
||
EventBus.getDefault().unregister(this)
|
||
super.onDestroy()
|
||
}
|
||
|
||
|
||
/**
|
||
* 调整View的宽高,使预览显示正常且采集框固定为380x380
|
||
*
|
||
* @param rgbPreview RGB预览View
|
||
* @param previewView 显示预览数据的view
|
||
* @param faceRectView 画框的view (380x380)
|
||
* @param previewSize 预览大小
|
||
* @param displayOrientation 相机旋转角度
|
||
* @param scale 缩放比例
|
||
* @return 调整后的LayoutParams
|
||
*/
|
||
private fun adjustPreviewViewSize(
|
||
rgbPreview: View,
|
||
previewView: View,
|
||
faceRectView: FaceRectView,
|
||
previewSize: Camera.Size,
|
||
displayOrientation: Int,
|
||
scale: Float
|
||
): ViewGroup.LayoutParams {
|
||
// val layoutParams = previewView.layoutParams
|
||
// val measuredWidth = previewView.measuredWidth
|
||
// val measuredHeight = previewView.measuredHeight
|
||
// var ratio = (previewSize.height.toFloat()) / previewSize.width.toFloat()
|
||
// if (ratio > 1) {
|
||
// ratio = 1 / ratio
|
||
// }
|
||
// if (displayOrientation % 180 == 0) {
|
||
// layoutParams.width = measuredWidth
|
||
// layoutParams.height = (measuredWidth * ratio).toInt()
|
||
// } else {
|
||
// layoutParams.height = measuredHeight
|
||
// layoutParams.width = (measuredHeight * ratio).toInt()
|
||
// }
|
||
// if (scale < 1f) {
|
||
// val rgbParam = rgbPreview.layoutParams
|
||
// layoutParams.width = (rgbParam.width * scale).toInt()
|
||
// layoutParams.height = (rgbParam.height * scale).toInt()
|
||
// } else {
|
||
// layoutParams.width = (layoutParams.width * scale).toInt()
|
||
// layoutParams.height = (layoutParams.height * scale).toInt()
|
||
// }
|
||
//
|
||
// val metrics = DisplayMetrics()
|
||
// windowManager.defaultDisplay.getMetrics(metrics)
|
||
//
|
||
// if (layoutParams.width >= metrics.widthPixels) {
|
||
// val viewRatio = layoutParams.width / (metrics.widthPixels.toFloat())
|
||
// layoutParams.width = (layoutParams.width / viewRatio).toInt()
|
||
// layoutParams.height = (layoutParams.height / viewRatio).toInt()
|
||
// }
|
||
// if (layoutParams.height >= metrics.heightPixels) {
|
||
// val viewRatio = layoutParams.height / (metrics.heightPixels.toFloat())
|
||
// layoutParams.width = (layoutParams.width / viewRatio).toInt()
|
||
// layoutParams.height = (layoutParams.height / viewRatio).toInt()
|
||
// }
|
||
|
||
val layoutParams = FrameLayout.LayoutParams(776, 582);
|
||
Timber.d("adjustPreviewViewSize,${layoutParams.width},${layoutParams.height}}")
|
||
previewView.setLayoutParams(layoutParams)
|
||
faceRectView.setLayoutParams(layoutParams)
|
||
return layoutParams
|
||
}
|
||
|
||
private fun initRgbCamera() {
|
||
val cameraListener: CameraListener = object : CameraListener {
|
||
override fun onCameraOpened(
|
||
camera: Camera,
|
||
cameraId: Int,
|
||
displayOrientation: Int,
|
||
isMirror: Boolean
|
||
) {
|
||
loadLogInfo("initRgbCamera Rgb onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation")
|
||
runOnUiThread {
|
||
val previewSizeRgb = camera.parameters.previewSize
|
||
val layoutParams = adjustPreviewViewSize(
|
||
binding.dualCameraTexturePreviewRgb,
|
||
binding.dualCameraTexturePreviewRgb, binding.dualCameraFaceRectView,
|
||
previewSizeRgb, displayOrientation, 0.7f
|
||
//0.6f
|
||
)
|
||
loadLogInfo("initRgbCamera previewSizeRgb = ${previewSizeRgb.width}-${previewSizeRgb.height}")
|
||
loadLogInfo("initRgbCamera layoutParams = ${layoutParams.width}-${layoutParams.height}")
|
||
loadLogInfo(
|
||
"initRgbCamera isMirror = ${isMirror}, isDrawRgbRectHorizontalMirror = ${
|
||
ConfigUtil.isDrawRgbRectHorizontalMirror(
|
||
context
|
||
)
|
||
}, isDrawRgbRectVerticalMirror = ${
|
||
ConfigUtil.isDrawRgbRectVerticalMirror(
|
||
context
|
||
)
|
||
}"
|
||
)
|
||
rgbFaceRectTransformer = FaceRectTransformer(
|
||
previewSizeRgb.width,
|
||
previewSizeRgb.height,
|
||
layoutParams.width,
|
||
layoutParams.height,
|
||
0,
|
||
cameraId,
|
||
isMirror,
|
||
false,
|
||
false
|
||
)
|
||
|
||
recognizeViewModel.onRgbCameraOpened(camera)
|
||
recognizeViewModel.setRgbFaceRectTransformer(rgbFaceRectTransformer)
|
||
}
|
||
}
|
||
|
||
@RequiresApi(api = Build.VERSION_CODES.Q)
|
||
override fun onPreview(nv21: ByteArray?, camera: Camera?) {
|
||
if (!isRecognition) {
|
||
return
|
||
}
|
||
binding.dualCameraFaceRectView.clearFaceInfo()
|
||
facePreviewInfoList = recognizeViewModel.onPreviewFrame(nv21, true)
|
||
if (facePreviewInfoList != null && rgbFaceRectTransformer != null) {
|
||
drawPreviewInfo(facePreviewInfoList)
|
||
}
|
||
if (facePreviewInfoList.isNullOrEmpty()) {
|
||
currentUserId = null
|
||
userFaceInfo = null
|
||
binding.btnCollect.tag = ""
|
||
binding.tvSimilarValue.text = ""
|
||
// 注意:此处不再重置 retryFailCount。
|
||
// 人脸检测会抖动(短暂丢帧),若每次无人脸就清零,"尝试几次失败"将无法累计。
|
||
// 失败计数仅在识别成功、会话重启、确认用户离开时清零。
|
||
// 人脸离开时,发送空帧清除 ViewModel 内部识别缓存,防止再次进入时使用旧数据
|
||
recognizeViewModel.onPreviewFrame(emptyFrame, true)
|
||
if (binding.llCollectButton.isVisible) {
|
||
leftDebouncer.debounce("leftDebouncer") { leftCountDown() }
|
||
}
|
||
}
|
||
recognizeViewModel.clearLeftFace(facePreviewInfoList)
|
||
}
|
||
|
||
override fun onCameraClosed() {
|
||
Timber.i("initRgbCamera onCameraClosed: ")
|
||
}
|
||
|
||
override fun onCameraError(e: java.lang.Exception) {
|
||
Timber.i("initRgbCamera onCameraError: %s", e.message)
|
||
e.printStackTrace()
|
||
}
|
||
|
||
override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) {
|
||
Timber.i("initRgbCamera onCameraConfigurationChanged: threadName = ${Thread.currentThread().name}")
|
||
if (rgbFaceRectTransformer != null) {
|
||
rgbFaceRectTransformer?.cameraDisplayOrientation = displayOrientation
|
||
}
|
||
Timber.i("initRgbCamera onCameraConfigurationChanged: $cameraID $displayOrientation")
|
||
}
|
||
}
|
||
|
||
val previewConfig: PreviewConfig = recognizeViewModel.previewConfig
|
||
rgbCameraHelper = DualCameraHelper.Builder()
|
||
.previewViewSize(
|
||
Point(
|
||
binding.dualCameraTexturePreviewRgb.measuredWidth,
|
||
binding.dualCameraTexturePreviewRgb.measuredHeight
|
||
)
|
||
)
|
||
.rotation(windowManager.defaultDisplay.rotation)
|
||
.additionalRotation(0) // 角度
|
||
.previewSize(recognizeViewModel.loadPreviewSize())
|
||
.specificCameraId(previewConfig.rgbCameraId)
|
||
// .isMirror(true)
|
||
.isMirror(true)
|
||
.previewOn(binding.dualCameraTexturePreviewRgb)
|
||
.cameraListener(cameraListener)
|
||
.build()
|
||
rgbCameraHelper?.init()
|
||
rgbCameraHelper?.start()
|
||
}
|
||
|
||
/**
|
||
* 绘制RGB、IR画面的实时人脸信息
|
||
*
|
||
* @param facePreviewInfoList RGB画面的实时人脸信息
|
||
*/
|
||
private fun drawPreviewInfo(facePreviewInfoList: MutableList<FacePreviewInfo>?) {
|
||
if (rgbFaceRectTransformer != null) {
|
||
val rgbDrawInfoList: MutableList<DrawInfo>? = recognizeViewModel.getDrawInfo(
|
||
facePreviewInfoList, LivenessType.RGB, openRectInfoDraw
|
||
)
|
||
// 识别成功
|
||
binding.dualCameraFaceRectView.drawRealtimeFaceInfo(rgbDrawInfoList)
|
||
}
|
||
}
|
||
|
||
// fun openRectInfoDraw(view: View?) {
|
||
// openRectInfoDraw = !openRectInfoDraw
|
||
// recognizeViewModel.setDrawRectInfoTextValue(openRectInfoDraw)
|
||
// }
|
||
|
||
|
||
override fun onGlobalLayout() {
|
||
loadLogInfo("onGlobalLayout")
|
||
binding.dualCameraTexturePreviewRgb.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||
// 请求权限
|
||
checkCameraPermission()
|
||
}
|
||
|
||
private fun resumeCamera() {
|
||
// isRecognition = true
|
||
val helper = rgbCameraHelper
|
||
if (helper != null && helper.isStopped) {
|
||
loadLogInfo("collectFace,resumeCamera: ")
|
||
isRecognition = true
|
||
helper.start()
|
||
} else {
|
||
//recognizeViewModel.onPreviewFrame(ByteArray(1382400), true)
|
||
|
||
recognizeViewModel.onPreviewFrame(emptyFrame, true)
|
||
binding.dualCameraFaceRectView.clearFaceInfo()
|
||
recognizeViewModel.clearLeftFace(facePreviewInfoList)
|
||
binding.root.postDelayed({
|
||
isRecognition = true
|
||
}, 500)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 构造符合分辨率的 NV21 格式全黑帧
|
||
* @param width 图像宽度(如 1920)
|
||
* @param height 图像高度(如 1080)
|
||
* @return 合法的全黑帧字节数组
|
||
*/
|
||
private fun createBlackNV21Frame(width: Int, height: Int): ByteArray {
|
||
val frameSize = width * height
|
||
val nv21Data = ByteArray(frameSize * 3 / 2) // NV21 格式长度 = 宽×高×1.5
|
||
|
||
// Y 分量(亮度)设为 0(全黑),UV 分量设为 128(默认值)
|
||
// 1. 填充 Y 分量(前 frameSize 个字节)
|
||
// Arrays.fill(nv21Data, 0, frameSize, 0.toByte())
|
||
nv21Data.fill(0, 0, frameSize)
|
||
// 2. 填充 UV 分量(后 frameSize/2 个字节)
|
||
// Arrays.fill(nv21Data, frameSize, nv21Data.size, 128.toByte())
|
||
nv21Data.fill(128.toByte(), frameSize, nv21Data.size)
|
||
|
||
return nv21Data
|
||
}
|
||
|
||
// 调用示例:构造 1920×1080 的全黑帧(长度 1382400)
|
||
// private val emptyFrame = createBlackNV21Frame(1920, 1080)
|
||
private val emptyFrame = createBlackNV21Frame(1280, 720)
|
||
// 传递“空帧” recognizeViewModel.onPreviewFrame(emptyFrame, true)
|
||
|
||
// /**
|
||
// * 临时用户id
|
||
// */
|
||
// private var tempUserId: String = ""
|
||
override fun onResume() {
|
||
super.onResume()
|
||
plateEnable = true
|
||
try {
|
||
if (isFirstOpen) {
|
||
isFirstOpen = false
|
||
goInitActivity()
|
||
return
|
||
}
|
||
|
||
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) {
|
||
e.printStackTrace()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 重置识别状态
|
||
*/
|
||
private fun resetRecognizeState() {
|
||
binding.ivCollectPhoto.gone()
|
||
binding.ivCollectPhoto.setImageDrawable(null)
|
||
binding.tvToInit.text = "15"
|
||
binding.llCollectButton.gone()
|
||
retryFailCount = 0
|
||
binding.tvSimilarValue.text = ""
|
||
isCollecting.set(false)
|
||
isCollectFace = false
|
||
plateEnable = true
|
||
collectCount = 0
|
||
// tempUserId = "${System.currentTimeMillis()}"
|
||
openPlateDebouncer.reset()
|
||
// 清空上次识别结果和 FaceHelper 内部状态,防止短时间内再次识别无法触发
|
||
recognizeViewModel.resetFaceState()
|
||
resumeCamera()
|
||
registerHandler = RegisterCallbackHandler()
|
||
|
||
recognizeCountDownUtil.startCountDown(
|
||
total = 15,
|
||
lifecycleScope = lifecycleScope,
|
||
onTick = { remaining ->
|
||
loadLogInfo("resetRecognizeState剩余时间:$remaining 秒")
|
||
// if (judgeRecognizeState()) return@startCountDown
|
||
loadLogInfo("initCountTime onTick = $remaining")
|
||
binding.tvToInit.text = "$remaining"
|
||
},
|
||
onFinish = {
|
||
loadLogInfo("resetRecognizeState倒计时结束")
|
||
// if (judgeRecognizeState()) return@startCountDown
|
||
if (facePreviewInfoList.isNullOrEmpty()) {
|
||
goInitActivity()
|
||
} else {
|
||
loadLogInfo("recognizeFail---initCountTime")
|
||
recognizeFail()
|
||
}
|
||
}
|
||
)
|
||
}
|
||
|
||
private var isFirstOpen = true
|
||
|
||
override fun onPause() {
|
||
pauseCamera()
|
||
super.onPause()
|
||
recognizeCountDownUtil.cancelCountDown()
|
||
}
|
||
|
||
private fun pauseCamera() {
|
||
isRecognition = false
|
||
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
|
||
// }
|
||
|
||
fun goInitActivity() {
|
||
val intent = Intent(context, InitActivity::class.java)
|
||
startActivity(intent)
|
||
}
|
||
|
||
/**
|
||
* 采集人脸
|
||
*/
|
||
private fun collectFace() {
|
||
loadLogInfo("collectFace: -----------1,开始采集人脸,开始时间:${DateTimeUtils.getDateTimeString()}----------")
|
||
if (isFirstOpen) return
|
||
loadLogInfo("collectFace: ----------2,isFirstOpen=false-----------")
|
||
loadLogInfo("collectFace: ----------4,检测到人脸信息-----------")
|
||
currentUserId = null
|
||
userFaceInfo = null
|
||
recognizeViewModel.updateRegisterStatus(REGISTER_STATUS_READY)
|
||
loadLogInfo("collectFace: ----------5,人脸采集中-----------")
|
||
}
|
||
|
||
private val uploadFaceDebouncer = Debouncer(10000)
|
||
|
||
/**
|
||
* 改为上传人脸数据,不需要发生人脸数据到收银端
|
||
*/
|
||
private fun sendFaceData(faceData: String, headBmp: Bitmap) {
|
||
loadLogInfo("collectFace,sendFaceData: ----------11,发送人脸数据-----------")
|
||
|
||
val imageFile = BitmapSaver.saveToAppFilesDir(
|
||
headBmp, this, "IMG_CROP_${System.currentTimeMillis()}.jpg"
|
||
)
|
||
loadLogInfo("collectFace,sendFaceData:${imageFile?.absolutePath}")
|
||
if (imageFile == null) {
|
||
ToastUtils.showToast("获取人脸照片失败")
|
||
return
|
||
}
|
||
uploadFaceDebouncer.debounce("uploadFaceDebouncer") {
|
||
addUserFace(faceData, imageFile)
|
||
}
|
||
}
|
||
|
||
private fun addUserFace(faceData: String, imageFile: File) {
|
||
netViewModelV2.addUserFace(faceData, imageFile) { status, faceModel ->
|
||
loadLogInfo("collectFace,saveFaceInfo: ----------12,人脸数据发送完成时间:${DateTimeUtils.getDateTimeString()},结果:$status-----------")
|
||
if (status.not()) {
|
||
ToastUtils.showToast("新增数据失败")
|
||
loadLogInfo("collectFace,sendFaceData: ----------12,发送人脸数据失败,faceData=${faceData}")
|
||
return@addUserFace
|
||
}
|
||
if (faceModel == null) {
|
||
ToastUtils.showToast("新增数据成功,但未拿到接口数据")
|
||
loadLogInfo("collectFace,sendFaceData: ----------12,发送人脸数据成功但接口返回为null,faceData=${faceData}")
|
||
return@addUserFace
|
||
}
|
||
// 新用户:注册成功拿到真实 userId 后,上报取餐盘时刻(P-09a)
|
||
reportPlatePickup(faceModel.userId, "collect")
|
||
lifecycleScope.launch(Dispatchers.IO) {
|
||
saveFaceInfo(faceModel) {
|
||
runOnUiThread {
|
||
Glide.with(this@LoginByFaceActivity).load(imageFile)
|
||
.into(binding.ivCollectPhoto)
|
||
}
|
||
}
|
||
uploadFaceDebouncer.reset()
|
||
}
|
||
}
|
||
}
|
||
|
||
private fun getFaceDataBlock() {
|
||
if (userFaceInfo?.faceFeature?.featureData == null) {
|
||
loadLogInfo("collectFace,getFaceData: ----------7,拿到人脸提取特征数据为null-----------")
|
||
return
|
||
}
|
||
val faceData: String? = Base64.encode(userFaceInfo?.faceFeature?.featureData)
|
||
if (faceData.isNullOrBlank()) {
|
||
loadLogInfo("collectFace,getFaceData: ----------7,拿到人脸提取特征数据Base64编码为空")
|
||
return
|
||
}
|
||
loadLogInfo("collectFace,getFaceData: ----------7,拿到人脸提取特征数据时间:${DateTimeUtils.getDateTimeString()}-----------")
|
||
val headBmp = userFaceInfo!!.headBmp
|
||
//block(faceData, headBmp)
|
||
isCollecting.set(false)
|
||
isCollectFace = false
|
||
// 采集成功后立即出盘,与上传后台并行执行,确保上传失败时仍能吐盘
|
||
outPlateForRegister()
|
||
// 异步上传后台(上传成功后才入库),不阻塞出盘
|
||
sendFaceData(faceData, headBmp)
|
||
}
|
||
|
||
/**
|
||
* 加载日志
|
||
* @param msg 日志内容
|
||
*/
|
||
private fun loadLogInfo(msg: String) {
|
||
Timber.tag(TAG).d(msg)
|
||
LogFileUtil.saveLog(msg)
|
||
}
|
||
|
||
private var facePreviewInfoList: MutableList<FacePreviewInfo>? = null
|
||
private var userFaceInfo: UserFaceInfo? = null
|
||
|
||
private var currentUserId: String? = null
|
||
|
||
private fun saveFaceInfo(faceModel: UserFaceModelV2, block: () -> Unit = {}) {
|
||
try {
|
||
loadLogInfo("collectFace,saveFaceInfo: ----------8,保存人脸取特征数据-----------")
|
||
val faceDao = FaceDatabase.getInstance(this).faceDao()
|
||
val faceEntity = faceModel.toFaceEntity()
|
||
// 先查后插:按 userFaceId 判重,避免采集入库与增量同步各插一条
|
||
val userFaceId = faceModel.userFaceId
|
||
val existing = if (!userFaceId.isNullOrEmpty()) faceDao.queryByUserFaceId(userFaceId) else null
|
||
val faceId = if (existing != null) {
|
||
// 已存在记录,更新特征等字段并复用主键,避免重复
|
||
faceEntity.faceId = existing.faceId
|
||
faceDao.updateFaceEntity(faceEntity)
|
||
existing.faceId
|
||
} else {
|
||
faceDao.insert(faceEntity)
|
||
}
|
||
faceEntity.faceId = faceId
|
||
loadLogInfo("collectFace,saveFaceInfo: ----------10,人脸数据保存完成-----------")
|
||
recognizeViewModel.refreshFaceList()
|
||
loadLogInfo("collectFace,saveFaceInfo: ----------11,刷新人脸数据-----------")
|
||
runOnUiThread {
|
||
//采集完成,停止识别(出盘已在采集成功时由 outPlateForRegister 触发,此处仅负责入库)
|
||
isRecognition = false
|
||
}
|
||
loadLogInfo("---------------------------------")
|
||
collectCount = 0
|
||
block()
|
||
} catch (e: Exception) {
|
||
e.printStackTrace()
|
||
loadLogInfo("collectFace,saveFaceInfo: ----------13,人脸采集异常:${e.message}-----------")
|
||
collectCount++
|
||
loadLogInfo("collectFace,saveFaceInfo: ----------14,${collectCount}次人脸采集异常:${e.message}-----------")
|
||
ToastUtils.showToast("人脸采集异常:${e.message}")
|
||
}
|
||
}
|
||
|
||
private var collectCount = 0
|
||
|
||
private var isCollectFace = false
|
||
private fun leftCountDown() {
|
||
leftCountDownUtil.startCountDown(total = 15, lifecycleScope = lifecycleScope, onStart = {
|
||
loadLogInfo("onPreview-----------leftCountDown-------------")
|
||
}, onTick = {
|
||
loadLogInfo("leftCountDown.onTick:$it")
|
||
if (facePreviewInfoList.isNullOrEmpty().not()) {
|
||
loadLogInfo("leftCountDown.onTick-cancel()")
|
||
leftCountDownUtil.cancelCountDown()
|
||
return@startCountDown
|
||
}
|
||
}, onFinish = {
|
||
if (facePreviewInfoList.isNullOrEmpty().not()) {
|
||
loadLogInfo("leftCountDown.onFinish-人脸未离开,不跳转")
|
||
return@startCountDown
|
||
}
|
||
// 确认用户已离开画面,重置识别失败计数,下次进入重新累计
|
||
retryFailCount = 0
|
||
goInitActivity()
|
||
})
|
||
}
|
||
} |