打开app不再清除本地数据,不再发送数据到结算终端,改为新增人脸采集接口,全量增量接口优化

This commit is contained in:
2026-02-03 14:59:22 +08:00
parent c5325f6ad6
commit 9fea23303c
23 changed files with 857 additions and 363 deletions
@@ -34,6 +34,11 @@ class MyApp : App() {
Log.d("MyApp", "initialize: deviceId=$deviceId") Log.d("MyApp", "initialize: deviceId=$deviceId")
GlobalData.deviceId = deviceId GlobalData.deviceId = deviceId
// TODO: 临时测试----------------
// if ("2987f0c5-5754-33e9-b00a-251db5e2e55f" == deviceId) {
// GlobalData.deviceId = "79814f20-f253-397c-9da7-8430ffa5ef00"
// }
GlobalData.appVersion = AppUtil.getAppVersionCode(this).toString() GlobalData.appVersion = AppUtil.getAppVersionCode(this).toString()
GlobalData.globalEquipmentCode = "202501171634" GlobalData.globalEquipmentCode = "202501171634"
@@ -1,6 +1,5 @@
package com.sw.platecabinet.activity package com.sw.platecabinet.activity
import android.app.Dialog
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@@ -15,23 +14,19 @@ 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
import com.sw.plate.App
import com.sw.plate.utils.Base64 import com.sw.plate.utils.Base64
import com.sw.plate.utils.ScanGunKeyEventHelper import com.sw.plate.utils.ScanGunKeyEventHelper
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.facedb.FaceDatabase import com.sw.plate.utils.arcface.facedb.FaceDatabase
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import com.sw.plate.utils.comn.SerialApi
import com.sw.plate.utils.comn.SerialPortManager
import com.sw.platecabinet.MyApp import com.sw.platecabinet.MyApp
import com.sw.platecabinet.R import com.sw.platecabinet.R
import com.sw.platecabinet.databinding.ItemTitleTimeBinding import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.dialog.BalanceNotEnoughDialog import com.sw.platecabinet.ext.clickWithCoroutines
import com.sw.platecabinet.ext.setClickListeners
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.IntervalExecutor import com.sw.platecabinet.utils.IntervalExecutor
@@ -60,10 +55,10 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
protected var keyEventHelper: ScanGunKeyEventHelper? = null protected var keyEventHelper: ScanGunKeyEventHelper? = null
// 管理员对应的viewmodel // 管理员对应的viewmodel
private val settingViewModel by viewModels<SettingViewModel>() val settingViewModel by viewModels<SettingViewModel>()
// 用户对应的viewModel // 用户对应的viewModel
protected val viewModel by viewModels<UserViewModel>() protected val userViewModel by viewModels<UserViewModel>()
private var startTime: Long = 0 private var startTime: Long = 0
@@ -88,8 +83,6 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
registerKeyEvent() registerKeyEvent()
val durationTime = System.currentTimeMillis() - startTime val durationTime = System.currentTimeMillis() - startTime
Timber.d("启动时间:$durationTime") Timber.d("启动时间:$durationTime")
startFaceTask()
} }
/** /**
@@ -115,7 +108,7 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
if (scanInfo.length > 6) { if (scanInfo.length > 6) {
plateNumber = scanInfo.take(6) plateNumber = scanInfo.take(6)
} }
Log.d("TAG", "handleScanKeyInfo: "+ Gson().toJson(MyApp.plateList)) Log.d("TAG", "handleScanKeyInfo: " + Gson().toJson(MyApp.plateList))
if (MyApp.plateList == null) { if (MyApp.plateList == null) {
initPlateList() initPlateList()
} }
@@ -146,12 +139,12 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
fun updateTime() { fun updateTime() {
headerBinding?.let { headerBinding?.let {
it.tvLeftDate.setClickListeners( it.tvLeftDate.clickWithCoroutines(
onDoubleClick = { onDoubleClick = {
onLeftDoubleClick() onLeftDoubleClick()
} }
) )
it.tvRightTime.setClickListeners( it.tvRightTime.clickWithCoroutines(
onDoubleClick = { onDoubleClick = {
onRightDoubleClick() onRightDoubleClick()
} }
@@ -426,7 +419,6 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
// private val dealyMillis = 10 * 60 * 1000L // private val dealyMillis = 10 * 60 * 1000L
private val initialDelay = 5 * 60 * 1000L private val initialDelay = 5 * 60 * 1000L
private val dealyMillis = 5 * 60 * 1000L private val dealyMillis = 5 * 60 * 1000L
private var taskPageNo = 1
fun startFaceTask() { fun startFaceTask() {
faceTaskJob = faceTaskJob =
intervalExecutor.startIntervalTaskWithInitialDelay(initialDelay, dealyMillis) { intervalExecutor.startIntervalTaskWithInitialDelay(initialDelay, dealyMillis) {
@@ -435,28 +427,28 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
} }
private var faceTimestamp = 0L private var faceTimestamp = 0L
//private var taskPageNo = 1
private fun getFaceIncrementList() { private fun getFaceIncrementList(pageNo: Int = 1) {
val timestamp = SpTool.lastFaceTimestamp val timestamp = SpTool.lastFaceTimestamp
if (timestamp == 0L) { if (timestamp == 0L) {
return return
} }
settingViewModel.getFaceIncrementList( settingViewModel.getFaceIncrementList(
pageNo = taskPageNo, pageNo = pageNo,
timestamp = timestamp timestamp = timestamp
) { list -> ) { list ->
runOnUiThread { runOnUiThread {
if (taskPageNo == 1 && list.isEmpty()) { if (pageNo == 1 && list.isEmpty()) {
//未查询到增量数据 //未查询到增量数据
return@runOnUiThread return@runOnUiThread
} }
updateFaceData(list) updateFaceData(list)
if (list.size >= settingViewModel.PAGE_SIZE) { //if (list.size >= settingViewModel.PAGE_SIZE) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0 // faceTimestamp = list.last().faceUpdateTimestamp ?: 0
taskPageNo++ // taskPageNo++
getFaceIncrementList() // getFaceIncrementList(taskPageNo)
return@runOnUiThread // return@runOnUiThread
} //}
if (list.isNotEmpty()) { if (list.isNotEmpty()) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0 faceTimestamp = list.last().faceUpdateTimestamp ?: 0
} }
@@ -481,7 +473,8 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
null, null,
Base64.decode(model.faceFeatureStr) Base64.decode(model.faceFeatureStr)
).also { ).also {
it.userType = "1" //1-会员、2-临时用户
it.userType = if (model.isMember) "1" else "2"
} }
faceList.add(faceEntity) faceList.add(faceEntity)
} }
@@ -512,4 +505,13 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
MyApp.plateList = list MyApp.plateList = list
} }
fun clearAllFace(block: () -> Unit) {
Thread {
val faceDao = FaceDatabase.getInstance(App.getContext()).faceDao()
faceDao.deleteAll()
faceDao.resetId()
runOnUiThread { block() }
}.start()
}
} }
@@ -1,22 +1,15 @@
package com.sw.platecabinet.activity package com.sw.platecabinet.activity
import android.content.Intent import android.content.Intent
import android.util.Log import androidx.lifecycle.lifecycleScope
import androidx.activity.viewModels
import com.sw.inbound.utils.DateTimeUtils
import com.sw.plate.App
import com.sw.plate.utils.AppUtil
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.facedb.FaceDatabase import com.sw.plate.utils.arcface.FaceApi
import com.sw.platecabinet.GlobalData import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.MyApp
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.ext.gone import kotlinx.coroutines.Dispatchers
import com.sw.platecabinet.ext.invisible import kotlinx.coroutines.launch
import com.sw.platecabinet.model.response.EquipmentUserInfo import kotlinx.coroutines.withContext
import com.sw.platecabinet.utils.SpTool
import com.sw.platecabinet.viewmodel.SettingViewModel
/** /**
* 设备初始化界面 * 设备初始化界面
@@ -53,37 +46,38 @@ class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
// }, 1000) // }, 1000)
initPlateList() initPlateList()
showWaitingDialog("加载中……") showWaitingDialog("加载中……")
clearAllFace { lifecycleScope.launch {
viewModel.getUserFaceCache() withContext(Dispatchers.Default) {
viewModel.getDeviceConfig { deviceConfig -> val count = FaceApi().queryFaceCount()
runOnUiThread { if (count == 0) {
if (deviceConfig == null) { userViewModel.getUserFaceCache { status, msg ->
hideWaitingDialog() getDeviceConfig()
ToastUtils.showToast("获取设备配置数据失败")
hideWaitingDialog()
goLoginActivity()
return@runOnUiThread
} }
GlobalData.appId = deviceConfig.arcsoftAppId ?: ""
GlobalData.sdkKey = deviceConfig.arcsoftSdkKey ?: ""
GlobalData.activeKey = deviceConfig.arcsoftActiveKey ?: ""
binding.root.postDelayed({
hideWaitingDialog()
goLoginActivity()
}, 500)
} }
} }
} }
// goLoginActivity() getDeviceConfig()
} }
private fun clearAllFace(block: () -> Unit) { private fun getDeviceConfig() {
Thread { userViewModel.getDeviceConfig { deviceConfig ->
val faceDao = FaceDatabase.getInstance(App.getContext()).faceDao() runOnUiThread {
faceDao.deleteAll() if (deviceConfig == null) {
faceDao.resetId() hideWaitingDialog()
runOnUiThread { block() } ToastUtils.showToast("获取设备配置数据失败")
}.start() hideWaitingDialog()
goLoginActivity()
return@runOnUiThread
}
GlobalData.appId = deviceConfig.arcsoftAppId ?: ""
GlobalData.sdkKey = deviceConfig.arcsoftSdkKey ?: ""
GlobalData.activeKey = deviceConfig.arcsoftActiveKey ?: ""
binding.root.postDelayed({
hideWaitingDialog()
goLoginActivity()
}, 500)
}
}
} }
override fun registerDataChange() { override fun registerDataChange() {
@@ -2,6 +2,7 @@ package com.sw.platecabinet.activity
import android.Manifest import android.Manifest
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Point import android.graphics.Point
import android.hardware.Camera import android.hardware.Camera
import android.os.Build import android.os.Build
@@ -14,7 +15,6 @@ import androidx.annotation.RequiresApi
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.arcsoft.face.ErrorInfo import com.arcsoft.face.ErrorInfo
import com.google.gson.Gson import com.google.gson.Gson
import com.sw.platecabinet.socket.TcpClientListenerImpl
import com.sw.inbound.utils.DateTimeUtils import com.sw.inbound.utils.DateTimeUtils
import com.sw.plate.utils.Base64 import com.sw.plate.utils.Base64
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
@@ -35,17 +35,15 @@ import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import com.sw.plate.utils.arcface.model.UserFaceInfo import com.sw.plate.utils.arcface.model.UserFaceInfo
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel.REGISTER_STATUS_READY import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel.REGISTER_STATUS_READY
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.MyApp import com.sw.platecabinet.MyApp
import com.sw.platecabinet.R import com.sw.platecabinet.R
import com.sw.platecabinet.databinding.ActivityLoginFaceBinding import com.sw.platecabinet.databinding.ActivityLoginFaceBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.model.response.EquipmentUserInfo import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.socket.LanServer import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.socket.LanServerListenerImpl import com.sw.platecabinet.utils.BitmapSaver
import com.sw.platecabinet.socket.TcpClient
import com.sw.platecabinet.utils.Debouncer import com.sw.platecabinet.utils.Debouncer
import com.sw.platecabinet.utils.FileUtil import com.sw.platecabinet.utils.LogFileUtil
import com.sw.platecabinet.utils.PermissionHelper import com.sw.platecabinet.utils.PermissionHelper
import com.sw.platecabinet.utils.PlateUtils import com.sw.platecabinet.utils.PlateUtils
import com.sw.platecabinet.utils.SoundPoolUtil import com.sw.platecabinet.utils.SoundPoolUtil
@@ -53,7 +51,6 @@ import com.sw.platecabinet.utils.SpTool
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 org.json.JSONObject
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@@ -98,15 +95,14 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
override fun initialize() { override fun initialize() {
instance = this instance = this
viewModel.activeEngine() userViewModel.activeEngine()
addSocketListener() // addSocketListener()
initCountTime() initCountTime()
initArcViewModel() initArcViewModel()
initArcView() initArcView()
openRectInfoDraw = true openRectInfoDraw = true
recognizeViewModel.setDrawRectInfoTextValue(true) recognizeViewModel.setDrawRectInfoTextValue(true)
//viewModel.generateToken() //viewModel.generateToken()
// viewModel.activeEngine()
// binding.llToPwd.setOnClickListener { // binding.llToPwd.setOnClickListener {
// val intent = Intent(this, LoginByPwdActivity::class.java) // val intent = Intent(this, LoginByPwdActivity::class.java)
@@ -141,6 +137,8 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
// } // }
// } // }
//开启人脸增量数据定时任务
startFaceTask()
val soundMap = hashMapOf<String, Int>() val soundMap = hashMapOf<String, Int>()
soundMap[COLLECT_SUCCESS] = R.raw.collect_success soundMap[COLLECT_SUCCESS] = R.raw.collect_success
@@ -193,11 +191,11 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
if (this.userFaceInfo == null && userFaceInfo != null) { if (this.userFaceInfo == null && userFaceInfo != null) {
recognizeViewModel.updateRegisterStatus(RecognizeViewModel.REGISTER_STATUS_DONE) recognizeViewModel.updateRegisterStatus(RecognizeViewModel.REGISTER_STATUS_DONE)
this.userFaceInfo = userFaceInfo this.userFaceInfo = userFaceInfo
getFaceDataBlock { faceData -> getFaceDataBlock { faceData, headBmp ->
isCollecting.set(false) isCollecting.set(false)
isCollectFace = false isCollectFace = false
Thread { Thread {
saveFaceInfo(faceData) sendFaceData(faceData, headBmp)
}.start() }.start()
} }
return@setOnRegisterFinishedCallback return@setOnRegisterFinishedCallback
@@ -476,8 +474,6 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
recognizeViewModel.destroy() recognizeViewModel.destroy()
EventBus.getDefault().unregister(this) EventBus.getDefault().unregister(this)
// getFaceScheduler?.shutdown()
// checkFaceScheduler?.shutdown()
super.onDestroy() super.onDestroy()
} }
@@ -705,7 +701,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
collectCount = 0 collectCount = 0
tempUserId = "${System.currentTimeMillis()}" tempUserId = "${System.currentTimeMillis()}"
resumeCamera() resumeCamera()
viewModel.resetUserInfo() userViewModel.resetUserInfo()
countDownTimer?.let { countDownTimer?.let {
it.cancel() it.cancel()
it.start() it.start()
@@ -754,6 +750,9 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
startActivity(intent) startActivity(intent)
} }
/**
* 采集人脸
*/
private fun collectFace() { private fun collectFace() {
loadLogInfo("collectFace: -----------1,开始采集人脸,开始时间:${DateTimeUtils.getDateTimeString()}----------") loadLogInfo("collectFace: -----------1,开始采集人脸,开始时间:${DateTimeUtils.getDateTimeString()}----------")
if (isFirstOpen) return if (isFirstOpen) return
@@ -765,25 +764,43 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
loadLogInfo("collectFace: ----------5,人脸采集中-----------") loadLogInfo("collectFace: ----------5,人脸采集中-----------")
} }
private fun sendFaceData(faceData: String) { /**
* 改为上传人脸数据,不需要发生人脸数据到收银端
*/
private fun sendFaceData(faceData: String, headBmp: Bitmap) {
loadLogInfo("collectFace,sendFaceData: ----------11,发送人脸数据-----------") loadLogInfo("collectFace,sendFaceData: ----------11,发送人脸数据-----------")
// 给某个客户端发送
val jsonObject = JSONObject().also { val imageFile = BitmapSaver.saveToAppFilesDir(
it.put("type", "faceFeature") headBmp, this, "IMG_CROP_${System.currentTimeMillis()}.jpg"
it.put("content", faceData) )
loadLogInfo("collectFace,sendFaceData:${imageFile?.absolutePath}")
if (imageFile == null) {
ToastUtils.showToast("获取人脸照片失败")
return
} }
tcpClient?.send(jsonObject) settingViewModel.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
}
saveFaceInfo(faceModel)
}
// // 给某个客户端发送
// val jsonObject = JSONObject().also {
// it.put("type", "faceFeature")
// it.put("content", faceData)
// }
// tcpClient?.send(jsonObject)
} }
// private var getFaceScheduler: ScheduledExecutorService? = null private fun getFaceDataBlock(block: (String, Bitmap) -> Unit) {
// private var checkFaceScheduler: ScheduledExecutorService? = null
//
// private fun initFaceScheduler() {
//// getFaceScheduler = Executors.newScheduledThreadPool(1)
// checkFaceScheduler = Executors.newScheduledThreadPool(1)
// }
private fun getFaceDataBlock(block: (String) -> Unit) {
if (userFaceInfo == null || userFaceInfo?.faceFeature?.featureData == null) { if (userFaceInfo == null || userFaceInfo?.faceFeature?.featureData == null) {
return return
} }
@@ -792,7 +809,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
return return
} }
loadLogInfo("collectFace,getFaceData: ----------7,拿到人脸提取特征数据时间:${DateTimeUtils.getDateTimeString()}-----------") loadLogInfo("collectFace,getFaceData: ----------7,拿到人脸提取特征数据时间:${DateTimeUtils.getDateTimeString()}-----------")
block(faceData) block(faceData, userFaceInfo!!.headBmp)
} }
/** /**
@@ -801,7 +818,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
*/ */
private fun loadLogInfo(msg: String) { private fun loadLogInfo(msg: String) {
Timber.tag(TAG).d(msg) Timber.tag(TAG).d(msg)
FileUtil.saveLog(msg) LogFileUtil.saveLog(msg)
} }
private var facePreviewInfoList: MutableList<FacePreviewInfo>? = null private var facePreviewInfoList: MutableList<FacePreviewInfo>? = null
@@ -809,23 +826,17 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
private var currentUserId: String? = null private var currentUserId: String? = null
private fun saveFaceInfo(faceData: String) { private fun saveFaceInfo(faceModel: UserFaceModel) {
try { try {
sendFaceData(faceData)
loadLogInfo("collectFace,saveFaceInfo: ----------12,人脸数据发送完成时间:${DateTimeUtils.getDateTimeString()}-----------")
// runOnUiThread {
// openPlate(tempUserId)
// }
loadLogInfo("collectFace,saveFaceInfo: ----------8,保存人脸取特征数据-----------") loadLogInfo("collectFace,saveFaceInfo: ----------8,保存人脸取特征数据-----------")
val faceDao = FaceDatabase.getInstance(this).faceDao() val faceDao = FaceDatabase.getInstance(this).faceDao()
val faceEntity = FaceEntity( val faceEntity = FaceEntity(
tempUserId, faceModel.userId,
null, null,
Base64.decode(faceData) Base64.decode(faceModel.faceFeatureStr)
).also { ).also {
it.userType = "2" //1-会员、2-临时用户
it.userType = if (faceModel.isMember) "1" else "2"
} }
faceDao.insert(faceEntity) faceDao.insert(faceEntity)
loadLogInfo("collectFace,saveFaceInfo: ----------10,人脸数据保存完成-----------") loadLogInfo("collectFace,saveFaceInfo: ----------10,人脸数据保存完成-----------")
@@ -843,70 +854,70 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
} }
} }
private var collectCount = 0; private var collectCount = 0
private var tcpClient: TcpClient? = null // private var tcpClient: TcpClient? = null
//
private fun addSocketListener() { // private fun addSocketListener() {
// val ipAddress = NetworkUtils.getIPAddress(true) //// val ipAddress = NetworkUtils.getIPAddress(true)
// loadLogInfo( "addSocketListener,ipAddress: $ipAddress"); //// loadLogInfo( "addSocketListener,ipAddress: $ipAddress");
tcpClient = TcpClient( // tcpClient = TcpClient(
"192.168.1.95", // "192.168.1.95",
5000, // 5000,
GlobalData.deviceId, // clientId // GlobalData.deviceId, // clientId
5000, // connectTimeoutMs // 5000, // connectTimeoutMs
10 * 1000, // heartbeatIntervalMs // 10 * 1000, // heartbeatIntervalMs
30 * 1000 // heartbeatTimeoutMs // 30 * 1000 // heartbeatTimeoutMs
) // )
//
tcpClient?.setListener(object : TcpClientListenerImpl() { // tcpClient?.setListener(object : TcpClientListenerImpl() {
override fun onSendSuccess(json: JSONObject?) { // override fun onSendSuccess(json: JSONObject?) {
super.onSendSuccess(json) // super.onSendSuccess(json)
loadLogInfo("collectFace,TcpClientListener发送成功:$json") // loadLogInfo("collectFace,TcpClientListener发送成功:$json")
// toast("发送成功") //// toast("发送成功")
} // }
//
override fun onSendFailed(json: JSONObject?, e: java.lang.Exception?) { // override fun onSendFailed(json: JSONObject?, e: java.lang.Exception?) {
super.onSendFailed(json, e) // super.onSendFailed(json, e)
ToastUtils.showToast("发送失败${e?.message}") // ToastUtils.showToast("发送失败${e?.message}")
loadLogInfo("collectFace,TcpClientListener人脸数据发送失败:${e?.message}") // loadLogInfo("collectFace,TcpClientListener人脸数据发送失败:${e?.message}")
} // }
//
override fun onMessage(json: JSONObject?) { // override fun onMessage(json: JSONObject?) {
super.onMessage(json) // super.onMessage(json)
loadLogInfo("collectFace,TcpClientListener收到消息:$json") // loadLogInfo("collectFace,TcpClientListener收到消息:$json")
val type = json?.getInt("type") // val type = json?.getInt("type")
// 根据服务端消息类型处理删除新增临时用户人脸数据--------------------- // // 根据服务端消息类型处理删除新增临时用户人脸数据---------------------
when (type) { // when (type) {
LanServer.TYPE_ADD_FACE_DATA -> { // LanServer.TYPE_ADD_FACE_DATA -> {
//采集设备id自己生成,不接收服务端的,在本地只判断是否存在同一人脸数据 // //采集设备id自己生成,不接收服务端的,在本地只判断是否存在同一人脸数据
loadLogInfo("collectFace,TcpClientListener收到消息,新增用户:采集设备id自己生成,不接收服务端的,在本地只判断是否存在同一人脸数据") // loadLogInfo("collectFace,TcpClientListener收到消息,新增用户:采集设备id自己生成,不接收服务端的,在本地只判断是否存在同一人脸数据")
} // }
//
LanServer.TYPE_CLEAR_FACE_DATA -> { // LanServer.TYPE_CLEAR_FACE_DATA -> {
clearFaceData() // clearFaceData()
} // }
//
else -> {} // else -> {}
} // }
} // }
}) // })
tcpClient?.start() // tcpClient?.start()
//
try { // try {
LanServer.getInstance().let { // LanServer.getInstance().let {
it.setListener(object : LanServerListenerImpl() { // it.setListener(object : LanServerListenerImpl() {
override fun onMessageReceived(clientId: String?, message: JSONObject?) { // override fun onMessageReceived(clientId: String?, message: JSONObject?) {
super.onMessageReceived(clientId, message) // super.onMessageReceived(clientId, message)
ToastUtils.showToast("收到消息clientId=$clientId") // ToastUtils.showToast("收到消息clientId=$clientId")
} // }
}) // })
it.start() // it.start()
} // }
} catch (e: Exception) { // } catch (e: Exception) {
e.printStackTrace() // e.printStackTrace()
} // }
} // }
private fun clearFaceData() { private fun clearFaceData() {
Thread { Thread {
@@ -917,34 +928,6 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
}.start() }.start()
} }
// private val WAITING_TIME = 3 * 1000L
// private var checkStartTime = 0L
private fun checkFaceState() {
// checkStartTime = System.currentTimeMillis()
//
// checkFaceScheduler?.scheduleWithFixedDelay(
// {
// checkFaceBlock()
// }, 5L, 5L, TimeUnit.MILLISECONDS
// )
}
private var isCollectFace = false private var isCollectFace = false
// private fun checkFaceBlock() {
// loadLogInfo("collectFace,currentUserId=${currentUserId}")
// if (currentUserId.isNullOrBlank().not()) {
// isCollectFace = false
//
// checkFaceScheduler?.shutdown()
// return
// }
//// if (System.currentTimeMillis() - checkStartTime >= WAITING_TIME) {
//// //超过1秒采集人脸并识别
//// checkFaceScheduler?.shutdown()
//// isCollectFace = true
//// collectFace()
//// }
// }
} }
@@ -2,14 +2,14 @@ package com.sw.platecabinet.activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.sw.platecabinet.MyApp import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.R import com.sw.platecabinet.R
import com.sw.platecabinet.databinding.ActivityMainBinding import com.sw.platecabinet.databinding.ActivityMainBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.dialog.CustomDialog
import com.sw.platecabinet.fragment.PlateCabinetFullFragment import com.sw.platecabinet.fragment.PlateCabinetFullFragment
import com.sw.platecabinet.fragment.PlateOpenFragment import com.sw.platecabinet.fragment.PlateOpenFragment
import com.sw.platecabinet.fragment.SettingListFragment import com.sw.platecabinet.fragment.SettingListFragment
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.utils.FragmentHelper import com.sw.platecabinet.utils.FragmentHelper
typealias Callback = (String) -> Unit typealias Callback = (String) -> Unit
@@ -49,7 +49,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
// context.startActivity(intent) // context.startActivity(intent)
// } // }
fun start(context: Context,pageType: PageType, isScanCode: Boolean = false) { fun start(context: Context, pageType: PageType, isScanCode: Boolean = false) {
val intent = Intent(context, MainActivity::class.java) val intent = Intent(context, MainActivity::class.java)
intent.putExtra(PARAM_PAGE_TYPE, pageType.name) intent.putExtra(PARAM_PAGE_TYPE, pageType.name)
intent.putExtra(PlateOpenFragment.IS_SCAN_CODE, isScanCode) intent.putExtra(PlateOpenFragment.IS_SCAN_CODE, isScanCode)
@@ -85,7 +85,9 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
} }
override fun onRightDoubleClick() { override fun onRightDoubleClick() {
if (pageType == PageType.SETTING_LIST) {
loadClearDialog()
}
} }
override fun handleScanKeyInfo(scanInfo: String) { override fun handleScanKeyInfo(scanInfo: String) {
@@ -101,8 +103,27 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
override fun registerDataChange() { override fun registerDataChange() {
super.registerDataChange() super.registerDataChange()
} }
}
/**
* 加载清除数据弹窗
*/
private fun loadClearDialog() {
CustomDialog(
activity = this,
content = "请确认是否清除本地所有数据,并重新拉取全量人脸数据?",
onConfirm = {
showWaitingDialog("加载中……")
clearAllFace {
userViewModel.getUserFaceCache { status, msg ->
hideWaitingDialog()
ToastUtils.showToast(
if (status) "请求成功,$msg" else "请求失败,$msg"
)
}
}
}).show()
}
}
/** /**
* 界面类型 * 界面类型
@@ -0,0 +1,48 @@
package com.sw.platecabinet.dialog
import android.app.Dialog
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.Window
import androidx.fragment.app.FragmentActivity
import com.sw.platecabinet.databinding.DialogCustomBinding
import com.sw.platecabinet.databinding.DialogUserBindRemindBinding
import com.sw.platecabinet.ext.dp
open class CustomDialog(
var activity: FragmentActivity,
var content: String,
var onCancel: () -> Unit = {},
var onConfirm: () -> Unit = {},
) : Dialog(activity) {
private lateinit var binding: DialogCustomBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
binding = DialogCustomBinding.inflate(layoutInflater)
setContentView(binding.root)
window?.run {
attributes = attributes.apply {
width = 480.dp
}
setBackgroundDrawable(ColorDrawable())
setCancelable(false)
}
binding.tvContent.text = content
addClickListener()
}
private fun addClickListener() {
binding.btnCancel.setOnClickListener {
onCancel()
dismiss()
}
binding.btnConfirm.setOnClickListener {
onConfirm()
dismiss()
}
}
}
@@ -4,6 +4,11 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/** /**
* 设置 TextView 的双击和单击事件监听器 * 设置 TextView 的双击和单击事件监听器
@@ -30,4 +35,38 @@ fun TextView.setClickListeners(
clickCount = 0 // 重置计数 clickCount = 0 // 重置计数
}, doubleClickInterval) }, doubleClickInterval)
} }
}
fun View.clickWithCoroutines(
doubleClickInterval: Long = 300,
onDoubleClick: (View) -> Unit = {},
onSingleClick: (View) -> Unit = {}
) {
var pendingItem: View? = null
var lastClickTime: Long = 0
var clickJob: Job? = null
setOnClickListener { view ->
val currentTime = System.currentTimeMillis()
// 如果是同一控件且在双击间隔内,则判定为双击
if (pendingItem === view && currentTime - lastClickTime < doubleClickInterval) {
// 取消之前的单击任务
clickJob?.cancel()
onDoubleClick(view)
pendingItem = null
} else {
// 取消之前的单击任务
clickJob?.cancel()
// 启动新的单击任务
clickJob = CoroutineScope(Dispatchers.Main).launch {
delay(doubleClickInterval)
onSingleClick(view)
}
pendingItem = view
lastClickTime = currentTime
}
}
} }
@@ -25,6 +25,18 @@ data class UserFaceModel(
val userId: String? = "", val userId: String? = "",
val faceFeatureStr: String? = "", val faceFeatureStr: String? = "",
val faceUpdateTimestamp:Long?=null, val faceUpdateTimestamp: Long? = null,
val faceDeleted: Boolean?=false /**
* 人脸删除标识
*/
val faceDeleted: Boolean? = false,
/**
* 会员编号
*/
val cardNo: String,
/**
* 是否会员
*/
val isMember: Boolean
) : Parcelable ) : Parcelable
@@ -16,7 +16,8 @@ object ApiClient {
// private const val DEVICE_BASE_URL = "http://device.shuziweidao.com:8889/" // private const val DEVICE_BASE_URL = "http://device.shuziweidao.com:8889/"
private var DEVICE_BASE_URL = GlobalData.appBaseUrl private var DEVICE_BASE_URL = GlobalData.appBaseUrl
// private const val BASE_URL = "http://192.168.1.8:9092" // private const val BASE_URL = "http://192.168.1.8:9092"
private const val TIME_OUT = 30L // 超时时间(秒) // private const val TIME_OUT = 30L // 超时时间(秒)
private const val TIME_OUT = 60L // 超时时间(秒)
val okHttpClient = OkHttpClient.Builder() val okHttpClient = OkHttpClient.Builder()
.connectTimeout(TIME_OUT, TimeUnit.SECONDS) .connectTimeout(TIME_OUT, TimeUnit.SECONDS)
@@ -11,12 +11,15 @@ import com.sw.platecabinet.model.response.EquipmentInfo
import com.sw.platecabinet.model.response.EquipmentUserInfo import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.SearchResult import com.sw.platecabinet.model.response.SearchResult
import com.sw.platecabinet.model.response.UserFaceModel import com.sw.platecabinet.model.response.UserFaceModel
import okhttp3.MultipartBody
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.Field import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Header import retrofit2.http.Header
import retrofit2.http.Multipart
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.Query import retrofit2.http.Query
import retrofit2.http.Url import retrofit2.http.Url
@@ -75,7 +78,7 @@ interface ApiService {
// @Body param: LoginParam // @Body param: LoginParam
// ): ApiResponse<EquipmentUserInfo> // ): ApiResponse<EquipmentUserInfo>
/** /**
* 餐盘用户信息获取 * 餐盘用户信息获取
*/ */
@POST//("/shuwei-zhct/swEquipmentRelUser/equipmentBoxLogin") @POST//("/shuwei-zhct/swEquipmentRelUser/equipmentBoxLogin")
@@ -145,7 +148,7 @@ interface ApiService {
// ): ApiResponse<EquipmentUserInfo> // ): ApiResponse<EquipmentUserInfo>
/** /**
* 通过餐盘号获取信息 * 通过餐盘号获取信息
*/ */
@GET @GET
@@ -155,8 +158,6 @@ interface ApiService {
): ApiResponse<EquipmentUserInfo> ): ApiResponse<EquipmentUserInfo>
/** /**
* 获取人脸增量数据 * 获取人脸增量数据
* *
@@ -173,8 +174,8 @@ interface ApiService {
*/ */
@POST @POST
suspend fun getFaceIncrementList( suspend fun getFaceIncrementList(
@Url url:String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/faceFeature/increment/list", @Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/faceFeature/increment/list",
@Body param:Map<String, Long> @Body param: Map<String, Long>
): ApiResponse<List<UserFaceModel>?> ): ApiResponse<List<UserFaceModel>?>
/** /**
@@ -185,5 +186,22 @@ interface ApiService {
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/getYxEquipmentByEquipmentCode" @Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/getYxEquipmentByEquipmentCode"
): ApiResponse<DeviceConfig?> ): ApiResponse<DeviceConfig?>
/**
* 添加人脸数据
*/
@POST
@FormUrlEncoded
suspend fun addUserFace(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/addUserByFace",
@Field("featureChar") faceData:String,
@Field("url") imageUr:String
): ApiResponse<UserFaceModel?>
@Multipart
@POST
suspend fun uploadImage(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/upload",
@Part file: MultipartBody.Part,
): ApiResponse<String?>
} }
@@ -1,17 +1,14 @@
package com.sw.platecabinet.repository package com.sw.platecabinet.repository
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.model.DeviceConfig import com.sw.platecabinet.model.DeviceConfig
import com.sw.platecabinet.model.request.BindParam
import com.sw.platecabinet.model.request.EquipmentParam
import com.sw.platecabinet.model.request.LoginParam
import com.sw.platecabinet.model.request.SearchParam import com.sw.platecabinet.model.request.SearchParam
import com.sw.platecabinet.model.response.ApiResponse import com.sw.platecabinet.model.response.ApiResponse
import com.sw.platecabinet.model.response.EquipmentInfo
import com.sw.platecabinet.model.response.EquipmentUserInfo import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.SearchResult import com.sw.platecabinet.model.response.SearchResult
import com.sw.platecabinet.model.response.UserFaceModel import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.network.api.ApiService import com.sw.platecabinet.network.api.ApiService
import com.sw.platecabinet.utils.FileUtils
import java.io.File
/** /**
* 远程数据处理 * 远程数据处理
@@ -73,19 +70,19 @@ class RemoteRepository constructor(
} }
} }
/** // /**
* 登录 // * 登录
*/ // */
suspend fun equipmentBoxLogin(param: LoginParam): ApiResponse<EquipmentUserInfo> { // suspend fun equipmentBoxLogin(param: LoginParam): ApiResponse<EquipmentUserInfo> {
return safeApiCall { apiService.equipmentBoxLogin(param = param) } // return safeApiCall { apiService.equipmentBoxLogin(param = param) }
} // }
/** // /**
* 获取设备绑定用户列表 // * 获取设备绑定用户列表
*/ // */
suspend fun getEquipmentList(): ApiResponse<List<EquipmentUserInfo>> { // suspend fun getEquipmentList(): ApiResponse<List<EquipmentUserInfo>> {
return safeApiCall { apiService.getEquipmentList() } // return safeApiCall { apiService.getEquipmentList() }
} // }
// /** // /**
// * 餐盘用户信息绑定解绑 // * 餐盘用户信息绑定解绑
@@ -94,13 +91,13 @@ class RemoteRepository constructor(
// return safeApiCall { apiService.bindEquipment(param = param) } // return safeApiCall { apiService.bindEquipment(param = param) }
// } // }
suspend fun plateBind(param: BindParam): ApiResponse<EquipmentUserInfo?> { // suspend fun plateBind(param: BindParam): ApiResponse<EquipmentUserInfo?> {
return safeApiCall { apiService.plateBind(param = param) } // return safeApiCall { apiService.plateBind(param = param) }
} // }
suspend fun plateUnbind(id: Long?): ApiResponse<Any?> { // suspend fun plateUnbind(id: Long?): ApiResponse<Any?> {
return safeApiCall { apiService.plateUnbind(id = id) } // return safeApiCall { apiService.plateUnbind(id = id) }
} // }
/** /**
* 用户信息模糊搜索 * 用户信息模糊搜索
@@ -116,11 +113,11 @@ class RemoteRepository constructor(
return safeApiCall { apiService.findByPlateNumber(plateNumber = plateNumber) } return safeApiCall { apiService.findByPlateNumber(plateNumber = plateNumber) }
} }
/** // /**
* 获取人脸增量数据 // * 获取人脸增量数据
* // *
* @param timeDate 时间戳(毫秒级) // * @param timeDate 时间戳(毫秒级)
*/ // */
// suspend fun getHeartbeatInterface(timeDate: Long): ApiResponse<List<UserFaceModel>> { // suspend fun getHeartbeatInterface(timeDate: Long): ApiResponse<List<UserFaceModel>> {
// return safeApiCall { apiService.getHeartbeatInterface(timeDate = timeDate) } // return safeApiCall { apiService.getHeartbeatInterface(timeDate = timeDate) }
// } // }
@@ -144,4 +141,19 @@ class RemoteRepository constructor(
suspend fun getDeviceConfig(): ApiResponse<DeviceConfig?> { suspend fun getDeviceConfig(): ApiResponse<DeviceConfig?> {
return safeApiCall { apiService.getDeviceConfig() } return safeApiCall { apiService.getDeviceConfig() }
} }
suspend fun addUserFace(faceData: String, imageUr:String): ApiResponse<UserFaceModel?> {
return safeApiCall { apiService.addUserFace(faceData = faceData, imageUr = imageUr) }
}
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)
}
}
}
} }
@@ -0,0 +1,59 @@
package com.sw.platecabinet.utils
import android.content.Context
import android.graphics.Bitmap
import android.os.Environment
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import kotlin.io.use
object BitmapSaver {
// 保存到公共目录(需WRITE_EXTERNAL_STORAGE权限)
fun saveToPublicDirectory(
bitmap: Bitmap,
folderName: String = Environment.DIRECTORY_PICTURES,
fileName: String,
format: Bitmap.CompressFormat = Bitmap.CompressFormat.PNG,
quality: Int = 100
): File? {
val dir = Environment.getExternalStoragePublicDirectory(folderName)
if (!dir.exists()) dir.mkdirs()
return saveBitmap(bitmap, File(dir, fileName), format, quality)
}
// 保存到应用私有目录(无需权限)
fun saveToAppFilesDir(
bitmap: Bitmap,
context: Context,
fileName: String,
format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
quality: Int = 100
): File? {
//val dir = context.getExternalFilesDir(null)
val dir = context.cacheDir
val cropFile = File(dir, "crop")
if (cropFile.exists().not()) {
cropFile.mkdirs()
}
return saveBitmap(bitmap, File(cropFile, fileName), format, quality)
}
private fun saveBitmap(
bitmap: Bitmap,
outputFile: File,
format: Bitmap.CompressFormat,
quality: Int
): File? {
return try {
FileOutputStream(outputFile).use { fos ->
bitmap.compress(format, quality, fos)
fos.flush()
}
outputFile
} catch (e: IOException) {
e.printStackTrace()
null
}
}
}
@@ -0,0 +1,163 @@
package com.sw.platecabinet.utils
import android.content.ContentUris
import android.content.Context
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import androidx.annotation.RequiresApi
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import timber.log.Timber
import java.io.File
import kotlin.io.copyTo
import kotlin.io.outputStream
import kotlin.io.use
import kotlin.text.equals
object FileUtils {
/**
* 从Uri获取File
* example: file:///data/user/0/com.sw.inbound/cache/IMG_17515262353556856678814444882273.jpg
*/
private fun getFileFromUri(context: Context, uri: Uri): File? {
Timber.d("getFileFromUri uri = ${uri.scheme}")
return when (uri.scheme) {
"file" -> File(uri.path ?: return null)
"content" -> {
try {
val inputStream = context.contentResolver.openInputStream(uri) ?: return null
val cacheDir = context.cacheDir
val file = File.createTempFile(
"upload_${System.currentTimeMillis()}",
".jpg",
cacheDir
)
file.outputStream().use { output ->
inputStream.copyTo(output)
}
file
} catch (e: Exception) {
Timber.e(e)
null
}
}
else -> null
}
}
/**
* 通过uri生成http请求体
*/
fun genRequestPart(context: Context, imageUri: Uri): MultipartBody.Part? {
Timber.d("genRequestPart imageUri = $imageUri")
// 1. 从Uri获取文件
val file = getFileFromUri(context, imageUri)
return genRequestPart(file)
}
fun genRequestPart(file: File?): MultipartBody.Part? {
if (file == null) {
Timber.e("getFileFromUri file is null")
return null
}
// 2. 创建请求体
val requestFile = file
.asRequestBody("application/octet-stream".toMediaTypeOrNull())
val imagePart = MultipartBody.Part.createFormData(
"file",
file.name,
requestFile
)
return imagePart
}
/**
* 通过Uri删除文件
* @param context 上下文
* @param uri 文件Uri
* @return Boolean 是否删除成功
*/
fun deleteFileWithUri(context: Context, uri: Uri): Boolean {
Timber.d("deleteFileWithUri uri = ${uri.scheme}")
return when {
// 1. 处理 content:// 类型的Uri (MediaStore)
uri.scheme.equals("content", ignoreCase = true) -> {
deleteContentUriFile(context, uri)
}
// 2. 处理 file:// 类型的Uri
uri.scheme.equals("file", ignoreCase = true) -> {
deleteFileUriFile(uri)
}
// 3. 其他情况尝试直接解析路径
else -> {
deleteFileFromPath(uri.path ?: return false)
}
}
}
// 删除Content Uri文件
private fun deleteContentUriFile(context: Context, uri: Uri): Boolean {
Timber.d("deleteContentUriFile uri = ${uri.scheme}")
return try {
context.contentResolver.delete(uri, null, null) > 0
} catch (e: SecurityException) {
// Android 10+需要特殊处理
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
deleteMediaStoreFile(context, uri)
} else {
false
}
} catch (e: Exception) {
Timber.e(e)
false
}
}
// Android 10+删除MediaStore文件
@RequiresApi(Build.VERSION_CODES.Q)
private fun deleteMediaStoreFile(context: Context, uri: Uri): Boolean {
Timber.d("deleteMediaStoreFile uri = ${uri.scheme}")
val contentResolver = context.contentResolver
val projection = arrayOf(MediaStore.MediaColumns._ID)
return try {
contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val id =
cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
val contentUri = ContentUris.withAppendedId(uri, id)
contentResolver.delete(contentUri, null, null) > 0
} else {
false
}
} ?: false
} catch (e: Exception) {
Timber.e(e)
false
}
}
// 删除File Uri文件
private fun deleteFileUriFile(uri: Uri): Boolean {
Timber.d("deleteFileUriFile uri = $uri")
return try {
File(uri.path ?: return false).delete()
} catch (e: Exception) {
Timber.e(e)
false
}
}
// 直接通过路径删除文件
private fun deleteFileFromPath(path: String): Boolean {
Timber.d("deleteFileFromPath path = $path")
return try {
File(path).delete()
} catch (e: Exception) {
Timber.e(e)
false
}
}
}
@@ -20,7 +20,7 @@ import java.text.DecimalFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public class FileUtil { public class LogFileUtil {
private static boolean isSaveLog = true; private static boolean isSaveLog = true;
public static final String FILE_STR_PATH = App.getContext().getExternalCacheDir().getAbsolutePath(); public static final String FILE_STR_PATH = App.getContext().getExternalCacheDir().getAbsolutePath();
@@ -624,7 +624,7 @@ public class FileUtil {
.append("\n"); .append("\n");
} }
String fileName = "log" + AppUtil.formatDateGetDay(System.currentTimeMillis()) + ".txt"; String fileName = "log" + AppUtil.formatDateGetDay(System.currentTimeMillis()) + ".txt";
FileUtil.saveStrFile(sb.toString(), fileName, FILE_STR_PATH, true); LogFileUtil.saveStrFile(sb.toString(), fileName, FILE_STR_PATH, true);
} }
}.start(); }.start();
@@ -1,22 +1,14 @@
package com.sw.platecabinet.viewmodel package com.sw.platecabinet.viewmodel
import android.text.TextUtils
import com.sw.inbound.utils.DateTimeUtils
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.GlobalData.globalEquipmentCode import com.sw.platecabinet.GlobalData.globalEquipmentCode
import com.sw.platecabinet.model.ErrorInfo
import com.sw.platecabinet.model.request.BindParam
import com.sw.platecabinet.model.request.EquipmentParam
import com.sw.platecabinet.model.request.SearchParam
import com.sw.platecabinet.model.response.EquipmentUserInfo import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.SearchResult import com.sw.platecabinet.model.response.SearchResult
import com.sw.platecabinet.model.response.UserFaceModel import com.sw.platecabinet.model.response.UserFaceModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import java.io.File
/** /**
* 管理员操作viewmodel * 管理员操作viewmodel
@@ -71,43 +63,43 @@ class SettingViewModel : BaseViewModel() {
// } // }
// } // }
/** // /**
* 搜索会员信息列表 // * 搜索会员信息列表
*/ // */
fun getSearchMemberList(param:String, pageNum: Int = 1, pageSize: Int = 50) { // fun getSearchMemberList(param:String, pageNum: Int = 1, pageSize: Int = 50) {
if (TextUtils.isEmpty(param)) return // if (TextUtils.isEmpty(param)) return
currentPage = pageNum // currentPage = pageNum
var name ="" // var name =""
var phone ="" // var phone =""
val num = param.toIntOrNull() // val num = param.toIntOrNull()
if (num != null) { // if (num != null) {
phone = param // phone = param
} else { // } else {
name = param // name = param
} // }
launch { // launch {
val searchParam = SearchParam( // val searchParam = SearchParam(
// appVersion = GlobalData.appVersion, //// appVersion = GlobalData.appVersion,
name = name, // name = name,
phone = phone, // phone = phone,
pageNum = pageNum, // pageNum = pageNum,
pageSize = pageSize // pageSize = pageSize
) // )
isLoading = true // isLoading = true
val response = repository.searchUser(searchParam) // val response = repository.searchUser(searchParam)
isLoading = false // isLoading = false
if (parseResponse(response)) { // if (parseResponse(response)) {
response.data?.let { list -> // response.data?.let { list ->
if (pageNum == 1) { // if (pageNum == 1) {
_searchMemberList.value = list // _searchMemberList.value = list
} else { // } else {
_searchMemberList.value = _searchMemberList.value.plus(list) // _searchMemberList.value = _searchMemberList.value.plus(list)
} // }
if (list.size >= pageSize) currentPage++ // if (list.size >= pageSize) currentPage++
} // }
} // }
} // }
} // }
// fun bindPlate( // fun bindPlate(
// equipmentBoxCode: String, // equipmentBoxCode: String,
@@ -158,35 +150,35 @@ class SettingViewModel : BaseViewModel() {
// } // }
// } // }
fun unbindPlate(id: Long?, block:(Pair<Boolean?, ErrorInfo>)-> Unit) { // fun unbindPlate(id: Long?, block:(Pair<Boolean?, ErrorInfo>)-> Unit) {
block(null to ErrorInfo()) // block(null to ErrorInfo())
launchWithLoading { // launchWithLoading {
val response = repository.plateUnbind(id) // val response = repository.plateUnbind(id)
if (parseResponse(response)) {
block(false to ErrorInfo())
} else {
block(false to ErrorInfo(response.code, response.msg.toString()))
}
}
}
fun bindPlate(bindParam: BindParam, block:(Pair<Boolean?, ErrorInfo>)-> Unit) {
launchWithLoading {
// val response = repository.plateBind(bindParam)
// if (parseResponse(response)) { // if (parseResponse(response)) {
// var userInfo = response.data // block(false to ErrorInfo())
// if (userInfo == null) {
// userInfo = EquipmentUserInfo()
// userInfo.equipmentBoxCode = bindParam.equipmentBoxCode
// userInfo.plateNumber = bindParam.plateNumber
// userInfo.equipmentCode = bindParam.equipmentCode
// }
// block(true to ErrorInfo(equipmentUserInfo = userInfo))
// } else { // } else {
// block(true to ErrorInfo(response.code, response.msg.toString())) // block(false to ErrorInfo(response.code, response.msg.toString()))
// } // }
} // }
} // }
// fun bindPlate(bindParam: BindParam, block:(Pair<Boolean?, ErrorInfo>)-> Unit) {
// launchWithLoading {
//// val response = repository.plateBind(bindParam)
//// if (parseResponse(response)) {
//// var userInfo = response.data
//// if (userInfo == null) {
//// userInfo = EquipmentUserInfo()
//// userInfo.equipmentBoxCode = bindParam.equipmentBoxCode
//// userInfo.plateNumber = bindParam.plateNumber
//// userInfo.equipmentCode = bindParam.equipmentCode
//// }
//// block(true to ErrorInfo(equipmentUserInfo = userInfo))
//// } else {
//// block(true to ErrorInfo(response.code, response.msg.toString()))
//// }
// }
// }
fun findByPlateNumber( fun findByPlateNumber(
plateNumber: String, plateNumber: String,
@@ -234,4 +226,25 @@ class SettingViewModel : BaseViewModel() {
} }
} }
/**
* 添加人脸数据
*/
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)
}
}
} }
@@ -58,38 +58,45 @@ class UserViewModel : BaseViewModel() {
/** /**
* 获取人脸数据 * 获取人脸数据
*/ */
fun getUserFaceCache(pageNo: Int = 1, pageSize: Int = PAGE_SIZE, refreshFaceBlock: () -> Unit={}) { fun getUserFaceCache(
pageNo: Int = 1,
pageSize: Int = PAGE_SIZE,
callback: (Boolean, String?) -> Unit = { statue, msg -> }
) {
var currentPageNo = pageNo var currentPageNo = pageNo
launchWithLoading { launchWithLoading {
val response = repository.getUserFaceCache(pageNum = pageNo, pageSize = pageSize) val response = repository.getUserFaceCache(pageNum = pageNo, pageSize = pageSize)
if (parseResponse(response)) { val status = parseResponse(response)
// 获取成功一次后缓存状态 if (status.not()) {
SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true) callback(false, response.msg)
val list: List<UserFaceModel> = response.data ?: emptyList() return@launchWithLoading
if (pageNo == 1 && list.isEmpty()) { }
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))
}
withContext(Dispatchers.Default) {
faceApi.updateFaceData(pageNo, faceEntityList)
if (list.size >= pageSize) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0
currentPageNo++
getUserFaceCache(
pageNo = currentPageNo,
callback = callback
)
return@withContext
} }
val faceEntityList = list.map { if (list.isNotEmpty()) {
FaceEntity(it.userId, null, Base64.decode(it.faceFeatureStr)) faceTimestamp = list.last().faceUpdateTimestamp ?: 0
}
withContext(Dispatchers.Default) {
faceApi.updateFaceData(pageNo, faceEntityList)
refreshFaceBlock()
if (list.size >= pageSize) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0
currentPageNo++
getUserFaceCache(
pageNo = currentPageNo,
refreshFaceBlock = refreshFaceBlock
)
return@withContext
}
if (list.isNotEmpty()) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0
}
SpTool.lastFaceTimestamp = faceTimestamp
//activeEngine()
} }
SpTool.lastFaceTimestamp = faceTimestamp
callback(true, "查询完成")
} }
} }
} }
+20 -3
View File
@@ -1,18 +1,35 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item <item
android:width="600dp" android:width="600dp"
android:height="1024dp"> android:height="1024dp">
<bitmap android:src="@drawable/bg"/> <bitmap android:src="@drawable/bg" />
</item> </item>
<item <item
android:width="337dp" android:width="337dp"
android:height="322dp" android:height="322dp"
android:gravity="top|center_horizontal" android:gravity="top|center_horizontal"
android:top="112dp"> android:top="112dp">
<bitmap android:src="@drawable/img_init" android:scaleType="fitCenter"/> <bitmap
android:scaleType="fitCenter"
android:src="@drawable/img_init" />
</item> </item>
<!-- TODO 临时测试 -->
<!-- <item-->
<!-- android:width="828dp"-->
<!-- android:height="1768dp">-->
<!-- <bitmap android:src="@drawable/bg" />-->
<!-- </item>-->
<!-- <item-->
<!-- android:width="337dp"-->
<!-- android:height="322dp"-->
<!-- android:gravity="top|center_horizontal"-->
<!-- android:top="360dp">-->
<!-- <bitmap-->
<!-- android:scaleType="fitCenter"-->
<!-- android:src="@drawable/img_init" />-->
<!-- </item>-->
</layer-list> </layer-list>
@@ -6,14 +6,14 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<ImageView <!-- <ImageView-->
android:layout_width="337dp" <!-- android:layout_width="337dp"-->
android:layout_height="322dp" <!-- android:layout_height="322dp"-->
android:layout_gravity="center_horizontal" <!-- android:layout_gravity="center_horizontal"-->
android:layout_marginTop="112dp" <!-- android:layout_marginTop="112dp"-->
android:src="@drawable/img_init" <!-- android:src="@drawable/img_init"-->
tools:ignore="ContentDescription" <!-- tools:ignore="ContentDescription"-->
android:visibility="invisible"/> <!-- android:visibility="invisible"/>-->
<TextView <TextView
android:id="@+id/btnInit" android:id="@+id/btnInit"
+3 -3
View File
@@ -47,15 +47,15 @@
android:id="@+id/takeButton" android:id="@+id/takeButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="-20dp"> android:layout_marginTop="-35dp">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="60dp" android:layout_height="60dp"
android:text="点击取盘" android:text="点击取盘"
android:textColor="#FFCC99" android:textColor="#FFCC99"
android:textSize="24sp" android:textSize="24sp"
android:layout_marginHorizontal="30dp" android:layout_marginHorizontal="60dp"
android:layout_marginVertical="20dp" android:layout_marginVertical="50dp"
android:paddingHorizontal="45dp" android:paddingHorizontal="45dp"
android:gravity="center" android:gravity="center"
android:background="@drawable/btn_outline" android:background="@drawable/btn_outline"
+75
View File
@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_dialog"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginTop="30dp"
android:src="@drawable/ic_tip_warn_unbind" />
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:textColor="#ffffffff"
android:textSize="28sp"
android:textStyle="bold"
tools:text="即将解除【曹德磊】的餐盘"
android:visibility="gone" />
<TextView
android:id="@+id/tvContent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
tools:text="解绑后该用户将无法使用餐盘柜"
android:textColor="#ff999999"
android:textSize="24sp"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="30dp"
android:background="#ff333333" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="90dp"
android:orientation="horizontal">
<TextView
android:id="@+id/btnCancel"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="取消"
android:textColor="#ff999999"
android:textSize="30sp"
android:textStyle="bold" />
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="#333333" />
<TextView
android:id="@+id/btnConfirm"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="确定"
android:textColor="#ffffcc99"
android:textSize="30sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
@@ -15,6 +15,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:spanCount="2" app:spanCount="2"
tools:itemCount="22"
android:overScrollMode="never" android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
tools:listitem="@layout/item_bind_view"/> tools:listitem="@layout/item_bind_view"/>
@@ -33,10 +33,10 @@ public class FaceApi {
public void updateFaceData(int index, List<FaceEntity> list) { public void updateFaceData(int index, List<FaceEntity> list) {
Log.d(TAG, "updateFaceData: index = " + index + ", listSize = " + list.size()); Log.d(TAG, "updateFaceData: index = " + index + ", listSize = " + list.size());
FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao(); FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao();
if (index == 1) { //if (index == 1) {
faceDao.deleteAll(); // faceDao.deleteAll();
faceDao.resetId(); // faceDao.resetId();
} //}
faceDao.insert(list); faceDao.insert(list);
} }
@@ -102,4 +102,9 @@ public class FaceApi {
return faceDao.queryByUserName(userName); return faceDao.queryByUserName(userName);
} }
public int queryFaceCount() {
FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao();
return faceDao.getFaceCount();
}
} }
@@ -45,10 +45,15 @@ public class FaceEntity implements Parcelable {
@ColumnInfo(name = "register_time") @ColumnInfo(name = "register_time")
private long registerTime; private long registerTime;
/** /**
* 用户类型:1-普通会员、2-临时用户、3-内部员工、或者其它待定类型 * 用户类型:1-普通会员、2-临时用户、或者其它待定类型
*/ */
@ColumnInfo(name = "user_type") @ColumnInfo(name = "user_type")
private String userType; private String userType;
/**
* 会员编号
*/
@ColumnInfo(name = "cardNo")
private String cardNo;
@Ignore @Ignore
private int trackId;//人脸追踪ID private int trackId;//人脸追踪ID
@@ -66,6 +71,8 @@ public class FaceEntity implements Parcelable {
this.imagePath = faceEntity.imagePath; this.imagePath = faceEntity.imagePath;
this.featureData = faceEntity.featureData; this.featureData = faceEntity.featureData;
this.registerTime = faceEntity.registerTime; this.registerTime = faceEntity.registerTime;
this.userType = faceEntity.getUserType();
this.cardNo = faceEntity.getCardNo();
} }
@@ -75,6 +82,8 @@ public class FaceEntity implements Parcelable {
userName = in.readString(); userName = in.readString();
imagePath = in.readString(); imagePath = in.readString();
featureData = in.createByteArray(); featureData = in.createByteArray();
userType = in.readString();
cardNo = in.readString();
} }
public static final Creator<FaceEntity> CREATOR = new Creator<FaceEntity>() { public static final Creator<FaceEntity> CREATOR = new Creator<FaceEntity>() {
@@ -145,6 +154,16 @@ public class FaceEntity implements Parcelable {
this.userType = userType; this.userType = userType;
} }
public String getCardNo() {
return cardNo;
}
public void setCardNo(String cardNo) {
this.cardNo = cardNo;
}
@Override @Override
public int describeContents() { public int describeContents() {
return 0; return 0;
@@ -158,10 +177,9 @@ public class FaceEntity implements Parcelable {
dest.writeString(imagePath); dest.writeString(imagePath);
dest.writeByteArray(featureData); dest.writeByteArray(featureData);
dest.writeString(userType); dest.writeString(userType);
dest.writeString(cardNo);
} }
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) { if (this == o) {
@@ -176,12 +194,13 @@ public class FaceEntity implements Parcelable {
TextUtils.equals(this.userName, that.userName) && TextUtils.equals(this.userName, that.userName) &&
TextUtils.equals(this.imagePath, that.imagePath) && TextUtils.equals(this.imagePath, that.imagePath) &&
Arrays.equals(featureData, that.featureData) && Arrays.equals(featureData, that.featureData) &&
TextUtils.equals(this.userType, that.userType); TextUtils.equals(this.userType, that.userType) &&
TextUtils.equals(this.cardNo, that.cardNo);
} }
@Override @Override
public int hashCode() { public int hashCode() {
int result = Objects.hash(faceId, registerTime, userName, imagePath, userType); int result = Objects.hash(faceId, registerTime, userName, imagePath, userType, cardNo);
result = 31 * result + Arrays.hashCode(featureData); result = 31 * result + Arrays.hashCode(featureData);
return result; return result;
} }