From 9fea23303ca97950772650c98584deee9a7db0bc Mon Sep 17 00:00:00 2001 From: lvmeng <848755140@qq.com> Date: Tue, 3 Feb 2026 14:59:22 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=93=E5=BC=80app=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E6=B8=85=E9=99=A4=E6=9C=AC=E5=9C=B0=E6=95=B0=E6=8D=AE=EF=BC=8C?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E5=8F=91=E9=80=81=E6=95=B0=E6=8D=AE=E5=88=B0?= =?UTF-8?q?=E7=BB=93=E7=AE=97=E7=BB=88=E7=AB=AF=EF=BC=8C=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=BA=BA=E8=84=B8=E9=87=87=E9=9B=86=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=EF=BC=8C=E5=85=A8=E9=87=8F=E5=A2=9E=E9=87=8F=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/sw/platecabinet/MyApp.kt | 5 + .../sw/platecabinet/activity/BaseActivity.kt | 54 ++-- .../activity/DeviceInitActivity.kt | 68 +++-- .../activity/LoginByFaceActivity.kt | 251 ++++++++---------- .../sw/platecabinet/activity/MainActivity.kt | 31 ++- .../sw/platecabinet/dialog/CustomDialog.kt | 48 ++++ .../com/sw/platecabinet/ext/TextViewExt.kt | 39 +++ .../model/response/UserFaceModel.kt | 16 +- .../com/sw/platecabinet/network/ApiClient.kt | 3 +- .../sw/platecabinet/network/api/ApiService.kt | 30 ++- .../repository/RemoteRepository.kt | 68 +++-- .../com/sw/platecabinet/utils/BitmapSaver.kt | 59 ++++ .../com/sw/platecabinet/utils/FileUtils.kt | 163 ++++++++++++ .../utils/{FileUtil.java => LogFileUtil.java} | 4 +- .../viewmodel/SettingViewModel.kt | 157 ++++++----- .../platecabinet/viewmodel/UserViewModel.kt | 61 +++-- app/src/main/res/drawable/bg_splash.xml | 23 +- .../main/res/layout/activity_device_init.xml | 16 +- app/src/main/res/layout/activity_init.xml | 6 +- app/src/main/res/layout/dialog_custom.xml | 75 ++++++ .../main/res/layout/fragment_setting_list.xml | 1 + .../com/sw/plate/utils/arcface/FaceApi.java | 13 +- .../arcface/facedb/entity/FaceEntity.java | 29 +- 23 files changed, 857 insertions(+), 363 deletions(-) create mode 100644 app/src/main/java/com/sw/platecabinet/dialog/CustomDialog.kt create mode 100644 app/src/main/java/com/sw/platecabinet/utils/BitmapSaver.kt create mode 100644 app/src/main/java/com/sw/platecabinet/utils/FileUtils.kt rename app/src/main/java/com/sw/platecabinet/utils/{FileUtil.java => LogFileUtil.java} (99%) create mode 100644 app/src/main/res/layout/dialog_custom.xml diff --git a/app/src/main/java/com/sw/platecabinet/MyApp.kt b/app/src/main/java/com/sw/platecabinet/MyApp.kt index 3215ee0..7ec4ff5 100644 --- a/app/src/main/java/com/sw/platecabinet/MyApp.kt +++ b/app/src/main/java/com/sw/platecabinet/MyApp.kt @@ -34,6 +34,11 @@ class MyApp : App() { Log.d("MyApp", "initialize: 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.globalEquipmentCode = "202501171634" diff --git a/app/src/main/java/com/sw/platecabinet/activity/BaseActivity.kt b/app/src/main/java/com/sw/platecabinet/activity/BaseActivity.kt index 286e662..a984522 100644 --- a/app/src/main/java/com/sw/platecabinet/activity/BaseActivity.kt +++ b/app/src/main/java/com/sw/platecabinet/activity/BaseActivity.kt @@ -1,6 +1,5 @@ package com.sw.platecabinet.activity -import android.app.Dialog import android.content.Context import android.os.Build import android.os.Bundle @@ -15,23 +14,19 @@ import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.view.WindowCompat -import androidx.lifecycle.lifecycleScope import androidx.viewbinding.ViewBinding import com.google.gson.Gson import com.sw.inbound.utils.DateTimeUtils +import com.sw.plate.App import com.sw.plate.utils.Base64 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.entity.FaceEntity 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.R import com.sw.platecabinet.databinding.ItemTitleTimeBinding -import com.sw.platecabinet.dialog.BalanceNotEnoughDialog -import com.sw.platecabinet.ext.setClickListeners +import com.sw.platecabinet.ext.clickWithCoroutines import com.sw.platecabinet.model.response.EquipmentUserInfo import com.sw.platecabinet.model.response.UserFaceModel import com.sw.platecabinet.utils.IntervalExecutor @@ -60,10 +55,10 @@ abstract class BaseActivity : AppCompatActivity() { protected var keyEventHelper: ScanGunKeyEventHelper? = null // 管理员对应的viewmodel - private val settingViewModel by viewModels() + val settingViewModel by viewModels() // 用户对应的viewModel - protected val viewModel by viewModels() + protected val userViewModel by viewModels() private var startTime: Long = 0 @@ -88,8 +83,6 @@ abstract class BaseActivity : AppCompatActivity() { registerKeyEvent() val durationTime = System.currentTimeMillis() - startTime Timber.d("启动时间:$durationTime") - - startFaceTask() } /** @@ -115,7 +108,7 @@ abstract class BaseActivity : AppCompatActivity() { if (scanInfo.length > 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) { initPlateList() } @@ -146,12 +139,12 @@ abstract class BaseActivity : AppCompatActivity() { fun updateTime() { headerBinding?.let { - it.tvLeftDate.setClickListeners( + it.tvLeftDate.clickWithCoroutines( onDoubleClick = { onLeftDoubleClick() } ) - it.tvRightTime.setClickListeners( + it.tvRightTime.clickWithCoroutines( onDoubleClick = { onRightDoubleClick() } @@ -426,7 +419,6 @@ abstract class BaseActivity : AppCompatActivity() { // private val dealyMillis = 10 * 60 * 1000L private val initialDelay = 5 * 60 * 1000L private val dealyMillis = 5 * 60 * 1000L - private var taskPageNo = 1 fun startFaceTask() { faceTaskJob = intervalExecutor.startIntervalTaskWithInitialDelay(initialDelay, dealyMillis) { @@ -435,28 +427,28 @@ abstract class BaseActivity : AppCompatActivity() { } private var faceTimestamp = 0L - - private fun getFaceIncrementList() { + //private var taskPageNo = 1 + private fun getFaceIncrementList(pageNo: Int = 1) { val timestamp = SpTool.lastFaceTimestamp if (timestamp == 0L) { return } settingViewModel.getFaceIncrementList( - pageNo = taskPageNo, + pageNo = pageNo, timestamp = timestamp ) { list -> runOnUiThread { - if (taskPageNo == 1 && list.isEmpty()) { + if (pageNo == 1 && list.isEmpty()) { //未查询到增量数据 return@runOnUiThread } updateFaceData(list) - if (list.size >= settingViewModel.PAGE_SIZE) { - faceTimestamp = list.last().faceUpdateTimestamp ?: 0 - taskPageNo++ - getFaceIncrementList() - return@runOnUiThread - } + //if (list.size >= settingViewModel.PAGE_SIZE) { + // faceTimestamp = list.last().faceUpdateTimestamp ?: 0 + // taskPageNo++ + // getFaceIncrementList(taskPageNo) + // return@runOnUiThread + //} if (list.isNotEmpty()) { faceTimestamp = list.last().faceUpdateTimestamp ?: 0 } @@ -481,7 +473,8 @@ abstract class BaseActivity : AppCompatActivity() { null, Base64.decode(model.faceFeatureStr) ).also { - it.userType = "1" + //1-会员、2-临时用户 + it.userType = if (model.isMember) "1" else "2" } faceList.add(faceEntity) } @@ -512,4 +505,13 @@ abstract class BaseActivity : AppCompatActivity() { MyApp.plateList = list } + fun clearAllFace(block: () -> Unit) { + Thread { + val faceDao = FaceDatabase.getInstance(App.getContext()).faceDao() + faceDao.deleteAll() + faceDao.resetId() + runOnUiThread { block() } + }.start() + } + } \ No newline at end of file diff --git a/app/src/main/java/com/sw/platecabinet/activity/DeviceInitActivity.kt b/app/src/main/java/com/sw/platecabinet/activity/DeviceInitActivity.kt index 5da092c..73a9abd 100644 --- a/app/src/main/java/com/sw/platecabinet/activity/DeviceInitActivity.kt +++ b/app/src/main/java/com/sw/platecabinet/activity/DeviceInitActivity.kt @@ -1,22 +1,15 @@ package com.sw.platecabinet.activity import android.content.Intent -import android.util.Log -import androidx.activity.viewModels -import com.sw.inbound.utils.DateTimeUtils -import com.sw.plate.App -import com.sw.plate.utils.AppUtil +import androidx.lifecycle.lifecycleScope 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.MyApp import com.sw.platecabinet.databinding.ActivityDeviceInitBinding import com.sw.platecabinet.databinding.ItemTitleTimeBinding -import com.sw.platecabinet.ext.gone -import com.sw.platecabinet.ext.invisible -import com.sw.platecabinet.model.response.EquipmentUserInfo -import com.sw.platecabinet.utils.SpTool -import com.sw.platecabinet.viewmodel.SettingViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * 设备初始化界面 @@ -53,37 +46,38 @@ class DeviceInitActivity : BaseActivity() { // }, 1000) initPlateList() showWaitingDialog("加载中……") - clearAllFace { - viewModel.getUserFaceCache() - viewModel.getDeviceConfig { deviceConfig -> - runOnUiThread { - if (deviceConfig == null) { - hideWaitingDialog() - ToastUtils.showToast("获取设备配置数据失败") - hideWaitingDialog() - goLoginActivity() - return@runOnUiThread + lifecycleScope.launch { + withContext(Dispatchers.Default) { + val count = FaceApi().queryFaceCount() + if (count == 0) { + userViewModel.getUserFaceCache { status, msg -> + getDeviceConfig() } - 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) { - Thread { - val faceDao = FaceDatabase.getInstance(App.getContext()).faceDao() - faceDao.deleteAll() - faceDao.resetId() - runOnUiThread { block() } - }.start() + private fun getDeviceConfig() { + userViewModel.getDeviceConfig { deviceConfig -> + runOnUiThread { + if (deviceConfig == null) { + hideWaitingDialog() + ToastUtils.showToast("获取设备配置数据失败") + 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() { diff --git a/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt b/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt index efa27b6..3ca56e5 100644 --- a/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt +++ b/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt @@ -2,6 +2,7 @@ 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.os.Build @@ -14,7 +15,6 @@ import androidx.annotation.RequiresApi import androidx.lifecycle.Observer import com.arcsoft.face.ErrorInfo import com.google.gson.Gson -import com.sw.platecabinet.socket.TcpClientListenerImpl import com.sw.inbound.utils.DateTimeUtils import com.sw.plate.utils.Base64 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.viewmodel.RecognizeViewModel 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.R import com.sw.platecabinet.databinding.ActivityLoginFaceBinding import com.sw.platecabinet.databinding.ItemTitleTimeBinding import com.sw.platecabinet.model.response.EquipmentUserInfo -import com.sw.platecabinet.socket.LanServer -import com.sw.platecabinet.socket.LanServerListenerImpl -import com.sw.platecabinet.socket.TcpClient +import com.sw.platecabinet.model.response.UserFaceModel +import com.sw.platecabinet.utils.BitmapSaver 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.PlateUtils 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.Subscribe import org.greenrobot.eventbus.ThreadMode -import org.json.JSONObject import timber.log.Timber import java.util.concurrent.atomic.AtomicBoolean @@ -98,15 +95,14 @@ class LoginByFaceActivity : BaseActivity(), override fun initialize() { instance = this - viewModel.activeEngine() - addSocketListener() + userViewModel.activeEngine() +// addSocketListener() initCountTime() initArcViewModel() initArcView() openRectInfoDraw = true recognizeViewModel.setDrawRectInfoTextValue(true) //viewModel.generateToken() -// viewModel.activeEngine() // binding.llToPwd.setOnClickListener { // val intent = Intent(this, LoginByPwdActivity::class.java) @@ -141,6 +137,8 @@ class LoginByFaceActivity : BaseActivity(), // } // } + //开启人脸增量数据定时任务 + startFaceTask() val soundMap = hashMapOf() soundMap[COLLECT_SUCCESS] = R.raw.collect_success @@ -193,11 +191,11 @@ class LoginByFaceActivity : BaseActivity(), if (this.userFaceInfo == null && userFaceInfo != null) { recognizeViewModel.updateRegisterStatus(RecognizeViewModel.REGISTER_STATUS_DONE) this.userFaceInfo = userFaceInfo - getFaceDataBlock { faceData -> + getFaceDataBlock { faceData, headBmp -> isCollecting.set(false) isCollectFace = false Thread { - saveFaceInfo(faceData) + sendFaceData(faceData, headBmp) }.start() } return@setOnRegisterFinishedCallback @@ -476,8 +474,6 @@ class LoginByFaceActivity : BaseActivity(), recognizeViewModel.destroy() EventBus.getDefault().unregister(this) -// getFaceScheduler?.shutdown() -// checkFaceScheduler?.shutdown() super.onDestroy() } @@ -705,7 +701,7 @@ class LoginByFaceActivity : BaseActivity(), collectCount = 0 tempUserId = "${System.currentTimeMillis()}" resumeCamera() - viewModel.resetUserInfo() + userViewModel.resetUserInfo() countDownTimer?.let { it.cancel() it.start() @@ -754,6 +750,9 @@ class LoginByFaceActivity : BaseActivity(), startActivity(intent) } + /** + * 采集人脸 + */ private fun collectFace() { loadLogInfo("collectFace: -----------1,开始采集人脸,开始时间:${DateTimeUtils.getDateTimeString()}----------") if (isFirstOpen) return @@ -765,25 +764,43 @@ class LoginByFaceActivity : BaseActivity(), loadLogInfo("collectFace: ----------5,人脸采集中-----------") } - private fun sendFaceData(faceData: String) { + /** + * 改为上传人脸数据,不需要发生人脸数据到收银端 + */ + private fun sendFaceData(faceData: String, headBmp: Bitmap) { loadLogInfo("collectFace,sendFaceData: ----------11,发送人脸数据-----------") - // 给某个客户端发送 - val jsonObject = JSONObject().also { - it.put("type", "faceFeature") - it.put("content", faceData) + + val imageFile = BitmapSaver.saveToAppFilesDir( + headBmp, this, "IMG_CROP_${System.currentTimeMillis()}.jpg" + ) + 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 var checkFaceScheduler: ScheduledExecutorService? = null -// -// private fun initFaceScheduler() { -//// getFaceScheduler = Executors.newScheduledThreadPool(1) -// checkFaceScheduler = Executors.newScheduledThreadPool(1) -// } - - private fun getFaceDataBlock(block: (String) -> Unit) { + private fun getFaceDataBlock(block: (String, Bitmap) -> Unit) { if (userFaceInfo == null || userFaceInfo?.faceFeature?.featureData == null) { return } @@ -792,7 +809,7 @@ class LoginByFaceActivity : BaseActivity(), return } loadLogInfo("collectFace,getFaceData: ----------7,拿到人脸提取特征数据时间:${DateTimeUtils.getDateTimeString()}-----------") - block(faceData) + block(faceData, userFaceInfo!!.headBmp) } /** @@ -801,7 +818,7 @@ class LoginByFaceActivity : BaseActivity(), */ private fun loadLogInfo(msg: String) { Timber.tag(TAG).d(msg) - FileUtil.saveLog(msg) + LogFileUtil.saveLog(msg) } private var facePreviewInfoList: MutableList? = null @@ -809,23 +826,17 @@ class LoginByFaceActivity : BaseActivity(), private var currentUserId: String? = null - private fun saveFaceInfo(faceData: String) { + private fun saveFaceInfo(faceModel: UserFaceModel) { try { - sendFaceData(faceData) - loadLogInfo("collectFace,saveFaceInfo: ----------12,人脸数据发送完成时间:${DateTimeUtils.getDateTimeString()}-----------") - -// runOnUiThread { -// openPlate(tempUserId) -// } - loadLogInfo("collectFace,saveFaceInfo: ----------8,保存人脸取特征数据-----------") val faceDao = FaceDatabase.getInstance(this).faceDao() val faceEntity = FaceEntity( - tempUserId, + faceModel.userId, null, - Base64.decode(faceData) + Base64.decode(faceModel.faceFeatureStr) ).also { - it.userType = "2" + //1-会员、2-临时用户 + it.userType = if (faceModel.isMember) "1" else "2" } faceDao.insert(faceEntity) loadLogInfo("collectFace,saveFaceInfo: ----------10,人脸数据保存完成-----------") @@ -843,70 +854,70 @@ class LoginByFaceActivity : BaseActivity(), } } - private var collectCount = 0; + private var collectCount = 0 - private var tcpClient: TcpClient? = null - - private fun addSocketListener() { -// val ipAddress = NetworkUtils.getIPAddress(true) -// loadLogInfo( "addSocketListener,ipAddress: $ipAddress"); - tcpClient = TcpClient( - "192.168.1.95", - 5000, - GlobalData.deviceId, // clientId - 5000, // connectTimeoutMs - 10 * 1000, // heartbeatIntervalMs - 30 * 1000 // heartbeatTimeoutMs - ) - - tcpClient?.setListener(object : TcpClientListenerImpl() { - override fun onSendSuccess(json: JSONObject?) { - super.onSendSuccess(json) - loadLogInfo("collectFace,TcpClientListener发送成功:$json") -// toast("发送成功") - } - - override fun onSendFailed(json: JSONObject?, e: java.lang.Exception?) { - super.onSendFailed(json, e) - ToastUtils.showToast("发送失败${e?.message}") - loadLogInfo("collectFace,TcpClientListener人脸数据发送失败:${e?.message}") - } - - override fun onMessage(json: JSONObject?) { - super.onMessage(json) - loadLogInfo("collectFace,TcpClientListener收到消息:$json") - val type = json?.getInt("type") - // 根据服务端消息类型处理删除新增临时用户人脸数据--------------------- - when (type) { - LanServer.TYPE_ADD_FACE_DATA -> { - //采集设备id自己生成,不接收服务端的,在本地只判断是否存在同一人脸数据 - loadLogInfo("collectFace,TcpClientListener收到消息,新增用户:采集设备id自己生成,不接收服务端的,在本地只判断是否存在同一人脸数据") - } - - LanServer.TYPE_CLEAR_FACE_DATA -> { - clearFaceData() - } - - else -> {} - } - } - }) - tcpClient?.start() - - try { - LanServer.getInstance().let { - it.setListener(object : LanServerListenerImpl() { - override fun onMessageReceived(clientId: String?, message: JSONObject?) { - super.onMessageReceived(clientId, message) - ToastUtils.showToast("收到消息clientId=$clientId") - } - }) - it.start() - } - } catch (e: Exception) { - e.printStackTrace() - } - } +// private var tcpClient: TcpClient? = null +// +// private fun addSocketListener() { +//// val ipAddress = NetworkUtils.getIPAddress(true) +//// loadLogInfo( "addSocketListener,ipAddress: $ipAddress"); +// tcpClient = TcpClient( +// "192.168.1.95", +// 5000, +// GlobalData.deviceId, // clientId +// 5000, // connectTimeoutMs +// 10 * 1000, // heartbeatIntervalMs +// 30 * 1000 // heartbeatTimeoutMs +// ) +// +// tcpClient?.setListener(object : TcpClientListenerImpl() { +// override fun onSendSuccess(json: JSONObject?) { +// super.onSendSuccess(json) +// loadLogInfo("collectFace,TcpClientListener发送成功:$json") +//// toast("发送成功") +// } +// +// override fun onSendFailed(json: JSONObject?, e: java.lang.Exception?) { +// super.onSendFailed(json, e) +// ToastUtils.showToast("发送失败${e?.message}") +// loadLogInfo("collectFace,TcpClientListener人脸数据发送失败:${e?.message}") +// } +// +// override fun onMessage(json: JSONObject?) { +// super.onMessage(json) +// loadLogInfo("collectFace,TcpClientListener收到消息:$json") +// val type = json?.getInt("type") +// // 根据服务端消息类型处理删除新增临时用户人脸数据--------------------- +// when (type) { +// LanServer.TYPE_ADD_FACE_DATA -> { +// //采集设备id自己生成,不接收服务端的,在本地只判断是否存在同一人脸数据 +// loadLogInfo("collectFace,TcpClientListener收到消息,新增用户:采集设备id自己生成,不接收服务端的,在本地只判断是否存在同一人脸数据") +// } +// +// LanServer.TYPE_CLEAR_FACE_DATA -> { +// clearFaceData() +// } +// +// else -> {} +// } +// } +// }) +// tcpClient?.start() +// +// try { +// LanServer.getInstance().let { +// it.setListener(object : LanServerListenerImpl() { +// override fun onMessageReceived(clientId: String?, message: JSONObject?) { +// super.onMessageReceived(clientId, message) +// ToastUtils.showToast("收到消息clientId=$clientId") +// } +// }) +// it.start() +// } +// } catch (e: Exception) { +// e.printStackTrace() +// } +// } private fun clearFaceData() { Thread { @@ -917,34 +928,6 @@ class LoginByFaceActivity : BaseActivity(), }.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 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() -//// } -// } } \ No newline at end of file diff --git a/app/src/main/java/com/sw/platecabinet/activity/MainActivity.kt b/app/src/main/java/com/sw/platecabinet/activity/MainActivity.kt index 441c742..3dbabdc 100644 --- a/app/src/main/java/com/sw/platecabinet/activity/MainActivity.kt +++ b/app/src/main/java/com/sw/platecabinet/activity/MainActivity.kt @@ -2,14 +2,14 @@ package com.sw.platecabinet.activity import android.content.Context import android.content.Intent -import com.sw.platecabinet.MyApp +import com.sw.plate.utils.ToastUtils import com.sw.platecabinet.R import com.sw.platecabinet.databinding.ActivityMainBinding import com.sw.platecabinet.databinding.ItemTitleTimeBinding +import com.sw.platecabinet.dialog.CustomDialog import com.sw.platecabinet.fragment.PlateCabinetFullFragment import com.sw.platecabinet.fragment.PlateOpenFragment import com.sw.platecabinet.fragment.SettingListFragment -import com.sw.platecabinet.model.response.EquipmentUserInfo import com.sw.platecabinet.utils.FragmentHelper typealias Callback = (String) -> Unit @@ -49,7 +49,7 @@ class MainActivity : BaseActivity() { // 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) intent.putExtra(PARAM_PAGE_TYPE, pageType.name) intent.putExtra(PlateOpenFragment.IS_SCAN_CODE, isScanCode) @@ -85,7 +85,9 @@ class MainActivity : BaseActivity() { } override fun onRightDoubleClick() { - + if (pageType == PageType.SETTING_LIST) { + loadClearDialog() + } } override fun handleScanKeyInfo(scanInfo: String) { @@ -101,8 +103,27 @@ class MainActivity : BaseActivity() { override fun 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() + } +} /** * 界面类型 diff --git a/app/src/main/java/com/sw/platecabinet/dialog/CustomDialog.kt b/app/src/main/java/com/sw/platecabinet/dialog/CustomDialog.kt new file mode 100644 index 0000000..522ef48 --- /dev/null +++ b/app/src/main/java/com/sw/platecabinet/dialog/CustomDialog.kt @@ -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() + } + } + +} diff --git a/app/src/main/java/com/sw/platecabinet/ext/TextViewExt.kt b/app/src/main/java/com/sw/platecabinet/ext/TextViewExt.kt index 7a9f4a0..451199a 100644 --- a/app/src/main/java/com/sw/platecabinet/ext/TextViewExt.kt +++ b/app/src/main/java/com/sw/platecabinet/ext/TextViewExt.kt @@ -4,6 +4,11 @@ import android.os.Handler import android.os.Looper import android.view.View 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 的双击和单击事件监听器 @@ -30,4 +35,38 @@ fun TextView.setClickListeners( clickCount = 0 // 重置计数 }, 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 + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/sw/platecabinet/model/response/UserFaceModel.kt b/app/src/main/java/com/sw/platecabinet/model/response/UserFaceModel.kt index 2ed0dd4..136a35c 100644 --- a/app/src/main/java/com/sw/platecabinet/model/response/UserFaceModel.kt +++ b/app/src/main/java/com/sw/platecabinet/model/response/UserFaceModel.kt @@ -25,6 +25,18 @@ data class UserFaceModel( val userId: String? = "", val faceFeatureStr: String? = "", - val faceUpdateTimestamp:Long?=null, - val faceDeleted: Boolean?=false + val faceUpdateTimestamp: Long? = null, + /** + * 人脸删除标识 + */ + val faceDeleted: Boolean? = false, + /** + * 会员编号 + */ + val cardNo: String, + + /** + * 是否会员 + */ + val isMember: Boolean ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/sw/platecabinet/network/ApiClient.kt b/app/src/main/java/com/sw/platecabinet/network/ApiClient.kt index 9efd866..73c75d5 100644 --- a/app/src/main/java/com/sw/platecabinet/network/ApiClient.kt +++ b/app/src/main/java/com/sw/platecabinet/network/ApiClient.kt @@ -16,7 +16,8 @@ object ApiClient { // private const val DEVICE_BASE_URL = "http://device.shuziweidao.com:8889/" private var DEVICE_BASE_URL = GlobalData.appBaseUrl // private const val BASE_URL = "http://192.168.1.8:9092" - private const val TIME_OUT = 30L // 超时时间(秒) +// private const val TIME_OUT = 30L // 超时时间(秒) + private const val TIME_OUT = 60L // 超时时间(秒) val okHttpClient = OkHttpClient.Builder() .connectTimeout(TIME_OUT, TimeUnit.SECONDS) diff --git a/app/src/main/java/com/sw/platecabinet/network/api/ApiService.kt b/app/src/main/java/com/sw/platecabinet/network/api/ApiService.kt index 453dccb..c1e1652 100644 --- a/app/src/main/java/com/sw/platecabinet/network/api/ApiService.kt +++ b/app/src/main/java/com/sw/platecabinet/network/api/ApiService.kt @@ -11,12 +11,15 @@ import com.sw.platecabinet.model.response.EquipmentInfo import com.sw.platecabinet.model.response.EquipmentUserInfo import com.sw.platecabinet.model.response.SearchResult import com.sw.platecabinet.model.response.UserFaceModel +import okhttp3.MultipartBody import retrofit2.http.Body import retrofit2.http.Field import retrofit2.http.FormUrlEncoded import retrofit2.http.GET import retrofit2.http.Header +import retrofit2.http.Multipart import retrofit2.http.POST +import retrofit2.http.Part import retrofit2.http.Query import retrofit2.http.Url @@ -75,7 +78,7 @@ interface ApiService { // @Body param: LoginParam // ): ApiResponse - /** + /** * 餐盘用户信息获取 */ @POST//("/shuwei-zhct/swEquipmentRelUser/equipmentBoxLogin") @@ -145,7 +148,7 @@ interface ApiService { // ): ApiResponse - /** + /** * 通过餐盘号获取信息 */ @GET @@ -155,8 +158,6 @@ interface ApiService { ): ApiResponse - - /** * 获取人脸增量数据 * @@ -173,8 +174,8 @@ interface ApiService { */ @POST suspend fun getFaceIncrementList( - @Url url:String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/faceFeature/increment/list", - @Body param:Map + @Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/faceFeature/increment/list", + @Body param: Map ): ApiResponse?> /** @@ -185,5 +186,22 @@ interface ApiService { @Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/getYxEquipmentByEquipmentCode" ): ApiResponse + /** + * 添加人脸数据 + */ + @POST + @FormUrlEncoded + suspend fun addUserFace( + @Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/addUserByFace", + @Field("featureChar") faceData:String, + @Field("url") imageUr:String + ): ApiResponse + + @Multipart + @POST + suspend fun uploadImage( + @Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/upload", + @Part file: MultipartBody.Part, + ): ApiResponse } \ No newline at end of file diff --git a/app/src/main/java/com/sw/platecabinet/repository/RemoteRepository.kt b/app/src/main/java/com/sw/platecabinet/repository/RemoteRepository.kt index cd99bf5..d09dfc0 100644 --- a/app/src/main/java/com/sw/platecabinet/repository/RemoteRepository.kt +++ b/app/src/main/java/com/sw/platecabinet/repository/RemoteRepository.kt @@ -1,17 +1,14 @@ package com.sw.platecabinet.repository -import com.sw.platecabinet.GlobalData 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.response.ApiResponse -import com.sw.platecabinet.model.response.EquipmentInfo import com.sw.platecabinet.model.response.EquipmentUserInfo import com.sw.platecabinet.model.response.SearchResult import com.sw.platecabinet.model.response.UserFaceModel 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 { - return safeApiCall { apiService.equipmentBoxLogin(param = param) } - } +// /** +// * 登录 +// */ +// suspend fun equipmentBoxLogin(param: LoginParam): ApiResponse { +// return safeApiCall { apiService.equipmentBoxLogin(param = param) } +// } - /** - * 获取设备绑定用户列表 - */ - suspend fun getEquipmentList(): ApiResponse> { - return safeApiCall { apiService.getEquipmentList() } - } +// /** +// * 获取设备绑定用户列表 +// */ +// suspend fun getEquipmentList(): ApiResponse> { +// return safeApiCall { apiService.getEquipmentList() } +// } // /** // * 餐盘用户信息绑定解绑 @@ -94,13 +91,13 @@ class RemoteRepository constructor( // return safeApiCall { apiService.bindEquipment(param = param) } // } - suspend fun plateBind(param: BindParam): ApiResponse { - return safeApiCall { apiService.plateBind(param = param) } - } +// suspend fun plateBind(param: BindParam): ApiResponse { +// return safeApiCall { apiService.plateBind(param = param) } +// } - suspend fun plateUnbind(id: Long?): ApiResponse { - return safeApiCall { apiService.plateUnbind(id = id) } - } +// suspend fun plateUnbind(id: Long?): ApiResponse { +// return safeApiCall { apiService.plateUnbind(id = id) } +// } /** * 用户信息模糊搜索 @@ -116,11 +113,11 @@ class RemoteRepository constructor( return safeApiCall { apiService.findByPlateNumber(plateNumber = plateNumber) } } - /** - * 获取人脸增量数据 - * - * @param timeDate 时间戳(毫秒级) - */ +// /** +// * 获取人脸增量数据 +// * +// * @param timeDate 时间戳(毫秒级) +// */ // suspend fun getHeartbeatInterface(timeDate: Long): ApiResponse> { // return safeApiCall { apiService.getHeartbeatInterface(timeDate = timeDate) } // } @@ -144,4 +141,19 @@ class RemoteRepository constructor( suspend fun getDeviceConfig(): ApiResponse { return safeApiCall { apiService.getDeviceConfig() } } + + suspend fun addUserFace(faceData: String, imageUr:String): ApiResponse { + return safeApiCall { apiService.addUserFace(faceData = faceData, imageUr = imageUr) } + } + + suspend fun uploadImage(file: File): ApiResponse { + return safeApiCall { + val part = FileUtils.genRequestPart(file) + if (part == null) { + ApiResponse(code = "-1", msg = "解析图片失败") + } else { + apiService.uploadImage(file = part) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/sw/platecabinet/utils/BitmapSaver.kt b/app/src/main/java/com/sw/platecabinet/utils/BitmapSaver.kt new file mode 100644 index 0000000..4fd886b --- /dev/null +++ b/app/src/main/java/com/sw/platecabinet/utils/BitmapSaver.kt @@ -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 + } + } +} diff --git a/app/src/main/java/com/sw/platecabinet/utils/FileUtils.kt b/app/src/main/java/com/sw/platecabinet/utils/FileUtils.kt new file mode 100644 index 0000000..51158c5 --- /dev/null +++ b/app/src/main/java/com/sw/platecabinet/utils/FileUtils.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/platecabinet/utils/FileUtil.java b/app/src/main/java/com/sw/platecabinet/utils/LogFileUtil.java similarity index 99% rename from app/src/main/java/com/sw/platecabinet/utils/FileUtil.java rename to app/src/main/java/com/sw/platecabinet/utils/LogFileUtil.java index fee6eb1..9493c6d 100644 --- a/app/src/main/java/com/sw/platecabinet/utils/FileUtil.java +++ b/app/src/main/java/com/sw/platecabinet/utils/LogFileUtil.java @@ -20,7 +20,7 @@ import java.text.DecimalFormat; import java.util.ArrayList; import java.util.List; -public class FileUtil { +public class LogFileUtil { private static boolean isSaveLog = true; public static final String FILE_STR_PATH = App.getContext().getExternalCacheDir().getAbsolutePath(); @@ -624,7 +624,7 @@ public class FileUtil { .append("\n"); } 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(); diff --git a/app/src/main/java/com/sw/platecabinet/viewmodel/SettingViewModel.kt b/app/src/main/java/com/sw/platecabinet/viewmodel/SettingViewModel.kt index f9c4b20..c6fdf68 100644 --- a/app/src/main/java/com/sw/platecabinet/viewmodel/SettingViewModel.kt +++ b/app/src/main/java/com/sw/platecabinet/viewmodel/SettingViewModel.kt @@ -1,22 +1,14 @@ 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.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.SearchResult import com.sw.platecabinet.model.response.UserFaceModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import timber.log.Timber +import java.io.File /** * 管理员操作viewmodel @@ -71,43 +63,43 @@ class SettingViewModel : BaseViewModel() { // } // } - /** - * 搜索会员信息列表 - */ - fun getSearchMemberList(param:String, pageNum: Int = 1, pageSize: Int = 50) { - if (TextUtils.isEmpty(param)) return - currentPage = pageNum - var name ="" - var phone ="" - val num = param.toIntOrNull() - if (num != null) { - phone = param - } else { - name = param - } - launch { - val searchParam = SearchParam( -// appVersion = GlobalData.appVersion, - name = name, - phone = phone, - pageNum = pageNum, - pageSize = pageSize - ) - isLoading = true - val response = repository.searchUser(searchParam) - isLoading = false - if (parseResponse(response)) { - response.data?.let { list -> - if (pageNum == 1) { - _searchMemberList.value = list - } else { - _searchMemberList.value = _searchMemberList.value.plus(list) - } - if (list.size >= pageSize) currentPage++ - } - } - } - } +// /** +// * 搜索会员信息列表 +// */ +// fun getSearchMemberList(param:String, pageNum: Int = 1, pageSize: Int = 50) { +// if (TextUtils.isEmpty(param)) return +// currentPage = pageNum +// var name ="" +// var phone ="" +// val num = param.toIntOrNull() +// if (num != null) { +// phone = param +// } else { +// name = param +// } +// launch { +// val searchParam = SearchParam( +//// appVersion = GlobalData.appVersion, +// name = name, +// phone = phone, +// pageNum = pageNum, +// pageSize = pageSize +// ) +// isLoading = true +// val response = repository.searchUser(searchParam) +// isLoading = false +// if (parseResponse(response)) { +// response.data?.let { list -> +// if (pageNum == 1) { +// _searchMemberList.value = list +// } else { +// _searchMemberList.value = _searchMemberList.value.plus(list) +// } +// if (list.size >= pageSize) currentPage++ +// } +// } +// } +// } // fun bindPlate( // equipmentBoxCode: String, @@ -158,35 +150,35 @@ class SettingViewModel : BaseViewModel() { // } // } - fun unbindPlate(id: Long?, block:(Pair)-> Unit) { - block(null to ErrorInfo()) - launchWithLoading { - 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)-> Unit) { - launchWithLoading { -// val response = repository.plateBind(bindParam) +// fun unbindPlate(id: Long?, block:(Pair)-> Unit) { +// block(null to ErrorInfo()) +// launchWithLoading { +// val response = repository.plateUnbind(id) // 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)) +// block(false to ErrorInfo()) // } 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)-> 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( 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) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/sw/platecabinet/viewmodel/UserViewModel.kt b/app/src/main/java/com/sw/platecabinet/viewmodel/UserViewModel.kt index c443fb9..d965fa1 100644 --- a/app/src/main/java/com/sw/platecabinet/viewmodel/UserViewModel.kt +++ b/app/src/main/java/com/sw/platecabinet/viewmodel/UserViewModel.kt @@ -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 launchWithLoading { val response = repository.getUserFaceCache(pageNum = pageNo, pageSize = pageSize) - if (parseResponse(response)) { - // 获取成功一次后缓存状态 - SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true) - val list: List = response.data ?: emptyList() - if (pageNo == 1 && list.isEmpty()) { - return@launchWithLoading + val status = parseResponse(response) + if (status.not()) { + callback(false, response.msg) + return@launchWithLoading + } + // 获取成功一次后缓存状态 + SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true) + val list: List = 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 { - FaceEntity(it.userId, null, Base64.decode(it.faceFeatureStr)) - } - 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() + if (list.isNotEmpty()) { + faceTimestamp = list.last().faceUpdateTimestamp ?: 0 } + SpTool.lastFaceTimestamp = faceTimestamp + callback(true, "查询完成") } } } diff --git a/app/src/main/res/drawable/bg_splash.xml b/app/src/main/res/drawable/bg_splash.xml index df638a7..dc25727 100644 --- a/app/src/main/res/drawable/bg_splash.xml +++ b/app/src/main/res/drawable/bg_splash.xml @@ -1,18 +1,35 @@ - - + - + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_device_init.xml b/app/src/main/res/layout/activity_device_init.xml index 8499b19..01bdede 100644 --- a/app/src/main/res/layout/activity_device_init.xml +++ b/app/src/main/res/layout/activity_device_init.xml @@ -6,14 +6,14 @@ android:layout_height="match_parent" android:orientation="vertical"> - + + + + + + + + + android:layout_marginTop="-35dp"> + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_setting_list.xml b/app/src/main/res/layout/fragment_setting_list.xml index 476308a..16abbb5 100644 --- a/app/src/main/res/layout/fragment_setting_list.xml +++ b/app/src/main/res/layout/fragment_setting_list.xml @@ -15,6 +15,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" app:spanCount="2" + tools:itemCount="22" android:overScrollMode="never" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" tools:listitem="@layout/item_bind_view"/> diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceApi.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceApi.java index acd2b22..aa2a2ab 100644 --- a/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceApi.java +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceApi.java @@ -33,10 +33,10 @@ public class FaceApi { public void updateFaceData(int index, List list) { Log.d(TAG, "updateFaceData: index = " + index + ", listSize = " + list.size()); FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao(); - if (index == 1) { - faceDao.deleteAll(); - faceDao.resetId(); - } + //if (index == 1) { + // faceDao.deleteAll(); + // faceDao.resetId(); + //} faceDao.insert(list); } @@ -102,4 +102,9 @@ public class FaceApi { return faceDao.queryByUserName(userName); } + public int queryFaceCount() { + FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao(); + return faceDao.getFaceCount(); + } + } diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/entity/FaceEntity.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/entity/FaceEntity.java index 27c8119..e0e4387 100644 --- a/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/entity/FaceEntity.java +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/entity/FaceEntity.java @@ -45,10 +45,15 @@ public class FaceEntity implements Parcelable { @ColumnInfo(name = "register_time") private long registerTime; /** - * 用户类型:1-普通会员、2-临时用户、3-内部员工、或者其它待定类型 + * 用户类型:1-普通会员、2-临时用户、或者其它待定类型 */ @ColumnInfo(name = "user_type") private String userType; + /** + * 会员编号 + */ + @ColumnInfo(name = "cardNo") + private String cardNo; @Ignore private int trackId;//人脸追踪ID @@ -66,6 +71,8 @@ public class FaceEntity implements Parcelable { this.imagePath = faceEntity.imagePath; this.featureData = faceEntity.featureData; this.registerTime = faceEntity.registerTime; + this.userType = faceEntity.getUserType(); + this.cardNo = faceEntity.getCardNo(); } @@ -75,6 +82,8 @@ public class FaceEntity implements Parcelable { userName = in.readString(); imagePath = in.readString(); featureData = in.createByteArray(); + userType = in.readString(); + cardNo = in.readString(); } public static final Creator CREATOR = new Creator() { @@ -145,6 +154,16 @@ public class FaceEntity implements Parcelable { this.userType = userType; } + public String getCardNo() { + return cardNo; + } + + public void setCardNo(String cardNo) { + this.cardNo = cardNo; + } + + + @Override public int describeContents() { return 0; @@ -158,10 +177,9 @@ public class FaceEntity implements Parcelable { dest.writeString(imagePath); dest.writeByteArray(featureData); dest.writeString(userType); + dest.writeString(cardNo); } - - @Override public boolean equals(Object o) { if (this == o) { @@ -176,12 +194,13 @@ public class FaceEntity implements Parcelable { TextUtils.equals(this.userName, that.userName) && TextUtils.equals(this.imagePath, that.imagePath) && Arrays.equals(featureData, that.featureData) && - TextUtils.equals(this.userType, that.userType); + TextUtils.equals(this.userType, that.userType) && + TextUtils.equals(this.cardNo, that.cardNo); } @Override 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); return result; }