Author SHA1 Message Date
mazengfei 97e5d74c4b 吐盘机: 避免相机关闭后预览画面残留
- 将双击判定窗口时间由300ms延长至600ms,提升触摸屏操作体验
- 为标题时间布局的左右TextView添加最小高度和内边距,优化显示效果
- 相机关闭时清空预览画面,防止TextureView冻结并残留上一用户画面
- 重启相机前先清空残留帧,避免闪现上一用户画面
2026-08-19 17:03:52 +08:00
mazengfei 524837ec20 feat(face): 支持按userFaceId查询单条人脸记录
- FaceDao新增queryByUserFaceId接口,实现按userFaceId精确查询
- FaceApi新增queryByUserFaceId方法,封装Dao查询逻辑
- LoginByFaceActivity插入人脸时,先按userFaceId查询,存在则更新避免重复
- NetViewModelV2同步人脸数据时,优先用userFaceId判重,减少重复入库
- 修正FaceEntity中userId字段数据库列名错误(user_id替代user_idd)
- 增加注释说明及代码格式优化,提升代码可读性和健壮性
2026-08-18 09:12:40 +08:00
mazengfei 747e243122 feat(face): 按 userFaceId 删除单条人脸记录
- 在 FaceDao 中新增按 userFaceId 删除人脸记录的SQL语句
- 在 FaceApi 类中新增 deleteByUserFaceId 方法调用该DAO方法
- 在 NetViewModelV2 中删除人脸时改为按 userFaceId 删除,避免误删所有同用户特征
2026-08-12 14:43:29 +08:00
mazengfei 6be9f4f1cf fix(plc): 统一添加超时兜底机制避免调用无响应挂起
- 在 PlcHelper 中新增 withTimeout 方法,包装所有 SDK 动作和查询接口
- 通过定时器实现超时检测,超时触发 onError 回调,避免永久挂起
- 确保 onSuccess 和 onError 回调至多调用一次,防止重复回调
- 为所有 PLC 操作接口分别设置合理超时时长(3s - 10s不等)
- 修改所有内部调用使用 withTimeout 包装,保证异常场景的错误回调触发
- 更新生产环境基础 URL 地址为平台测试环境地址
2026-08-10 14:02:30 +08:00
mazengfei c9dd294f87 feat(api): 新增取餐盘时刻上报接口及相关调用
- 在 ApiServiceV2 中添加 platePickup 接口用于上报用户开始就餐时刻
- 新增请求体 IdDTO,封装用户 id(餐盘号)字段
- RemoteRepositoryV2 添加对应的 platePickup 方法调用接口
- NetViewModelV2 实现业务方法 platePickup,支持异步回调上报结果
- LoginByFaceActivity 中识别成功出盘后及新用户注册成功后调用 platePickup 上报时刻
- reportPlatePickup 方法完善调用逻辑及日志记录,错误时不会影响主流程
2026-07-10 09:37:41 +08:00
mazengfei 5b29c99b34 refactor(login): 优化出盘流程并增强错误处理
- 抽取出盘成功与失败的统一处理方法,简化代码逻辑
- 新增新用户注册采集成功后立即出盘功能,避免与识别成功逻辑冲突
- 出盘成功后取消识别倒计时并控制出盘开关,防止重复出盘
- 出盘失败时统一提示错误并跳转回初始页
- 采集成功后立即异步出盘,确保上传失败时仍能出盘
- 调整识别失败计数重置逻辑,避免无人脸短暂丢帧导致计数异常
- 在人脸离开时重置识别失败计数,保证下次重新累计
- 网络请求增加异常捕获,防止因异常导致回调阻塞无反馈
2026-07-08 18:30:14 +08:00
mazengfei 9a8ac83d33 feat(face): 新增人脸库展示与增量更新支持
- 在PLC设置界面新增人脸库统计模块,显示人脸总数并支持刷新
- 人脸总数数据从本地数据库异步查询并更新界面
- 修改API接口调用,采用POST请求体方式提交人脸数据(包含url和特征字符串)
- 在网络调用中实现人脸数据的增量分页获取,递归拉取所有数据页
- 维护人脸数据的最大时间戳,确保增量更新只处理最新数据
- 优化人脸数据缓存逻辑,第一页重置数据并同步时间戳
- 添加FeatureBody数据类封装人脸特征请求体参数
- 修正相关仓库层方法以适配新的API接口参数和返回结构
2026-07-07 17:33:04 +08:00
mazengfei cb107c2da2 feat(face): 集成5.0口罩检测引擎并升级数据库版本
- 将FaceDatabase版本升级至2并添加回退破坏性迁移方案
- FaceHelper中新增maskEngine字段与相关处理逻辑,支持口罩检测
- 更新RecognizeViewModel,初始化和管理口罩检测引擎maskEngine
- 在人脸识别流程中使用maskEngine进行口罩检测处理
- 修改ndk配置支持arm64-v8a架构
- 替换项目Gradle和JDK版本为corretto-17
- 优化人脸活体检测代码,移除不再使用的needUpdateFaceData逻辑
- 增加.idea项目设置文件,配置Firebase Crashlytics与设备选择器设置
2026-07-01 16:00:32 +08:00
mazengfei dbb4288217 feat(face): 集成5.0口罩检测引擎并升级数据库版本
- 将FaceDatabase版本升级至2并添加回退破坏性迁移方案
- FaceHelper中新增maskEngine字段与相关处理逻辑,支持口罩检测
- 更新RecognizeViewModel,初始化和管理口罩检测引擎maskEngine
- 在人脸识别流程中使用maskEngine进行口罩检测处理
- 修改ndk配置支持arm64-v8a架构
- 替换项目Gradle和JDK版本为corretto-17
- 优化人脸活体检测代码,移除不再使用的needUpdateFaceData逻辑
- 增加.idea项目设置文件,配置Firebase Crashlytics与设备选择器设置
2026-07-01 15:59:13 +08:00
lvmeng d1e9c4d34f refactor(config): 更新环境配置URL映射关系
- 将TEST_BASE_URL替换为LOCAL_BASE_URL作为本地开发地址
- 将UAT_BASE_URL替换为TEST_BASE_URL作为测试环境地址
- 更新环境切换对话框中的单选按钮映射逻辑
- 修改应用启动时的设备ID判断逻辑和默认环境配置
- 移除部分临时测试代码并调整注释状态
2026-06-30 16:58:11 +08:00
lvmeng 38a00e8d34 refactor(face): 更新FaceEntity实体类并调整引擎激活逻辑
- 为FaceEntity添加默认构造函数并初始化注册时间
- 为FaceEntity的所有构造函数添加@Ignore注解
- 补充FaceEntity的Parcel序列化和反序列化逻辑
- 完善FaceEntity的getter和setter方法实现
- 更新equals和hashCode方法以包含新增字段
- 将人脸引擎激活从userViewModel切换到netViewModelV2
- 添加lint配置文件忽略通知权限检查警告
2026-06-30 10:31:06 +08:00
lvmeng 95b4f5e8f2 feat(network): 添加API V2版本支持并重构网络层
- 新增ApiServiceV2接口定义,支持人脸缓存、增量数据、设备配置等功能
- 添加NetViewModelV2视图模型,实现新版本API的数据处理逻辑
- 创建RemoteRepositoryV2仓库,封装API V2的网络请求操作
- 新增UserFaceModelV2数据模型,优化人脸特征数据结构
- 添加DeviceConfigV2设备配置模型,支持更多设备参数
- 在ApiClient中注册apiServiceV2实例
- 更新BaseActivity使用NetViewModelV2替代UserViewModel
- 修改DeviceInitActivity中的人脸数据获取和设备配置逻辑
- 重构FaceDao数据库访问对象,优化用户类型查询和删除操作
- 更新FaceEntity实体类,增加更多用户相关字段
- 移除BaseViewModel中的旧版网络基础功能代码
- 优化人脸数据同步和存储流程,提升性能表现
2026-06-30 10:08:10 +08:00
mazengfeiandClaude Sonnet 4.6 e8735db27b fix: 优化人脸采集日志及提示界面交互
- LoginByFaceActivity: 新增人脸采集关键节点调试日志
- PlcSettingActivity: 添加点击提示区域关闭提示布局功能,格式化代码
- activity_plc_setting.xml: 给提示 TextView 添加 id,字体调整为 32sp

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 10:41:25 +08:00
lvmeng 66a0c399ec feat(face): 添加人脸识别距离检测和环境切换时的人脸数据重置功能
- 在FaceHelper中添加人脸宽度小于200像素时的过滤逻辑,避免远距离识别人脸
- 注释掉LoginByFaceActivity中的重复识别状态判断逻辑
- 在PlcSettingActivity中添加协程支持用于异步处理人脸数据清除操作
- 实现环境切换时清空所有人脸数据并重新获取用户人脸缓存的功能
- 修改UserViewModel中回调逻辑,当第一页无数据时返回false而非true
2026-04-02 19:11:51 +08:00
lvmeng 4cbe318ab2 feat(plc): 添加环境切换功能
- 在PLC设置页面增加环境切换按钮
- 集成EnvSwitchDialog对话框组件
- 实现切换环境按钮点击事件处理
- 配置环境切换界面布局和样式
- 添加环境相关的UI元素和交互逻辑
2026-03-31 18:12:52 +08:00
lvmengandClaude Sonnet 4.6 1cbad71eaf feat(dialog): 添加环境切换对话框并修复包名引用
- 新增 BaseDialog 抽象基类,统一对话框样式和行为
- 新增 EnvSwitchDialog 支持 TEST/UAT/PROD 三环境切换
- 修复 BaseDialog 和 EnvSwitchDialog 的包名(com.shuwei.intelligent.shelves → com.sw.platecabinet)
- 新增 View.hideKeyboard() 扩展函数到 CommonExt.kt
- 重构 GlobalData 环境 URL 常量命名(TEST_URL/PRO_URL → TEST_BASE_URL/UAT_BASE_URL/PROD_BASE_URL)
- MyApp 启动时从 SpTool 读取已保存的环境配置
- SpTool 新增 baseUrl 持久化支持
- 修正 drawable 资源颜色引用为硬编码值

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 18:12:22 +08:00
mazengfei 0a193ed84e refactor(base): 优化协程作用域和空安全处理
- 将自定义CoroutineScope替换为lifecycleScope以避免内存泄漏
- 添加runCatching包装器处理潜在异常并记录错误日志
- 使用安全调用操作符替代强制调用防止空指针异常
- 在CountDownTimer中正确管理生命周期以避免内存泄漏
- 修复设备信息获取中的数组越界风险
- 统一异步操作的线程切换方式
- 保护Fragment中的_binding访问权限
- 改进相机助手对象引用的安全性检查
2026-03-31 17:12:14 +08:00
mazengfei df9212d2dd 完善吐盘逻辑,调整设置界面,隐藏无关设置 2026-03-31 10:38:45 +08:00
mazengfei cc16fcecff feat(ui): 添加PLC控制设置界面
- 新增PLC设置界面布局文件,包含连接控制、出盘操作、平台控制等功能模块
- 在AndroidManifest.xml中注册PlcSettingActivity组件
- 为初始化和人脸识别登录页面添加右键双击打开PLC设置的功能
- 实现完整的PLC设置活动类,集成串口通信、自动模式、出盘、平台升降等控制功能
- 添加参数设置输入验证和状态信息实时显示功能
- 实现故障复位和状态刷新等关键操作接口
2026-03-30 18:17:49 +08:00
mazengfei 83f9654987 refactor(build): 排除 lib_face 中的 android-serialport 依赖
- 在 lib_face 模块中排除 com.licheedev:android-serialport 依赖
- 防止依赖冲突和重复打包问题
2026-03-30 15:04:41 +08:00
mazengfei 536b1b98b4 feat(login): 集成PLC控制系统实现餐盘柜硬件交互
- 添加plc_aar_104.aar依赖库支持PLC硬件通信
- 新增PlcCallback、PlcHelper和PlcStatus工具类封装PLC操作接口
- 实现PLC状态查询功能替换原有的模拟检测逻辑
- 集成出盘操作流程,支持真实硬件控制
- 优化餐盘检测逻辑,增加PLC通信异常处理机制
- 统一回调处理确保所有操作在主线程执行
2026-03-30 14:06:10 +08:00
lvmeng 7d2bf08023 feat(app): 重构应用为发盘机app,界面布局优化、移除无用代码
- 将应用名称从 SmartPlateCabinet 更改为 SmartTakePlate
- 更新初始化页面背景图片为 bg_collect_page3
- 替换 View 组件为 Space 组件以优化布局权重分配
- 调整取盘按钮尺寸和文字大小提升用户体验
- 简化人脸识别登录页面布局结构
- 更新应用图标资源为 logo_launcher
- 移除未使用的权限声明和活动注册
- 优化人脸识别相关 API 接口调用
- 调整时间显示逻辑仅保留时间部分
- 移除扫描枪事件监听相关功能代码
- 重构人脸识别数据更新逻辑
- 移除余额不足对话框相关代码
2026-03-27 15:48:04 +08:00
mazengfei 86c3c07e42 隐藏测试代码 2026-03-12 11:10:26 +08:00
lvmengandClaude Sonnet 4.6 aa96455f66 refactor(face): 优化人脸特征存储与增量同步逻辑
- FaceDao 新增 queryAllByUserName 查询用户全部特征记录
- 改为查询所有记录后做去重判断,支持同一用户保存多条特征数据
- 将 Thread 替换为 lifecycleScope.launch(Dispatchers.IO) 协程
- 提取 getNewFaceEntity 方法消除重复代码
- 新增特征比对调试日志

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

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 09:07:48 +08:00
99 changed files with 5463 additions and 3869 deletions
Generated
+1 -1
View File
@@ -1 +1 @@
SmartPlateCabinet SmartTakePlate
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="com.codeverse.userSettings.MarscodeWorkspaceAppSettingsState">
<option name="progress" value="1.0" />
</component>
</project>
+27
View File
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AppInsightsSettings">
<option name="selectedTabId" value="Firebase Crashlytics" />
<option name="tabSettings">
<map>
<entry key="Firebase Crashlytics">
<value>
<InsightsFilterSettings>
<option name="connection">
<ConnectionSetting>
<option name="appId" value="PLACEHOLDER" />
<option name="mobileSdkAppId" value="" />
<option name="projectId" value="" />
<option name="projectNumber" value="" />
</ConnectionSetting>
</option>
<option name="signal" value="SIGNAL_UNSPECIFIED" />
<option name="timeIntervalDays" value="THIRTY_DAYS" />
<option name="visibilityType" value="ALL" />
</InsightsFilterSettings>
</value>
</entry>
</map>
</option>
</component>
</project>
+3 -8
View File
@@ -2,16 +2,11 @@
<project version="4"> <project version="4">
<component name="deploymentTargetSelector"> <component name="deploymentTargetSelector">
<selectionStates> <selectionStates>
<SelectionState runConfigName="SmartPlateCabinet.app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-02-05T08:42:12.462597100Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="Default" identifier="serial=192.168.3.114:5555;connection=a41f05a9" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState> </SelectionState>
</selectionStates> </selectionStates>
</component> </component>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>
+1 -1
View File
@@ -6,7 +6,7 @@
<GradleProjectSettings> <GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" /> <option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="azul-17" /> <option name="gradleJvm" value="corretto-17" />
<option name="modules"> <option name="modules">
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />
+6
View File
@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="UsePropertyAccessSyntax" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
</profile>
</component>
+1 -1
View File
@@ -1,6 +1,6 @@
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="zulu-17" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="corretto-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">
+6 -3
View File
@@ -18,7 +18,7 @@ android {
compileSdk = 35 compileSdk = 35
defaultConfig { defaultConfig {
applicationId = "com.sw.platecabinet" applicationId = "com.sw.take.plate"
minSdk = 24 minSdk = 24
targetSdk = 35 targetSdk = 35
versionCode = 1 versionCode = 1
@@ -27,7 +27,7 @@ android {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ndk { ndk {
abiFilters.addAll(listOf("armeabi-v7a"/*, "arm64-v8a"*/)) abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a"))
} }
setProperty("archivesBaseName", "zncpg_${versionName}") setProperty("archivesBaseName", "zncpg_${versionName}")
} }
@@ -59,7 +59,10 @@ dependencies {
implementation(libs.material) implementation(libs.material)
implementation(libs.androidx.activity) implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout) implementation(libs.androidx.constraintlayout)
implementation(project(":lib_face")) implementation(project(":lib_face")) {
exclude(group = "com.licheedev", module = "android-serialport")
}
implementation(files("libs/plc_aar_104.aar"))
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)
Binary file not shown.
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<lint>
<!-- Glide库中的NotificationTarget触发了此检查,但本kiosk应用不使用通知功能 -->
<issue id="NotificationPermission" severity="ignore" />
</lint>
+10 -12
View File
@@ -10,7 +10,8 @@
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" /> android:maxSdkVersion="32"
tools:ignore="ScopedStorage" />
<uses-permission <uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE" android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" /> android:maxSdkVersion="32" />
@@ -21,9 +22,9 @@
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_logo512" android:icon="@drawable/logo_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_logo512" android:roundIcon="@drawable/logo_launcher"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.SmartPlateCabinet" android:theme="@style/Theme.SmartPlateCabinet"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
@@ -38,19 +39,13 @@
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<activity
android:name=".activity.UnBindDialogActivity"
android:exported="false"
android:theme="@style/DialogActivity" />
<activity <activity
android:name=".activity.InitActivity" android:name=".activity.InitActivity"
android:exported="false" android:exported="false"
android:launchMode="singleTask"> android:launchMode="singleTask">
</activity> </activity>
<activity
android:name=".activity.LoginByPwdActivity"
android:exported="false"
android:launchMode="singleTask"/>
<activity <activity
android:name=".activity.LoginByFaceActivity" android:name=".activity.LoginByFaceActivity"
android:exported="true" android:exported="true"
@@ -69,7 +64,10 @@
android:name=".activity.MainActivity" android:name=".activity.MainActivity"
android:exported="false"> android:exported="false">
</activity> </activity>
<activity android:name="com.sw.platecabinet.activity.FunActivity" /> <activity
android:name=".activity.PlcSettingActivity"
android:exported="false" />
<!-- <activity android:name="com.sw.platecabinet.activity.FunActivity" />-->
</application> </application>
</manifest> </manifest>
@@ -33,7 +33,14 @@ object GlobalData {
/** /**
* 具体业务baseurl * 具体业务baseurl
*/ */
var appBaseUrl: String = "https://dev.yixiong-tech.com:8081" var appBaseUrl: String = ""
/**
* 具体业务 BaseUrl
*/
const val LOCAL_BASE_URL: String = "http://192.168.10.101:24801"
const val TEST_BASE_URL: String = "https://dev.yixiong-tech.com:8081"
const val PROD_BASE_URL: String = "https://platform-api.uat.shuziweidao.com"
/** /**
* 设备id * 设备id
@@ -61,4 +68,5 @@ object GlobalKey {
const val KEY_FIRST_RUN = "firstRun" const val KEY_FIRST_RUN = "firstRun"
const val KEY_USER_INFO = "userInfoKey" const val KEY_USER_INFO = "userInfoKey"
const val PARAM_EQUIPMENT_INFO = "equipmentUserInfo" const val PARAM_EQUIPMENT_INFO = "equipmentUserInfo"
const val KEY_BASE_URL = "baseUrlKey"
} }
+20 -3
View File
@@ -1,17 +1,18 @@
package com.sw.platecabinet package com.sw.platecabinet
import android.annotation.SuppressLint
import android.util.Log import android.util.Log
import com.sw.plate.App import com.sw.plate.App
import com.sw.plate.utils.AppUtil import com.sw.plate.utils.AppUtil
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.utils.CrashHandler import com.sw.platecabinet.utils.CrashHandler
import com.sw.platecabinet.utils.SpTool
import timber.log.Timber import timber.log.Timber
class MyApp : App() { class MyApp : App() {
companion object { companion object {
const val DEBUG: Boolean = true const val DEBUG: Boolean = true
var instance: MyApp? = null var instance: MyApp? = null
var plateList: List<EquipmentUserInfo>? = null // var plateList: List<EquipmentUserInfo>? = null
} }
override fun onCreate() { override fun onCreate() {
@@ -28,13 +29,28 @@ class MyApp : App() {
/** /**
* 初始化全局数据 * 初始化全局数据
*/ */
@SuppressLint("LogNotTimber")
private fun initGlobalData() { private fun initGlobalData() {
var deviceId = AppUtil.getUDID(this) val deviceId = AppUtil.getUDID(this)
// deviceId = "be154831-3466-3ba2-a2ea-57652c919fed" // deviceId = "be154831-3466-3ba2-a2ea-57652c919fed"
Log.d("MyApp", "initialize: deviceId=$deviceId") Log.d("MyApp", "initialize: deviceId=$deviceId")
GlobalData.deviceId = deviceId GlobalData.deviceId = deviceId
val url = SpTool.baseUrl
if (url.isNotBlank()) {
GlobalData.appBaseUrl = url
} else {
GlobalData.appBaseUrl =
if ("14d2dad5-7d33-3ced-8a59-f14cb1ef2d21" == deviceId)
GlobalData.LOCAL_BASE_URL
else
// GlobalData.PROD_BASE_URL
GlobalData.TEST_BASE_URL
}
// TODO: 临时测试---------------- // TODO: 临时测试----------------
// GlobalData.appBaseUrl = GlobalData.TEST_BASE_URL
// GlobalData.deviceId = "79814f20-f253-397c-9da7-8430ffa5ef00"
// if ("2987f0c5-5754-33e9-b00a-251db5e2e55f" == deviceId) { // if ("2987f0c5-5754-33e9-b00a-251db5e2e55f" == deviceId) {
// GlobalData.deviceId = "79814f20-f253-397c-9da7-8430ffa5ef00" // GlobalData.deviceId = "79814f20-f253-397c-9da7-8430ffa5ef00"
// } // }
@@ -42,6 +58,7 @@ class MyApp : App() {
GlobalData.appVersion = AppUtil.getAppVersionCode(this).toString() GlobalData.appVersion = AppUtil.getAppVersionCode(this).toString()
GlobalData.globalEquipmentCode = "202501171634" GlobalData.globalEquipmentCode = "202501171634"
// GlobalData.appBaseUrl = "http://192.168.1.201:14801" // GlobalData.appBaseUrl = "http://192.168.1.201:14801"
} }
} }
@@ -1,45 +1,38 @@
package com.sw.platecabinet.activity package com.sw.platecabinet.activity
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.util.Log
import android.view.KeyEvent
import android.view.View import android.view.View
import android.view.WindowInsetsController import android.view.WindowInsetsController
import android.view.WindowManager import android.view.WindowManager
import android.widget.TextView import android.widget.TextView
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
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.sw.inbound.utils.DateTimeUtils import com.sw.inbound.utils.DateTimeUtils
import com.sw.plate.App 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.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.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.ext.clickWithCoroutines 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 import com.sw.platecabinet.utils.IntervalExecutor
import com.sw.platecabinet.utils.PermissionHelper
import com.sw.platecabinet.utils.PlateUtils
import com.sw.platecabinet.utils.SpTool import com.sw.platecabinet.utils.SpTool
import com.sw.platecabinet.view.CustomLoadingDialog import com.sw.platecabinet.view.CustomLoadingDialog
import com.sw.platecabinet.viewmodel.SettingViewModel import com.sw.platecabinet.viewmodel.NetViewModelV2
import com.sw.platecabinet.viewmodel.UserViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
/** /**
@@ -51,14 +44,11 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
protected lateinit var context: Context protected lateinit var context: Context
private var timeJob: Job? = null private var timeJob: Job? = null
private var mDialogWaiting: CustomLoadingDialog? = null private var mDialogWaiting: CustomLoadingDialog? = null
private val permissionHelpers = mutableMapOf<Int, PermissionHelper>() // private val permissionHelpers = mutableMapOf<Int, PermissionHelper>()
protected var keyEventHelper: ScanGunKeyEventHelper? = null // protected var keyEventHelper: ScanGunKeyEventHelper? = null
// 管理员对应的viewmodel public val recognizeViewModel by viewModels<RecognizeViewModel>()
val settingViewModel by viewModels<SettingViewModel>() public val netViewModelV2 by viewModels<NetViewModelV2>()
// 用户对应的viewModel
protected val userViewModel by viewModels<UserViewModel>()
private var startTime: Long = 0 private var startTime: Long = 0
@@ -80,81 +70,83 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
updateTime() updateTime()
registerDataChange() registerDataChange()
initialize() initialize()
registerKeyEvent() // registerKeyEvent()
val durationTime = System.currentTimeMillis() - startTime val durationTime = System.currentTimeMillis() - startTime
Timber.d("启动时间:$durationTime") Timber.d("启动时间:$durationTime")
} }
/** // /**
* 监听扫描枪扫描事件 // * 监听扫描枪扫描事件
*/ // */
protected fun registerKeyEvent() { // protected fun registerKeyEvent() {
keyEventHelper = // keyEventHelper =
ScanGunKeyEventHelper(context, object : ScanGunKeyEventHelper.OnScanSuccessListener { // ScanGunKeyEventHelper(context, object : ScanGunKeyEventHelper.OnScanSuccessListener {
override fun onScanSuccess(barcode: String?) { // override fun onScanSuccess(barcode: String?) {
Timber.d("onScanSuccess barcode = $barcode") // Timber.d("onScanSuccess barcode = $barcode")
if (barcode == null) return // if (barcode == null) return
handleScanKeyInfo(barcode) // handleScanKeyInfo(barcode)
} // }
}) // })
} // }
/** /**
* 处理扫描枪数据 * 处理扫描枪数据
*/ */
protected open fun handleScanKeyInfo(scanInfo: String) { protected open fun handleScanKeyInfo(scanInfo: String) {
// settingViewModel.findByPlateNumber(plateNumber = scanInfo) //// settingViewModel.findByPlateNumber(plateNumber = scanInfo)
var plateNumber = scanInfo // var plateNumber = scanInfo
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()
} // }
//柜子未关联该餐盘,直接找空位置放入盘子 // //柜子未关联该餐盘,直接找空位置放入盘子
val item = MyApp.plateList?.firstOrNull { it.plateNumber.isNullOrBlank() } // val item = MyApp.plateList?.firstOrNull { it.plateNumber.isNullOrBlank() }
if (item == null) { // if (item == null) {
//柜子已满 // //柜子已满
MainActivity.start(context = context, pageType = PageType.PLATE_CABINET_FULL) // MainActivity.start(context = context, pageType = PageType.PLATE_CABINET_FULL)
return // return
} // }
//找到位置,放入餐盘,清除已有id和时间 // //找到位置,放入餐盘,清除已有id和时间
item.plateNumber = plateNumber // item.plateNumber = plateNumber
item.faceId = null // item.faceId = null
item.updateTime = null // item.updateTime = null
PlateUtils.open(this, item.equipmentBoxCode, true) // PlateUtils.open(this, item.equipmentBoxCode, true)
SpTool.savePlateData(MyApp.plateList!!) // SpTool.savePlateData(MyApp.plateList!!)
} }
override fun dispatchKeyEvent(event: KeyEvent): Boolean { // override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (keyEventHelper != null) { // if (keyEventHelper != null) {
if (keyEventHelper!!.isScanGunEvent(event)) { // if (keyEventHelper!!.isScanGunEvent(event)) {
keyEventHelper!!.analysisKeyEvent(event) // keyEventHelper!!.analysisKeyEvent(event)
return true // return true
} // }
} // }
return super.dispatchKeyEvent(event) // return super.dispatchKeyEvent(event)
} // }
fun updateTime() { fun updateTime() {
headerBinding?.let { headerBinding?.let {
// 双击判定窗口放宽至 600ms:默认 300ms 对触摸屏上的手动双击过短,
// 两次点击间隔常在 300~600ms,导致设置页"点很多次才进"
it.tvLeftDate.clickWithCoroutines( it.tvLeftDate.clickWithCoroutines(
doubleClickInterval = 600,
onDoubleClick = { onDoubleClick = {
onLeftDoubleClick() onLeftDoubleClick()
} }
) )
it.tvRightTime.clickWithCoroutines( it.tvRightTime.clickWithCoroutines(
doubleClickInterval = 600,
onDoubleClick = { onDoubleClick = {
onRightDoubleClick() onRightDoubleClick()
} }
) )
val scope = CoroutineScope(Dispatchers.Main) timeJob = lifecycleScope.launch {
timeJob = scope.launch {
DateTimeUtils.realTimeChineseDateFlow() DateTimeUtils.realTimeChineseDateFlow()
.collect { (date, time) -> .collect { date ->
it.tvLeftDate.text = date it.tvRightTime.text = date
it.tvRightTime.text = time
} }
} }
} }
@@ -171,36 +163,36 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
* 右侧时间双击 * 右侧时间双击
*/ */
open fun onRightDoubleClick() { open fun onRightDoubleClick() {
MainActivity.start(context, pageType = PageType.SETTING_LIST) //MainActivity.start(context, pageType = PageType.SETTING_LIST)
} }
override fun onRequestPermissionsResult( // override fun onRequestPermissionsResult(
requestCode: Int, // requestCode: Int,
permissions: Array<out String>, // permissions: Array<out String>,
grantResults: IntArray // grantResults: IntArray
) { // ) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults) // super.onRequestPermissionsResult(requestCode, permissions, grantResults)
//
val helper = permissionHelpers[requestCode] // val helper = permissionHelpers[requestCode]
val allGranted = PermissionHelper.handlePermissionResult( // val allGranted = PermissionHelper.handlePermissionResult(
this, // this,
requestCode, // requestCode,
permissions, // permissions,
grantResults, // grantResults,
permissionHelper = helper // permissionHelper = helper
) // )
//
if (allGranted) { // if (allGranted) {
permissionHelpers.remove(requestCode) // permissionHelpers.remove(requestCode)
} // }
} // }
//
/** // /**
* 注册PermissionHelper以便处理结果 // * 注册PermissionHelper以便处理结果
*/ // */
fun registerPermissionHelper(helper: PermissionHelper) { // fun registerPermissionHelper(helper: PermissionHelper) {
permissionHelpers[helper.requestCode] = helper // permissionHelpers[helper.requestCode] = helper
} // }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
@@ -265,6 +257,7 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
mDialogWaiting = CustomLoadingDialog(this, view, R.style.MyDialog) mDialogWaiting = CustomLoadingDialog(this, view, R.style.MyDialog)
mDialogWaiting!!.show() mDialogWaiting!!.show()
mDialogWaiting!!.setCancelable(true) mDialogWaiting!!.setCancelable(true)
mDialogWaiting!!.setCanceledOnTouchOutside(true)
} }
} }
@@ -289,125 +282,12 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
*/ */
protected open fun registerDataChange() { protected open fun registerDataChange() {
Timber.d("registerDataChange") Timber.d("registerDataChange")
// lifecycleScope.launch {
// viewModel.showLoading.collect {
// Timber.d("registerDataChange 用户 showLoading = $it")
// if (it) {
// showWaitingDialog("")
// } else {
// hideWaitingDialog()
// }
// }
// }
// lifecycleScope.launch {
// viewModel.currentUserInfo.collect {
// if (it == null) return@collect
//// it.showBalanceNotEnoughDialog = true
// handleLoginSuccess(it, false)
// }
// }
// lifecycleScope.launch {
// settingViewModel.showLoading.collect {
// Timber.d("registerDataChange 管理员 showLoading = $it")
// if (it) {
// showWaitingDialog("")
// } else {
// hideWaitingDialog()
// }
// }
// }
// lifecycleScope.launch {
// settingViewModel.searchUserInfo.collect {
// Timber.d("registerDataChange 管理员 currentUserInfo = $it")
// if (it == null) return@collect
// handleLoginSuccess(it, true)
// }
// }
// lifecycleScope.launch {
// settingViewModel.equipmentList.collect {
// loadEquipmentList(it)
// }
// }
} }
// private fun loadEquipmentList(items: List<EquipmentUserInfo>) {
// if (items.isEmpty()) return
// val unbindList = items.filter { !it.isBound() }
// if (unbindList.isEmpty()) {
// MainActivity.start(context = context, pageType = PageType.PLATE_CABINET_FULL)
// } else {
// val firstInfo = unbindList[0]
// val currentUserInfo = settingViewModel.searchUserInfo.value
// firstInfo.plateNumber = currentUserInfo?.plateNumber ?: ""
// MainActivity.start(
// context = context,
// pageType = PageType.BIND_PLATE,
// equipmentUserInfo = firstInfo
// )
// }
// }
/**
* 处理登录成功操作
* @param isAdmin true 管理员 执行绑盘此操作 false 用户,提示错误
*/
open fun handleLoginSuccess(equipmentUserInfo: EquipmentUserInfo, isAdmin: Boolean) {
// Timber.d("handleLoginSuccess isAdmin = $isAdmin")
// equipmentUserInfo.isIntercept = false
// val cardBalance = equipmentUserInfo.cardBalance ?: 0.toDouble()
// val balanceIsNotEnough = cardBalance <= 0.toDouble()
// if (balanceIsNotEnough && equipmentUserInfo.showBalanceNotEnoughDialog) {
// //提示余额不足弹窗
// equipmentUserInfo.isIntercept = true
// BalanceNotEnoughDialog(this).show()
// return
// }
// viewModel.resetUserInfo()
//// if (equipmentUserInfo.isOtherEquipment()) {
//// MainActivity.start(
//// context,
//// pageType = PageType.PLATE_TIP,
//// equipmentUserInfo = equipmentUserInfo,
//// isAdmin
//// )
//// return
//// }
// if (equipmentUserInfo.equipmentBoxCode?.isNotEmpty() == true) {
// SerialApi.openPlate(
// equipmentUserInfo.equipmentBoxCode!!.toInt(),
// object : SerialPortManager.SendCallback {
// override fun onSuccess() {
// MainActivity.start(
// context,
// pageType = PageType.PLATE_TIP,
// isAdmin = isAdmin
// )
// }
//
// override fun onFail(e: Exception?) {
// ToastUtils.showToast("柜门打开失败")
// }
// })
// } else {
// if (isAdmin) {
// settingViewModel.getEquipmentList {
// loadEquipmentList(it)
// }
// } else {
// Timber.d("无餐盘信息")
// MainActivity.start(
// context,
// pageType = PageType.PLATE_TIP,
// equipmentUserInfo = equipmentUserInfo,
// isAdmin = isAdmin
// )
// }
// }
}
override fun onDestroy() { override fun onDestroy() {
timeJob?.cancel() timeJob?.cancel()
keyEventHelper?.onDestroy() // keyEventHelper?.onDestroy()
super.onDestroy() super.onDestroy()
} }
@@ -417,109 +297,63 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
// private val initialDelay = 5 * 60 * 1000L // private val initialDelay = 5 * 60 * 1000L
// private val dealyMillis = 10 * 60 * 1000L // private val dealyMillis = 10 * 60 * 1000L
private val initialDelay = 5 * 60 * 1000L private val initialDelay = 1 * 1000L
private val dealyMillis = 5 * 60 * 1000L private val dealyMillis = 10 * 1000L
fun startFaceTask(block: (List<String?>) -> Unit) { fun startFaceTask() {
faceTaskJob = faceTaskJob =
intervalExecutor.startIntervalTaskWithInitialDelay(initialDelay, dealyMillis) { intervalExecutor.startIntervalTaskWithInitialDelay(initialDelay, dealyMillis) {
getFaceIncrementList(action = block) getFaceIncrementList()
} }
} }
private var faceTimestamp = 0L private var faceTimestamp = 0L
//private var taskPageNo = 1 //private var taskPageNo = 1
private fun getFaceIncrementList(pageNo: Int = 1, action:(List<String?>)-> Unit) { private fun getFaceIncrementList(pageNo: Int = 1) {
val timestamp = SpTool.lastFaceTimestamp val timestamp = SpTool.lastFaceTimestamp
if (timestamp == 0L) { netViewModelV2.getFaceIncrementList(
return
}
settingViewModel.getFaceIncrementList(
pageNo = pageNo, pageNo = pageNo,
timestamp = timestamp timestamp = timestamp
) { list -> ) {
runOnUiThread {
if (pageNo == 1 && list.isEmpty()) {
//未查询到增量数据
return@runOnUiThread
}
updateFaceData(list)
val userIdList = list.map { it.userId }
action(userIdList)
//if (list.size >= settingViewModel.PAGE_SIZE) {
// faceTimestamp = list.last().faceUpdateTimestamp ?: 0
// taskPageNo++
// getFaceIncrementList(taskPageNo)
// return@runOnUiThread
//}
if (list.isNotEmpty()) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0
}
SpTool.lastFaceTimestamp = faceTimestamp
}
}
}
public val recognizeViewModel by viewModels<RecognizeViewModel>()
private fun updateFaceData(list: List<UserFaceModel>) {
Thread {
val faceList = mutableListOf<FaceEntity>()
val faceDao = FaceDatabase.getInstance(this).faceDao()
try {
list.forEach { model ->
if (model.faceDeleted == true) {
//删除数据
faceDao.deleteFaceById(model.userId)
} else {
val faceData = faceDao.queryByUserName(model.userId)
if (faceData == null) {
//保存数据
val faceEntity = FaceEntity(
model.userId,
null,
Base64.decode(model.faceFeatureStr)
).also {
//1-会员、2-临时用户
it.userType = if (model.isMember) "1" else "2"
it.cardNo = model.cardNo
}
faceList.add(faceEntity)
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
try {
if (faceList.isNotEmpty()) {
FaceDatabase.getInstance(this).faceDao().insert(faceList)
recognizeViewModel.refreshFaceList() recognizeViewModel.refreshFaceList()
} }
} catch (e: Exception) {
e.printStackTrace()
}
}.start()
}
fun initPlateList() {
// SpTool.putString(SpTool.PLATE_BOX_DATA, "")
// var list = SpTool.getPlateData()
val list: MutableList<EquipmentUserInfo> = mutableListOf()
repeat(22) { index ->
list.add(
EquipmentUserInfo(equipmentBoxCode = "${index + 1}")
)
}
MyApp.plateList = list
} }
fun clearAllFace(block: () -> Unit) { fun clearAllFace(block: () -> Unit) {
Thread { lifecycleScope.launch(Dispatchers.IO) {
val faceDao = FaceDatabase.getInstance(App.getContext()).faceDao() val faceDao = FaceDatabase.getInstance(App.getContext()).faceDao()
faceDao.deleteAll() faceDao.deleteAll()
faceDao.resetId() faceDao.resetId()
runOnUiThread { block() } recognizeViewModel.refreshFaceList()
}.start() withContext(Dispatchers.Main) { block() }
}
}
private var permissionCallback: ((isGranted: Boolean) -> Unit)? = null
private var activityCallback: ((intent: Intent?) -> Unit)? = null
fun requestPermission(permission: String, callback: (isGranted: Boolean) -> Unit) {
this.permissionCallback = callback
requestPermissionLauncher.launch(permission)
}
// 权限请求回调
val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
permissionCallback?.invoke(isGranted)
}
fun startActivity(intent: Intent, callback: (Intent?) -> Unit) {
this.activityCallback = callback
startActivityLauncher.launch(intent)
}
// activity页面返回的回调
private val startActivityLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
activityCallback?.invoke(it.data)
} }
} }
@@ -3,10 +3,10 @@ package com.sw.platecabinet.activity
import android.content.Intent import android.content.Intent
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.FaceApi
import com.sw.platecabinet.GlobalData import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.databinding.ActivityDeviceInitBinding import com.sw.platecabinet.databinding.ActivityDeviceInitBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.utils.SpTool
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -25,32 +25,14 @@ class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
} }
override fun initialize() { override fun initialize() {
// val isSuccess = deviceViewModel.checkEquipmentInfo()
// if (isSuccess) {
// goLoginActivity()
// return
// }
// binding.ivQrCode.setImageBitmap(
// QRCodeUtil.generateQRCode(
// content = GlobalData.deviceId,
// size = 200
// )
// )
// binding.btnInit.setOnClickListener {
// deviceViewModel.getDeviceToken()
// }
// binding.btnInit.gone()
// binding.ivQrCode.invisible()
// binding.root.postDelayed({
// goLoginActivity()
// }, 1000)
initPlateList()
showWaitingDialog("加载中……") showWaitingDialog("加载中……")
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
val count = FaceApi().queryFaceCount() //val count = FaceApi().queryFaceCount()
if (count == 0) { //if (count == 0) {
userViewModel.getUserFaceCache { status, msg -> if (SpTool.firstGetFace) {
netViewModelV2.getUserFaceCache { status, msg ->
SpTool.firstGetFace = false
getDeviceConfig() getDeviceConfig()
} }
} else { } else {
@@ -61,7 +43,7 @@ class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
} }
private fun getDeviceConfig() { private fun getDeviceConfig() {
userViewModel.getDeviceConfig { deviceConfig -> netViewModelV2.getDeviceConfig(onSuccess = { deviceConfig ->
runOnUiThread { runOnUiThread {
if (deviceConfig == null) { if (deviceConfig == null) {
hideWaitingDialog() hideWaitingDialog()
@@ -78,7 +60,11 @@ class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
goLoginActivity() goLoginActivity()
}, 500) }, 500)
} }
}, onFailure = { errMsg ->
runOnUiThread {
ToastUtils.showToast(errMsg ?: "接口异常")
} }
})
} }
override fun registerDataChange() { override fun registerDataChange() {
@@ -1,127 +1,152 @@
package com.sw.platecabinet.activity //package com.sw.platecabinet.activity
//
import androidx.lifecycle.lifecycleScope //import androidx.activity.viewModels
import com.sw.plate.utils.ToastUtils //import androidx.lifecycle.lifecycleScope
import com.sw.plate.utils.arcface.facedb.FaceDatabase //import com.sw.plate.utils.Base64
import com.sw.platecabinet.MyApp //import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.databinding.ActivityFunBinding //import com.sw.plate.utils.arcface.facedb.FaceDatabase
import com.sw.platecabinet.databinding.ItemTitleTimeBinding //import com.sw.platecabinet.MyApp
import com.sw.platecabinet.dialog.CustomDialog //import com.sw.platecabinet.databinding.ActivityFunBinding
import com.sw.platecabinet.ext.clickWithDebounce //import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.utils.PlateUtils //import com.sw.platecabinet.dialog.CustomDialog
import com.sw.platecabinet.utils.SpTool //import com.sw.platecabinet.ext.clickWithDebounce
import kotlinx.coroutines.CoroutineDispatcher //import com.sw.platecabinet.utils.PlateUtils
import kotlinx.coroutines.Dispatchers //import com.sw.platecabinet.utils.SpTool
import kotlinx.coroutines.delay //import com.sw.platecabinet.viewmodel.UserViewModel
import kotlinx.coroutines.launch //import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext //import kotlinx.coroutines.delay
//import kotlinx.coroutines.launch
class FunActivity : BaseActivity<ActivityFunBinding>() { //import kotlinx.coroutines.withContext
//import kotlin.getValue
override fun inflateTitleBinding(): ItemTitleTimeBinding? { //
return binding.includeHeader //class FunActivity : BaseActivity<ActivityFunBinding>() {
} //
// override fun inflateTitleBinding(): ItemTitleTimeBinding? {
override fun inflateViewBinding() = ActivityFunBinding.inflate(layoutInflater) // return binding.includeHeader
// }
override fun initialize() { //
binding.btnClearFace.clickWithDebounce { clearFace() } // override fun inflateViewBinding() = ActivityFunBinding.inflate(layoutInflater)
binding.btnClearAndGetAll.clickWithDebounce { clearFaceAndGetAll() } //
binding.btnOpenAllPlate.clickWithDebounce { // override fun initialize() {
lifecycleScope.launch { // binding.btnClearFace.clickWithDebounce { clearFace() }
openAllPlate() // binding.btnClearAndGetAll.clickWithDebounce { clearFaceAndGetAll() }
} // binding.btnOpenAllPlate.clickWithDebounce {
} // lifecycleScope.launch {
binding.btnPutPlate.clickWithDebounce { putPlate() } // openAllPlate()
// }
queryUserCount() // }
} // binding.btnPutPlate.clickWithDebounce { putPlate() }
//
private fun clearFace() { // queryUserCount()
loadClearDialog("请确认是否清除本地所有数据?") { //
showWaitingDialog("加载中……") // binding.btnTestFeature.clickWithDebounce {
clearAllFace { // testFeature()
binding.root.postDelayed( // }
{ // }
hideWaitingDialog() // private fun testFeature() {
ToastUtils.showToast("已清除") // userViewModel.getUserFace(1, 1) {list ->
}, 500 // lifecycleScope.launch {
) // withContext(Dispatchers.IO) {
} // if (list.isEmpty()) return@withContext
} // val userId = list[0].userId
} // val base64Feature = list[0].faceFeatureStr
// val faceDao = FaceDatabase.getInstance(this@FunActivity).faceDao()
private fun clearFaceAndGetAll() { // val entityList = faceDao.queryAllByUserName(userId)
loadClearDialog("请确认是否清除本地所有数据,并重新拉取全量人脸数据?") { // val resultList = entityList.map { base64Feature == Base64.encode(it.featureData) }
showWaitingDialog("加载中……") // val isEqual = resultList.any { true }
clearAllFace { // ToastUtils.showToast("比较结果:$isEqual")
userViewModel.getUserFaceCache { status, msg -> // }
hideWaitingDialog() // }
ToastUtils.showToast( // }
if (status) "请求成功,$msg" else "请求失败,$msg" // }
) //
} // private fun clearFace() {
} // loadClearDialog("请确认是否清除本地所有数据?") {
} // showWaitingDialog("加载中……")
} // clearAllFace {
// binding.root.postDelayed(
private suspend fun openAllPlate() { // {
MyApp.plateList?.let { plateList -> // hideWaitingDialog()
val boxCodeList = plateList.filter { it.equipmentBoxCode.isNullOrBlank().not() } // ToastUtils.showToast("已清除")
.map { it.equipmentBoxCode } // queryUserCount()
boxCodeList.forEach { boxCode -> // }, 500
withContext(Dispatchers.Default) { // )
PlateUtils.openDirect(boxCode) // }
delay(500) // }
} // }
} //
} // private fun clearFaceAndGetAll() {
} // loadClearDialog("请确认是否清除本地所有数据,并重新拉取全量人脸数据?") {
// showWaitingDialog("加载中……")
private fun putPlate() { // clearAllFace {
showWaitingDialog("加载中……") // userViewModel.getUserFaceCache { status, msg ->
MyApp.plateList?.forEachIndexed { index, item -> // hideWaitingDialog()
val num = index + 1 // ToastUtils.showToast(
val realNum = if (num < 10) "0${num}" else "${num}" // if (status) "请求成功,$msg" else "请求失败,$msg"
item.plateNumber = realNum + realNum + realNum // )
item.faceId = null // queryUserCount()
item.updateTime = null // }
} // }
SpTool.savePlateData(MyApp.plateList!!) // }
binding.root.postDelayed({ // }
hideWaitingDialog() //
ToastUtils.showToast("已完成") // private suspend fun openAllPlate() {
}, 1000) // MyApp.plateList?.let { plateList ->
} // val boxCodeList = plateList.filter { it.equipmentBoxCode.isNullOrBlank().not() }
// .map { it.equipmentBoxCode }
/** // boxCodeList.forEach { boxCode ->
* 加载清除数据弹窗 // withContext(Dispatchers.Default) {
*/ // PlateUtils.openDirect(boxCode)
private fun loadClearDialog(msg: String, callback: () -> Unit) { // delay(500)
CustomDialog( // }
activity = this, // }
content = msg, // }
onConfirm = { // }
callback() //
}).show() // private fun putPlate() {
} // showWaitingDialog("加载中……")
// MyApp.plateList?.forEachIndexed { index, item ->
override fun onLeftDoubleClick() { // val num = index + 1
finish() // val realNum = if (num < 10) "0${num}" else "${num}"
} // item.plateNumber = realNum + realNum + realNum
// item.faceId = null
override fun onRightDoubleClick() { // item.updateTime = null
// }
} // SpTool.savePlateData(MyApp.plateList!!)
// binding.root.postDelayed({
private fun queryUserCount() { // hideWaitingDialog()
lifecycleScope.launch { // ToastUtils.showToast("已完成")
val faceDao = FaceDatabase.getInstance(this@FunActivity).faceDao() // }, 1000)
val memberUserCount = faceDao.getFaceCount("1") // }
val tempUserCount = faceDao.getFaceCount("2") //
binding.tvMemberUserCount.text = "会员用户:${memberUserCount}" // /**
binding.tvTempUserCount.text = "临时用户:${tempUserCount}" // * 加载清除数据弹窗
} // */
} // private fun loadClearDialog(msg: String, callback: () -> Unit) {
// CustomDialog(
} // activity = this,
// content = msg,
// onConfirm = {
// callback()
// }).show()
// }
//
// override fun onLeftDoubleClick() {
// finish()
// }
//
// override fun onRightDoubleClick() {
//
// }
//
// private fun queryUserCount() {
// lifecycleScope.launch {
// val faceDao = FaceDatabase.getInstance(this@FunActivity).faceDao()
// val memberUserCount = withContext(Dispatchers.IO) { faceDao.getFaceCount("1") }
// val tempUserCount = withContext(Dispatchers.IO) { faceDao.getFaceCount("2") }
// binding.tvMemberUserCount.text = "会员用户:${memberUserCount}人"
// binding.tvTempUserCount.text = "临时用户:${tempUserCount}人"
// }
// }
//
//}
@@ -41,4 +41,8 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
finish() finish()
} }
override fun onRightDoubleClick() {
PlcSettingActivity.start(this)
}
} }
@@ -7,17 +7,19 @@ import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Point import android.graphics.Point
import android.hardware.Camera import android.hardware.Camera
import android.net.Uri
import android.os.Build import android.os.Build
import android.util.DisplayMetrics import android.provider.Settings
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
import android.widget.FrameLayout
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.arcsoft.face.ErrorInfo import com.arcsoft.face.ErrorInfo
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.google.gson.Gson
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
@@ -30,7 +32,6 @@ import com.sw.plate.utils.arcface.PreviewConfig
import com.sw.plate.utils.arcface.camera.CameraListener import com.sw.plate.utils.arcface.camera.CameraListener
import com.sw.plate.utils.arcface.camera.DualCameraHelper import com.sw.plate.utils.arcface.camera.DualCameraHelper
import com.sw.plate.utils.arcface.face.constants.LivenessType import com.sw.plate.utils.arcface.face.constants.LivenessType
import com.sw.plate.utils.arcface.face.constants.RecognizeColor
import com.sw.plate.utils.arcface.face.model.CompareResult import com.sw.plate.utils.arcface.face.model.CompareResult
import com.sw.plate.utils.arcface.face.model.FacePreviewInfo import com.sw.plate.utils.arcface.face.model.FacePreviewInfo
import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration
@@ -39,27 +40,26 @@ 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.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.dialog.CustomDialog
import com.sw.platecabinet.ext.clickWithDebounce import com.sw.platecabinet.ext.clickWithDebounce
import com.sw.platecabinet.ext.gone import com.sw.platecabinet.ext.gone
import com.sw.platecabinet.ext.visible import com.sw.platecabinet.ext.visible
import com.sw.platecabinet.model.response.EquipmentUserInfo import com.sw.platecabinet.model.response.UserFaceModelV2
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.utils.BitmapSaver import com.sw.platecabinet.utils.BitmapSaver
import com.sw.platecabinet.utils.CountDownManager
import com.sw.platecabinet.utils.CountDownUtil import com.sw.platecabinet.utils.CountDownUtil
import com.sw.platecabinet.utils.Debouncer import com.sw.platecabinet.utils.Debouncer
import com.sw.platecabinet.utils.LogFileUtil import com.sw.platecabinet.utils.LogFileUtil
import com.sw.platecabinet.utils.PermissionHelper
import com.sw.platecabinet.utils.PlateUtils
import com.sw.platecabinet.utils.RegisterCallbackHandler import com.sw.platecabinet.utils.RegisterCallbackHandler
import com.sw.platecabinet.utils.SoundPoolUtil import com.sw.platecabinet.utils.SoundPoolUtil
import com.sw.platecabinet.utils.SpTool import com.sw.platecabinet.utils.mego.PlcCallback
import com.sw.platecabinet.utils.mego.PlcHelper
import com.sw.platecabinet.utils.mego.PlcStatus
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
@@ -73,13 +73,6 @@ import java.util.concurrent.atomic.AtomicBoolean
class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(), class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
ViewTreeObserver.OnGlobalLayoutListener { ViewTreeObserver.OnGlobalLayoutListener {
private val CAMERA_PERMISSION_REQUEST_CODE = 100
private val REQUIRED_PERMISSIONS: Array<String> = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
override fun inflateViewBinding(): ActivityLoginFaceBinding { override fun inflateViewBinding(): ActivityLoginFaceBinding {
return ActivityLoginFaceBinding.inflate(layoutInflater) return ActivityLoginFaceBinding.inflate(layoutInflater)
} }
@@ -97,7 +90,18 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
} }
override fun initialize() { override fun initialize() {
userViewModel.activeEngine() netViewModelV2.activeEngine()
PlcHelper.getInstance(this@LoginByFaceActivity)
.openSerialPort(object : PlcCallback<String> {
override fun onSuccess(result: String) {
ToastUtils.showToast("打开串口 → 成功: ${result}")
}
override fun onError(message: String) {
ToastUtils.showToast("打开串口 → 失败: $message")
}
})
initArcViewModel() initArcViewModel()
initArcView() initArcView()
@@ -107,9 +111,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
EventBus.getDefault().register(this) EventBus.getDefault().register(this)
//开启人脸增量数据定时任务 //开启人脸增量数据定时任务
startFaceTask { startFaceTask()
loadLogInfo("collectFace增量接口数据用户id:${Gson().toJson(it)}")
}
val soundMap = hashMapOf<String, Int>() val soundMap = hashMapOf<String, Int>()
soundMap[COLLECT_SUCCESS] = R.raw.collect_success soundMap[COLLECT_SUCCESS] = R.raw.collect_success
@@ -132,22 +134,53 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
resetRecognizeState() resetRecognizeState()
} }
} }
// 检查相机权限
private fun checkCameraPermission() { private fun checkCameraPermission() {
Timber.i("checkCameraPermission") Timber.i("checkCameraPermission")
val permissionHelper = val hasCameraPermission = ContextCompat.checkSelfPermission(
PermissionHelper.with(this, REQUIRED_PERMISSIONS, CAMERA_PERMISSION_REQUEST_CODE) this,
.onGranted { Manifest.permission.CAMERA
loadLogInfo("checkCameraPermission 权限已允许") ) == android.content.pm.PackageManager.PERMISSION_GRANTED
if (hasCameraPermission) {
// 权限已授予 // 权限已授予
loadLogInfo("checkCameraPermission 权限已允许")
recognizeViewModel.init() recognizeViewModel.init()
initRgbCamera() initRgbCamera()
resumeCamera() resumeCamera()
} else {
// 无权限,请求相机权限
requestCameraPermission()
}
}
private fun requestCameraPermission() {
requestPermission(Manifest.permission.CAMERA) { isGranted ->
if (isGranted) {
// 权限已授予
loadLogInfo("checkCameraPermission 权限已允许")
recognizeViewModel.init()
initRgbCamera()
resumeCamera()
} else {
// 权限被拒绝
loadLogInfo("checkCameraPermission 权限被拒绝")
CustomDialog(
activity = this,
content = "请在设置中开启相机权限",
onConfirm = {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
startActivity(intent) {
checkCameraPermission()
}
}, onCancel = {
}).show()
}
} }
.build()
registerPermissionHelper(permissionHelper)
permissionHelper.checkAndRequest()
} }
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
@@ -161,19 +194,30 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
finish() finish()
} }
override fun onRightDoubleClick() {
PlcSettingActivity.start(this)
}
private var isRecognition = false private var isRecognition = false
private var rgbCameraHelper: DualCameraHelper? = null private var rgbCameraHelper: DualCameraHelper? = null
private var rgbFaceRectTransformer: FaceRectTransformer? = null private var rgbFaceRectTransformer: FaceRectTransformer? = null
private val livenessType = LivenessType.RGB private val livenessType = LivenessType.RGB
private var openRectInfoDraw = false private var openRectInfoDraw = false
private val leftDebouncer = Debouncer(30*1000) private val leftDebouncer = Debouncer(30 * 1000)
/** 识别超时倒计时(15秒无操作跳回初始页) */
private val recognizeCountDownUtil = CountDownUtil()
/** 用户离开画面后的倒计时(llCollectButton 显示状态下无人脸时触发) */
private val leftCountDownUtil = CountDownUtil()
private var registerHandler: RegisterCallbackHandler? = null private var registerHandler: RegisterCallbackHandler? = null
private fun isFitData(userFaceInfo: UserFaceInfo?): Boolean { private fun isFitData(userFaceInfo: UserFaceInfo?): Boolean {
if (userFaceInfo == null) return false if (userFaceInfo == null) return false
val headBmp = userFaceInfo.headBmp val headBmp = userFaceInfo.headBmp
loadLogInfo("collectFace,isFitData:w=${headBmp?.width},height=${headBmp?.height}")
if (headBmp == null) return false if (headBmp == null) return false
// if (headBmp.width <= 200 || headBmp.height <= 200) { // if (headBmp.width <= 200 || headBmp.height <= 200) {
// loadLogInfo("collectFace,isFitData:拿到人脸提取特征数据照片过小-----------") // loadLogInfo("collectFace,isFitData:拿到人脸提取特征数据照片过小-----------")
@@ -187,11 +231,17 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
recognizeViewModel.prepareRegister() recognizeViewModel.prepareRegister()
recognizeViewModel.setOnRegisterFinishedCallback { facePreviewInfo, userFaceInfo -> recognizeViewModel.setOnRegisterFinishedCallback { facePreviewInfo, userFaceInfo ->
registerHandler?.setCallback { registerHandler?.setCallback {
loadLogInfo("collectFace,registerHandler?.setCallback")
if (!isCollectFace || !isCollecting.get()) { if (!isCollectFace || !isCollecting.get()) {
loadLogInfo("collectFace,isCollectFace=${isCollectFace},isCollecting=${isCollecting.get()}")
return@setCallback return@setCallback
} }
loadLogInfo("collectFace,userFaceInfo==null:${userFaceInfo==null}")
if (this.userFaceInfo == null && isFitData(userFaceInfo)) { if (this.userFaceInfo == null && isFitData(userFaceInfo)) {
val headBmp = userFaceInfo.headBmp val headBmp = userFaceInfo.headBmp ?: run {
loadLogInfo("collectFace,isFitData:headBmp 为 null,跳过")
return@setCallback
}
if (headBmp.width <= 200 || headBmp.height <= 200) { if (headBmp.width <= 200 || headBmp.height <= 200) {
loadLogInfo("collectFace,isFitData:拿到人脸提取特征数据照片过小w:${headBmp.width},h:${headBmp.height}-----------") loadLogInfo("collectFace,isFitData:拿到人脸提取特征数据照片过小w:${headBmp.width},h:${headBmp.height}-----------")
binding.llCollectButton.visible() binding.llCollectButton.visible()
@@ -206,30 +256,30 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
} }
} }
recognizeViewModel.ftInitCode.observe(this) { ftInitCode: Int? -> recognizeViewModel.ftInitCode.observe(this) { ftInitCode: Int? ->
if (ftInitCode != ErrorInfo.MOK) { if (ftInitCode != null && ftInitCode != ErrorInfo.MOK) {
val error: String? = getString( val error: String? = getString(
R.string.specific_engine_init_failed, "ftEngine", R.string.specific_engine_init_failed, "ftEngine",
ftInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(ftInitCode!!) ftInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(ftInitCode)
) )
Timber.e("ftInitCode observe = $error") Timber.e("ftInitCode observe = $error")
ToastUtils.showToast(error) ToastUtils.showToast(error)
} }
} }
recognizeViewModel.frInitCode.observe(this) { frInitCode: Int? -> recognizeViewModel.frInitCode.observe(this) { frInitCode: Int? ->
if (frInitCode != ErrorInfo.MOK) { if (frInitCode != null && frInitCode != ErrorInfo.MOK) {
val error: String? = getString( val error: String? = getString(
R.string.specific_engine_init_failed, "frEngine", R.string.specific_engine_init_failed, "frEngine",
frInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(frInitCode!!) frInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(frInitCode)
) )
Timber.e("frInitCode observe = $error") Timber.e("frInitCode observe = $error")
ToastUtils.showToast(error) ToastUtils.showToast(error)
} }
} }
recognizeViewModel.flInitCode.observe(this) { flInitCode: Int? -> recognizeViewModel.flInitCode.observe(this) { flInitCode: Int? ->
if (flInitCode != ErrorInfo.MOK) { if (flInitCode != null && flInitCode != ErrorInfo.MOK) {
val error: String? = getString( val error: String? = getString(
R.string.specific_engine_init_failed, "flEngine", R.string.specific_engine_init_failed, "flEngine",
flInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(flInitCode!!) flInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(flInitCode)
) )
Timber.e("flInitCode observe = $error") Timber.e("flInitCode observe = $error")
ToastUtils.showToast(error) ToastUtils.showToast(error)
@@ -245,7 +295,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
} }
recognizeViewModel.recognizeUserId.observe(this) { result: CompareResult? -> recognizeViewModel.recognizeUserId.observe(this) { result: CompareResult? ->
loadLogInfo("collectFace,recognizeUserId observe userId = ${result?.faceEntity?.userName}") loadLogInfo("collectFace,recognizeUserId observe userId=${result?.faceEntity?.userName},similar=${result?.similar},userType=${result?.faceEntity?.userType}")
loadFaceRecognizeResult3(result) loadFaceRecognizeResult3(result)
} }
@@ -269,7 +319,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
*/ */
private val faceSuccessThreshold by lazy { ConfigUtil.getRecognizeThreshold(this) } private val faceSuccessThreshold by lazy { ConfigUtil.getRecognizeThreshold(this) }
private val openPlateDebouncer = Debouncer(5000) private val openPlateDebouncer = Debouncer(3000)
private val isCollecting = AtomicBoolean(false) private val isCollecting = AtomicBoolean(false)
private fun loadFaceRecognizeResult3(result: CompareResult?) { private fun loadFaceRecognizeResult3(result: CompareResult?) {
@@ -313,11 +363,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
loadLogInfo("collectFace,recognizeUserId 识别成功但 userId 为空,忽略") loadLogInfo("collectFace,recognizeUserId 识别成功但 userId 为空,忽略")
return return
} }
loadLogInfo("collectFace,recognizeUserId 识别成功 userId=$userId, time=${DateTimeUtils.getDateTimeString()}")
loadLogInfo(
"collectFace,recognizeUserId 识别成功 userId=$userId, time=${DateTimeUtils.getDateTimeString()}"
)
// 成功后必须重置状态 // 成功后必须重置状态
retryFailCount = 0 retryFailCount = 0
isCollecting.set(false) isCollecting.set(false)
@@ -325,13 +371,13 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
currentUserId = userId currentUserId = userId
// 防抖,防止重复开闸 // 防抖,防止重复开闸
openPlateDebouncer.debounce { openPlateDebouncer.debounce("openPlateDebouncer") {
openPlate(userId) openPlate(userId)
} }
} }
private fun recognizeFail() { private fun recognizeFail() {
CountDownManager.cancelCountDown() recognizeCountDownUtil.cancelCountDown()
binding.tvToInit.text = "" binding.tvToInit.text = ""
binding.tvSimilarValue.text = "" binding.tvSimilarValue.text = ""
binding.llCollectButton.visible() binding.llCollectButton.visible()
@@ -343,59 +389,153 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
binding.ivCollectPhoto.visible() binding.ivCollectPhoto.visible()
} }
/** private fun newUserCollect() {
* 查询餐盘数据 recognizeCountDownUtil.cancelCountDown()
*/ binding.tvToInit.text = ""
private fun getPlateData(block: (List<EquipmentUserInfo>) -> Unit) { binding.tvSimilarValue.text = ""
MyApp.plateList?.let { binding.llCollectButton.visible()
block(it) binding.btnRetry.gone()
binding.btnCollect.run {
visible()
text = "新用户采集"
} }
binding.ivCollectPhoto.gone()
openPlateDebouncer.reset()
plateEnable = true
resumeCamera()
} }
private var currentPlateInfo: EquipmentUserInfo? = null
private var plateEnable = false private var plateEnable = false
/** /**
* 打开餐盘柜 * 打开餐盘柜:执行出盘,出盘后查询剩余餐盘状态并播放对应提示音
*/ */
private fun openPlate(userId: String?) { private fun openPlate(userId: String?) {
loadLogInfo("collectFace,openPlate: userId=$userId,plateEnable=$plateEnable")
if (plateEnable.not()) return if (plateEnable.not()) return
loadLogInfo("collectFace,openPlate: userId=$userId")
getPlateData { items -> PlcHelper.getInstance(this@LoginByFaceActivity)
loadLogInfo("collectFace,openPlate: items:${Gson().toJson(items)}") .outPlate(object : PlcCallback<String> {
// 未查询到当前用户,查询第一个未绑定的餐盘 override fun onSuccess(result: String) {
val item = items.firstOrNull { it.plateNumber.isNullOrEmpty().not() } loadLogInfo("collectFace,openPlate: 出盘完成,result=$result,开始查询剩余餐盘")
loadLogInfo("collectFace,openPlate: item2:${item}") onOutPlateSuccess(result, RECOGNIZE_SUCCESS)
if (item == null) { }
//暂无餐盘
loadLogInfo("collectFace,openPlate: 暂无餐盘,请联系管理人员,isCollectFace=$isCollectFace") override fun onError(message: String) {
//ToastUtils.showToast("暂无餐盘,请联系管理人员") onOutPlateError(message)
}
})
}
/**
* 新用户注册采集成功后立即出盘(与上传后台并行,互不阻塞)。
* 故意不经过 recognizeSuccess:避免使用临时 userId、避免与识别成功的防抖语义混淆。
*/
private fun outPlateForRegister() {
loadLogInfo("collectFace,outPlateForRegister: 新用户采集成功,立即出盘,plateEnable=$plateEnable")
if (plateEnable.not()) return
PlcHelper.getInstance(this@LoginByFaceActivity)
.outPlate(object : PlcCallback<String> {
override fun onSuccess(result: String) {
loadLogInfo("collectFace,outPlateForRegister: 出盘完成,result=$result")
onOutPlateSuccess(result, COLLECT_SUCCESS)
}
override fun onError(message: String) {
onOutPlateError(message)
}
})
}
/**
* 出盘成功后的统一处理:
* 1. 置 plateEnable=false、取消识别倒计时,防止重复出盘;
* 2. result 为 "false" 表示出盘后已无餐盘,播放无盘提示音并跳转;
* 3. 否则播放提示音并查询剩余餐盘状态。
*
* @param result PLC 返回结果("false" 表示出盘后无餐盘)
* @param successSound 出盘成功播放的提示音 key(识别成功 / 采集成功)
*/
private fun onOutPlateSuccess(result: String, successSound: String) {
plateEnable = false
recognizeCountDownUtil.cancelCountDown()
if (result.equals("false", ignoreCase = true)) {
loadLogInfo("collectFace,onOutPlateSuccess: result=false,餐盘已空")
SoundPoolUtil.getInstance().play(NO_PLATE_REMIND, 0) SoundPoolUtil.getInstance().play(NO_PLATE_REMIND, 0)
goInitActivity() goInitActivity()
return@getPlateData return
} }
// if (tempUserId != currentUserId) { SoundPoolUtil.getInstance().play(successSound, 0)
SoundPoolUtil.getInstance().play(RECOGNIZE_SUCCESS, 0) // 已注册用户:出盘成功且有盘时,上报取餐盘时刻(P-09a)
// } else { if (successSound == RECOGNIZE_SUCCESS) {
// SoundPoolUtil.getInstance().play(COLLECT_SUCCESS, 0) reportPlatePickup(currentUserId, "recognize")
// } }
loadLogInfo("collectFace,openPlate: item3:${item}") checkRemainingPlate()
item.faceId = userId }
item.updateTime = DateTimeUtils.getDateTimeString()
item.plateNumber = null /**
plateEnable = false * 出盘失败后的统一处理:提示失败并跳转回初始页
openDebouncer.debounce { */
//打开柜子 private fun onOutPlateError(message: String) {
PlateUtils.open(this, item.equipmentBoxCode, false) { ToastUtils.showToast("出盘失败,message=$message")
loadLogInfo("collectFace,onOutPlateError: 出盘失败,message=$message")
goInitActivity() goInitActivity()
} }
CountDownManager.cancelCountDown()
/**
* 上报取餐盘时刻(P-09a):记录用户开始就餐时刻,是后续就餐记录(P-09)的强前置。
*
* 调用时机:
* - 已注册用户:识别成功且出盘成功(有盘)后,userId 取自 [currentUserId](来源 "recognize");
* - 新用户:addUserFace 注册成功拿到真实 userId 后(出盘已由 outPlateForRegister 并行触发,来源 "collect")。
*
* 上报失败仅记录日志,不影响吐盘主流程。
*
* @param userId 用户 id(餐盘号 = userId),为空或非数字时跳过
* @param source 调用来源标识,仅用于日志区分
*/
private fun reportPlatePickup(userId: String?, source: String) {
val id = userId?.toLongOrNull()
if (id == null) {
loadLogInfo("collectFace,reportPlatePickup[$source]: userId 为空或非数字,跳过上报,userId=$userId")
return
} }
currentPlateInfo = item loadLogInfo("collectFace,reportPlatePickup[$source]: 上报取餐盘时刻,userId=$id")
MyApp.plateList = items netViewModelV2.platePickup(id) { success, msg ->
SpTool.savePlateData(items) loadLogInfo("collectFace,reportPlatePickup[$source]: 上报完成,success=$successmsg=$msg")
} }
} }
/**
* 出盘成功后查询剩余餐盘状态,无盘则播放提示音
*/
private fun checkRemainingPlate() {
PlcHelper.getInstance(this@LoginByFaceActivity)
.getStatus(object : PlcCallback<PlcStatus> {
override fun onSuccess(result: PlcStatus) {
if (isFinishing || isDestroyed) return
loadLogInfo("collectFace,checkRemainingPlate: isEmptyPlate=${result.isEmptyPlate}")
if (result.isEmptyPlate) {
// 餐盘已空,播放无盘提示音
SoundPoolUtil.getInstance().play(NO_PLATE_REMIND, 0)
//跳转到餐盘设置页面
PlcSettingActivity.start(this@LoginByFaceActivity, showTips = true)
} else {
loadLogInfo("collectFace,checkRemainingPlate: 剩余餐盘,继续使用")
goInitActivity()
}
}
override fun onError(message: String) {
if (isFinishing || isDestroyed) return
// 状态查询失败,不影响主流程,直接跳转
loadLogInfo("collectFace,checkRemainingPlate: 获取PLC状态失败,message=$message")
goInitActivity()
}
})
}
private val openDebouncer by lazy { Debouncer(5000) } private val openDebouncer by lazy { Debouncer(5000) }
private fun initArcView() { private fun initArcView() {
@@ -407,13 +547,13 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
override fun onDestroy() { override fun onDestroy() {
loadLogInfo("onDestroy") loadLogInfo("onDestroy")
if (rgbCameraHelper != null) {
rgbCameraHelper?.release() rgbCameraHelper?.release()
rgbCameraHelper = null rgbCameraHelper = null
}
recognizeViewModel.destroy() recognizeViewModel.destroy()
PlcHelper.getInstance(this@LoginByFaceActivity).release()
EventBus.getDefault().unregister(this) EventBus.getDefault().unregister(this)
super.onDestroy() super.onDestroy()
} }
@@ -438,43 +578,45 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
displayOrientation: Int, displayOrientation: Int,
scale: Float scale: Float
): ViewGroup.LayoutParams { ): ViewGroup.LayoutParams {
val layoutParams = previewView.layoutParams // val layoutParams = previewView.layoutParams
val measuredWidth = previewView.measuredWidth // val measuredWidth = previewView.measuredWidth
val measuredHeight = previewView.measuredHeight // val measuredHeight = previewView.measuredHeight
var ratio = (previewSize.height.toFloat()) / previewSize.width.toFloat() // var ratio = (previewSize.height.toFloat()) / previewSize.width.toFloat()
if (ratio > 1) { // if (ratio > 1) {
ratio = 1 / ratio // ratio = 1 / ratio
} // }
if (displayOrientation % 180 == 0) { // if (displayOrientation % 180 == 0) {
layoutParams.width = measuredWidth // layoutParams.width = measuredWidth
layoutParams.height = (measuredWidth * ratio).toInt() // layoutParams.height = (measuredWidth * ratio).toInt()
} else { // } else {
layoutParams.height = measuredHeight // layoutParams.height = measuredHeight
layoutParams.width = (measuredHeight * ratio).toInt() // layoutParams.width = (measuredHeight * ratio).toInt()
} // }
if (scale < 1f) { // if (scale < 1f) {
val rgbParam = rgbPreview.layoutParams // val rgbParam = rgbPreview.layoutParams
layoutParams.width = (rgbParam.width * scale).toInt() // layoutParams.width = (rgbParam.width * scale).toInt()
layoutParams.height = (rgbParam.height * scale).toInt() // layoutParams.height = (rgbParam.height * scale).toInt()
} else { // } else {
layoutParams.width = (layoutParams.width * scale).toInt() // layoutParams.width = (layoutParams.width * scale).toInt()
layoutParams.height = (layoutParams.height * scale).toInt() // layoutParams.height = (layoutParams.height * scale).toInt()
} // }
//
val metrics = DisplayMetrics() // val metrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(metrics) // windowManager.defaultDisplay.getMetrics(metrics)
//
if (layoutParams.width >= metrics.widthPixels) { // if (layoutParams.width >= metrics.widthPixels) {
val viewRatio = layoutParams.width / (metrics.widthPixels.toFloat()) // val viewRatio = layoutParams.width / (metrics.widthPixels.toFloat())
layoutParams.width = (layoutParams.width / viewRatio).toInt() // layoutParams.width = (layoutParams.width / viewRatio).toInt()
layoutParams.height = (layoutParams.height / viewRatio).toInt() // layoutParams.height = (layoutParams.height / viewRatio).toInt()
} // }
if (layoutParams.height >= metrics.heightPixels) { // if (layoutParams.height >= metrics.heightPixels) {
val viewRatio = layoutParams.height / (metrics.heightPixels.toFloat()) // val viewRatio = layoutParams.height / (metrics.heightPixels.toFloat())
layoutParams.width = (layoutParams.width / viewRatio).toInt() // layoutParams.width = (layoutParams.width / viewRatio).toInt()
layoutParams.height = (layoutParams.height / viewRatio).toInt() // layoutParams.height = (layoutParams.height / viewRatio).toInt()
} // }
val layoutParams = FrameLayout.LayoutParams(776, 582);
Timber.d("adjustPreviewViewSize,${layoutParams.width},${layoutParams.height}}")
previewView.setLayoutParams(layoutParams) previewView.setLayoutParams(layoutParams)
faceRectView.setLayoutParams(layoutParams) faceRectView.setLayoutParams(layoutParams)
return layoutParams return layoutParams
@@ -515,11 +657,11 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
previewSizeRgb.height, previewSizeRgb.height,
layoutParams.width, layoutParams.width,
layoutParams.height, layoutParams.height,
90, 0,
cameraId, cameraId,
isMirror, isMirror,
true, false,
true false
) )
recognizeViewModel.onRgbCameraOpened(camera) recognizeViewModel.onRgbCameraOpened(camera)
@@ -538,15 +680,17 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
drawPreviewInfo(facePreviewInfoList) drawPreviewInfo(facePreviewInfoList)
} }
if (facePreviewInfoList.isNullOrEmpty()) { if (facePreviewInfoList.isNullOrEmpty()) {
currentPlateInfo = null
currentUserId = null currentUserId = null
userFaceInfo = null userFaceInfo = null
binding.btnCollect.tag = "" binding.btnCollect.tag = ""
binding.tvSimilarValue.text = "" binding.tvSimilarValue.text = ""
retryFailCount = 0 // 注意:此处不再重置 retryFailCount。
// 人脸检测会抖动(短暂丢帧),若每次无人脸就清零,"尝试几次失败"将无法累计。
// 失败计数仅在识别成功、会话重启、确认用户离开时清零。
// 人脸离开时,发送空帧清除 ViewModel 内部识别缓存,防止再次进入时使用旧数据
recognizeViewModel.onPreviewFrame(emptyFrame, true)
if (binding.llCollectButton.isVisible) { if (binding.llCollectButton.isVisible) {
leftDebouncer.debounce { leftCountDown() } leftDebouncer.debounce("leftDebouncer") { leftCountDown() }
} }
} }
recognizeViewModel.clearLeftFace(facePreviewInfoList) recognizeViewModel.clearLeftFace(facePreviewInfoList)
@@ -554,6 +698,8 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
override fun onCameraClosed() { override fun onCameraClosed() {
Timber.i("initRgbCamera onCameraClosed: ") Timber.i("initRgbCamera onCameraClosed: ")
// 相机关闭时清空预览画面,防止 TextureView 冻结残留上一位用户的帧
clearPreviewFrame()
} }
override fun onCameraError(e: java.lang.Exception) { override fun onCameraError(e: java.lang.Exception) {
@@ -564,7 +710,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) { override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) {
Timber.i("initRgbCamera onCameraConfigurationChanged: threadName = ${Thread.currentThread().name}") Timber.i("initRgbCamera onCameraConfigurationChanged: threadName = ${Thread.currentThread().name}")
if (rgbFaceRectTransformer != null) { if (rgbFaceRectTransformer != null) {
rgbFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation rgbFaceRectTransformer?.cameraDisplayOrientation = displayOrientation
} }
Timber.i("initRgbCamera onCameraConfigurationChanged: $cameraID $displayOrientation") Timber.i("initRgbCamera onCameraConfigurationChanged: $cameraID $displayOrientation")
} }
@@ -582,6 +728,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
.additionalRotation(0) // 角度 .additionalRotation(0) // 角度
.previewSize(recognizeViewModel.loadPreviewSize()) .previewSize(recognizeViewModel.loadPreviewSize())
.specificCameraId(previewConfig.rgbCameraId) .specificCameraId(previewConfig.rgbCameraId)
// .isMirror(true)
.isMirror(true) .isMirror(true)
.previewOn(binding.dualCameraTexturePreviewRgb) .previewOn(binding.dualCameraTexturePreviewRgb)
.cameraListener(cameraListener) .cameraListener(cameraListener)
@@ -605,10 +752,10 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
} }
} }
fun openRectInfoDraw(view: View?) { // fun openRectInfoDraw(view: View?) {
openRectInfoDraw = !openRectInfoDraw // openRectInfoDraw = !openRectInfoDraw
recognizeViewModel.setDrawRectInfoTextValue(openRectInfoDraw) // recognizeViewModel.setDrawRectInfoTextValue(openRectInfoDraw)
} // }
override fun onGlobalLayout() { override fun onGlobalLayout() {
@@ -618,12 +765,32 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
checkCameraPermission() checkCameraPermission()
} }
/**
* 清除预览 TextureView 中缓存的最后一帧画面(提交一帧全黑)。
* 相机关闭后 TextureView 会冻结保留最后一帧,若不清除,
* 下一位用户重启识别时会先闪现上一位用户的画面。
*/
private fun clearPreviewFrame() {
val previewView = binding.dualCameraTexturePreviewRgb
if (!previewView.isAvailable) return
runCatching {
val canvas = previewView.lockCanvas() ?: return@runCatching
canvas.drawColor(android.graphics.Color.BLACK)
previewView.unlockCanvasAndPost(canvas)
}.onFailure {
loadLogInfo("clearPreviewFrame 失败: ${it.message}")
}
}
private fun resumeCamera() { private fun resumeCamera() {
// isRecognition = true // isRecognition = true
if (rgbCameraHelper != null && rgbCameraHelper!!.isStopped) { val helper = rgbCameraHelper
if (helper != null && helper.isStopped) {
loadLogInfo("collectFace,resumeCamera: ") loadLogInfo("collectFace,resumeCamera: ")
// 重启相机前先清空残留帧,避免闪现上一位用户的画面
clearPreviewFrame()
isRecognition = true isRecognition = true
rgbCameraHelper?.start() helper.start()
} else { } else {
//recognizeViewModel.onPreviewFrame(ByteArray(1382400), true) //recognizeViewModel.onPreviewFrame(ByteArray(1382400), true)
@@ -658,24 +825,37 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
} }
// 调用示例:构造 1920×1080 的全黑帧(长度 1382400 // 调用示例:构造 1920×1080 的全黑帧(长度 1382400
private val emptyFrame = createBlackNV21Frame(1920, 1080) // private val emptyFrame = createBlackNV21Frame(1920, 1080)
private val emptyFrame = createBlackNV21Frame(1280, 720)
// 传递“空帧” recognizeViewModel.onPreviewFrame(emptyFrame, true) // 传递“空帧” recognizeViewModel.onPreviewFrame(emptyFrame, true)
// /** // /**
// * 临时用户id // * 临时用户id
// */ // */
// private var tempUserId: String = "" // private var tempUserId: String = ""
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
plateEnable = true
try { try {
if (isFirstOpen) { if (isFirstOpen) {
goInitActivity()
binding.root.postDelayed({
isFirstOpen = false isFirstOpen = false
}, 1000) goInitActivity()
return return
} }
lifecycleScope.launch {
try {
val faceDao = FaceDatabase.getInstance(this@LoginByFaceActivity).faceDao()
val faceCount = withContext(Dispatchers.IO) { faceDao.faceCount }
if (faceCount == 0) {
newUserCollect()
} else {
resetRecognizeState() resetRecognizeState()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@@ -696,20 +876,24 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
plateEnable = true plateEnable = true
collectCount = 0 collectCount = 0
// tempUserId = "${System.currentTimeMillis()}" // tempUserId = "${System.currentTimeMillis()}"
openPlateDebouncer.reset()
// 清空上次识别结果和 FaceHelper 内部状态,防止短时间内再次识别无法触发
recognizeViewModel.resetFaceState()
resumeCamera() resumeCamera()
registerHandler = RegisterCallbackHandler() registerHandler = RegisterCallbackHandler()
CountDownManager.startCountDown( recognizeCountDownUtil.startCountDown(
totalSeconds = 15, total = 15,
lifecycleScope = lifecycleScope,
onTick = { remaining -> onTick = { remaining ->
loadLogInfo("resetRecognizeState剩余时间:$remaining") loadLogInfo("resetRecognizeState剩余时间:$remaining")
if (judgeRecognizeState()) return@startCountDown // if (judgeRecognizeState()) return@startCountDown
loadLogInfo("initCountTime onTick = $remaining") loadLogInfo("initCountTime onTick = $remaining")
binding.tvToInit.text = "$remaining" binding.tvToInit.text = "$remaining"
}, },
onFinish = { onFinish = {
loadLogInfo("resetRecognizeState倒计时结束") loadLogInfo("resetRecognizeState倒计时结束")
if (judgeRecognizeState()) return@startCountDown // if (judgeRecognizeState()) return@startCountDown
if (facePreviewInfoList.isNullOrEmpty()) { if (facePreviewInfoList.isNullOrEmpty()) {
goInitActivity() goInitActivity()
} else { } else {
@@ -725,26 +909,28 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
override fun onPause() { override fun onPause() {
pauseCamera() pauseCamera()
super.onPause() super.onPause()
CountDownManager.cancelCountDown() recognizeCountDownUtil.cancelCountDown()
} }
private fun pauseCamera() { private fun pauseCamera() {
isRecognition = false isRecognition = false
// recognizeViewModel.onPreviewFrame(emptyFrame, true) recognizeViewModel.onPreviewFrame(emptyFrame, true)
} }
private fun judgeRecognizeState(): Boolean { // private fun judgeRecognizeState(): Boolean {
val rectColor = binding.dualCameraFaceRectView.rectColor // val rectColor = binding.dualCameraFaceRectView.rectColor
if (rectColor == RecognizeColor.COLOR_SUCCESS && currentUserId.isNullOrBlank().not() && facePreviewInfoList.isNullOrEmpty().not()) { // if (rectColor == RecognizeColor.COLOR_SUCCESS && currentUserId.isNullOrBlank()
//人未离开,再次识别 // .not() && facePreviewInfoList.isNullOrEmpty().not()
loadLogInfo("userId=$currentUserId") // ) {
openPlateDebouncer.debounce { // //人未离开,再次识别
openPlate(currentUserId) // loadLogInfo("userId=$currentUserId")
} // openPlateDebouncer.debounce {
return true // openPlate(currentUserId)
} // }
return false // return true
} // }
// return false
// }
fun goInitActivity() { fun goInitActivity() {
val intent = Intent(context, InitActivity::class.java) val intent = Intent(context, InitActivity::class.java)
@@ -781,13 +967,13 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
ToastUtils.showToast("获取人脸照片失败") ToastUtils.showToast("获取人脸照片失败")
return return
} }
uploadFaceDebouncer.debounce { uploadFaceDebouncer.debounce("uploadFaceDebouncer") {
addUserFace(faceData, imageFile) addUserFace(faceData, imageFile)
} }
} }
private fun addUserFace(faceData: String, imageFile: File) { private fun addUserFace(faceData: String, imageFile: File) {
settingViewModel.addUserFace(faceData, imageFile) { status, faceModel -> netViewModelV2.addUserFace(faceData, imageFile) { status, faceModel ->
loadLogInfo("collectFace,saveFaceInfo: ----------12,人脸数据发送完成时间:${DateTimeUtils.getDateTimeString()},结果:$status-----------") loadLogInfo("collectFace,saveFaceInfo: ----------12,人脸数据发送完成时间:${DateTimeUtils.getDateTimeString()},结果:$status-----------")
if (status.not()) { if (status.not()) {
ToastUtils.showToast("新增数据失败") ToastUtils.showToast("新增数据失败")
@@ -799,10 +985,13 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
loadLogInfo("collectFace,sendFaceData: ----------12,发送人脸数据成功但接口返回为null,faceData=${faceData}") loadLogInfo("collectFace,sendFaceData: ----------12,发送人脸数据成功但接口返回为null,faceData=${faceData}")
return@addUserFace return@addUserFace
} }
// 新用户:注册成功拿到真实 userId 后,上报取餐盘时刻(P-09a)
reportPlatePickup(faceModel.userId, "collect")
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
saveFaceInfo(faceModel) { saveFaceInfo(faceModel) {
runOnUiThread { runOnUiThread {
Glide.with(this@LoginByFaceActivity).load(imageFile).into(binding.ivCollectPhoto) Glide.with(this@LoginByFaceActivity).load(imageFile)
.into(binding.ivCollectPhoto)
} }
} }
uploadFaceDebouncer.reset() uploadFaceDebouncer.reset()
@@ -825,6 +1014,9 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
//block(faceData, headBmp) //block(faceData, headBmp)
isCollecting.set(false) isCollecting.set(false)
isCollectFace = false isCollectFace = false
// 采集成功后立即出盘,与上传后台并行执行,确保上传失败时仍能吐盘
outPlateForRegister()
// 异步上传后台(上传成功后才入库),不阻塞出盘
sendFaceData(faceData, headBmp) sendFaceData(faceData, headBmp)
} }
@@ -842,23 +1034,30 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
private var currentUserId: String? = null private var currentUserId: String? = null
private fun saveFaceInfo(faceModel: UserFaceModel, block: () -> Unit = {}) { private fun saveFaceInfo(faceModel: UserFaceModelV2, block: () -> Unit = {}) {
try { try {
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 = faceModel.toFaceEntity()
faceModel.userId, // 先查后插:按 userFaceId 判重,避免采集入库与增量同步各插一条
null, val userFaceId = faceModel.userFaceId
Base64.decode(faceModel.faceFeatureStr) val existing = if (!userFaceId.isNullOrEmpty()) faceDao.queryByUserFaceId(userFaceId) else null
).also { val faceId = if (existing != null) {
//1-会员、2-临时用户 // 已存在记录,更新特征等字段并复用主键,避免重复
it.userType = if (faceModel.isMember) "1" else "2" faceEntity.faceId = existing.faceId
} faceDao.updateFaceEntity(faceEntity)
existing.faceId
} else {
faceDao.insert(faceEntity) faceDao.insert(faceEntity)
}
faceEntity.faceId = faceId
loadLogInfo("collectFace,saveFaceInfo: ----------10,人脸数据保存完成-----------") loadLogInfo("collectFace,saveFaceInfo: ----------10,人脸数据保存完成-----------")
recognizeViewModel.refreshFaceList() recognizeViewModel.refreshFaceList()
loadLogInfo("collectFace,saveFaceInfo: ----------11,刷新人脸数据-----------") loadLogInfo("collectFace,saveFaceInfo: ----------11,刷新人脸数据-----------")
runOnUiThread { recognizeViewModel.onPreviewFrame(emptyFrame, true) } runOnUiThread {
//采集完成,停止识别(出盘已在采集成功时由 outPlateForRegister 触发,此处仅负责入库)
isRecognition = false
}
loadLogInfo("---------------------------------") loadLogInfo("---------------------------------")
collectCount = 0 collectCount = 0
block() block()
@@ -875,22 +1074,22 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
private var isCollectFace = false private var isCollectFace = false
private fun leftCountDown() { private fun leftCountDown() {
CountDownUtil.startCountDown(total = 15, lifecycleScope = lifecycleScope, onStart = { leftCountDownUtil.startCountDown(total = 15, lifecycleScope = lifecycleScope, onStart = {
loadLogInfo("onPreview-----------leftCountDown-------------") loadLogInfo("onPreview-----------leftCountDown-------------")
}, onTick = { }, onTick = {
loadLogInfo("leftCountDown.onTick:$it") loadLogInfo("leftCountDown.onTick:$it")
if (facePreviewInfoList.isNullOrEmpty().not()) { if (facePreviewInfoList.isNullOrEmpty().not()) {
loadLogInfo("leftCountDown.onTick-cancel()") loadLogInfo("leftCountDown.onTick-cancel()")
CountDownUtil.cancelCountDown() leftCountDownUtil.cancelCountDown()
return@startCountDown return@startCountDown
} }
}, onFinish = { }, onFinish = {
if (facePreviewInfoList.isNullOrEmpty().not()) { if (facePreviewInfoList.isNullOrEmpty().not()) {
loadLogInfo("leftCountDown.onFinish-cancel()") loadLogInfo("leftCountDown.onFinish-人脸未离开,不跳转")
CountDownUtil.cancelCountDown()
return@startCountDown return@startCountDown
} }
CountDownUtil.cancelCountDown() // 确认用户已离开画面,重置识别失败计数,下次进入重新累计
retryFailCount = 0
goInitActivity() goInitActivity()
}) })
} }
@@ -1,946 +0,0 @@
//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
//import android.os.CountDownTimer
//import android.util.DisplayMetrics
//import android.view.View
//import android.view.ViewGroup
//import android.view.ViewTreeObserver
//import androidx.annotation.RequiresApi
//import androidx.lifecycle.Observer
//import com.arcsoft.face.ErrorInfo
//import com.google.gson.Gson
//import com.sw.inbound.utils.DateTimeUtils
//import com.sw.plate.utils.Base64
//import com.sw.plate.utils.ToastUtils
//import com.sw.plate.utils.arcface.ConfigUtil
//import com.sw.plate.utils.arcface.ErrorCodeUtil
//import com.sw.plate.utils.arcface.FaceRectTransformer
//import com.sw.plate.utils.arcface.FaceRectView
//import com.sw.plate.utils.arcface.FaceRectView.DrawInfo
//import com.sw.plate.utils.arcface.PreviewConfig
//import com.sw.plate.utils.arcface.camera.CameraListener
//import com.sw.plate.utils.arcface.camera.DualCameraHelper
//import com.sw.plate.utils.arcface.face.constants.LivenessType
//import com.sw.plate.utils.arcface.face.model.CompareResult
//import com.sw.plate.utils.arcface.face.model.FacePreviewInfo
//import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration
//import com.sw.plate.utils.arcface.facedb.FaceDatabase
//import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
//import com.sw.plate.utils.arcface.model.UserFaceInfo
//import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
//import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel.REGISTER_STATUS_READY
//import com.sw.platecabinet.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.model.response.UserFaceModel
//import com.sw.platecabinet.utils.BitmapSaver
//import com.sw.platecabinet.utils.Debouncer
//import com.sw.platecabinet.utils.LogFileUtil
//import com.sw.platecabinet.utils.PermissionHelper
//import com.sw.platecabinet.utils.PlateUtils
//import com.sw.platecabinet.utils.RegisterCallbackHandler
//import com.sw.platecabinet.utils.SoundPoolUtil
//import com.sw.platecabinet.utils.SpTool
//import org.greenrobot.eventbus.EventBus
//import org.greenrobot.eventbus.Subscribe
//import org.greenrobot.eventbus.ThreadMode
//import timber.log.Timber
//import java.io.File
//import java.util.concurrent.atomic.AtomicBoolean
//
///**
// * 人脸识别
// */
//class LoginByFaceActivityBak : BaseActivity<ActivityLoginFaceBinding>(),
// ViewTreeObserver.OnGlobalLayoutListener {
// // private val recognizeViewModel by viewModels<RecognizeViewModel>()
// private var countDownTimer: CountDownTimer? = null
//
// private val CAMERA_PERMISSION_REQUEST_CODE = 100
// private val REQUIRED_PERMISSIONS: Array<String> = arrayOf(
// Manifest.permission.CAMERA,
// Manifest.permission.WRITE_EXTERNAL_STORAGE,
// Manifest.permission.READ_EXTERNAL_STORAGE
// )
//
// override fun inflateViewBinding(): ActivityLoginFaceBinding {
// return ActivityLoginFaceBinding.inflate(layoutInflater)
// }
//
// override fun inflateTitleBinding(): ItemTitleTimeBinding {
// return binding.includeHeader
// }
//
// companion object {
// private const val TAG = "LoginByFaceActivity"
// private var instance: LoginByFaceActivityBak? = null
//
// private const val COLLECT_SUCCESS = "collect_success"
// private const val RECOGNIZE_SUCCESS = "recognize_success"
// private const val NO_PLATE_REMIND = "no_plate_remind";
//
// fun goInitActivity() {
// instance?.goInitActivity()
// }
// }
//
//// private var logList: MutableList<String> = mutableListOf()
//// private val logAdapter by lazy { LogAdapter(logList) }
//
// override fun initialize() {
// instance = this
// userViewModel.activeEngine()
//// addSocketListener()
// initCountTime()
// initArcViewModel()
// initArcView()
// openRectInfoDraw = true
// recognizeViewModel.setDrawRectInfoTextValue(true)
// //viewModel.generateToken()
//
//// binding.llToPwd.setOnClickListener {
//// val intent = Intent(this, LoginByPwdActivity::class.java)
//// startActivity(intent)
//// }
//
//// TaskManager.startTask()
//
// EventBus.getDefault().register(this)
//
//// binding.btnCollectFace.setOnClickListener { collectFace() }
//
//
//// binding.rvLogInfo.let {
//// it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, true)
//// it.adapter = logAdapter
//// }
////
//// binding.btnLogState.let {
//// it.tag = false
//// it.setImageResource(R.drawable.ic_log_show)
//// it.setOnClickListener { v ->
//// binding.rvLogInfo.run {
//// val state = it.tag.toString().toBooleanStrictOrNull() ?: false
//// if (state) visible() else gone()
//// }
//// val state = it.tag.toString().toBooleanStrictOrNull() ?: false
//// it.tag = state
//// it.setImageResource(
//// if (state) R.drawable.ic_log_hide else R.drawable.ic_log_show
//// )
//// }
//// }
//
// //开启人脸增量数据定时任务
// startFaceTask {
// loadLogInfo("collectFace增量接口数据用户id:${Gson().toJson(it)}")
// }
//
// val soundMap = hashMapOf<String, Int>()
// soundMap[COLLECT_SUCCESS] = R.raw.collect_success
// soundMap[RECOGNIZE_SUCCESS] = R.raw.recognize_success
// soundMap[NO_PLATE_REMIND] = R.raw.no_plate_remind
// SoundPoolUtil.getInstance().loadR(this, soundMap);
// }
//
//
// private fun checkCameraPermission() {
// Timber.i("checkCameraPermission")
// val permissionHelper =
// PermissionHelper.with(this, REQUIRED_PERMISSIONS, CAMERA_PERMISSION_REQUEST_CODE)
// .onGranted {
// loadLogInfo("checkCameraPermission 权限已允许")
// // 权限已授予
// recognizeViewModel.init()
// initRgbCamera()
// resumeCamera()
// }
// .build()
// registerPermissionHelper(permissionHelper)
// permissionHelper.checkAndRequest()
// }
//
// @Subscribe(threadMode = ThreadMode.MAIN)
// fun getFaceEntity(insertEntity: FaceEntity) {
// Timber.tag("performSync").e("-time=%s", insertEntity.registerTime)
// recognizeViewModel.addFace(insertEntity)
// Timber.tag("performSync over").e("-time=%s", insertEntity.registerTime)
// }
//
// override fun onLeftDoubleClick() {
// finish()
// }
//
// private var isRecognition = false
// private var rgbCameraHelper: DualCameraHelper? = null
// private var rgbFaceRectTransformer: FaceRectTransformer? = null
// private val livenessType = LivenessType.RGB
// private var openRectInfoDraw = false
// private val regDebouncer = Debouncer(3000)
// private var registerHandler: RegisterCallbackHandler? = null
//
// private fun initArcViewModel() {
// recognizeViewModel.setLiveType(livenessType)
// recognizeViewModel.prepareRegister()
// recognizeViewModel.setOnRegisterFinishedCallback { facePreviewInfo, userFaceInfo ->
// registerHandler?.setCallback {
// if (!isCollectFace) {
// return@setCallback
// }
// if (this.userFaceInfo == null && userFaceInfo != null) {
// recognizeViewModel.updateRegisterStatus(RecognizeViewModel.REGISTER_STATUS_DONE)
// this.userFaceInfo = userFaceInfo
// getFaceDataBlock { faceData, headBmp ->
// isCollecting.set(false)
// isCollectFace = false
// sendFaceData(faceData, headBmp)
// }
// }
// }
// }
// recognizeViewModel.ftInitCode.observe(this, Observer { ftInitCode: Int? ->
// if (ftInitCode != ErrorInfo.MOK) {
// val error: String? = getString(
// R.string.specific_engine_init_failed, "ftEngine",
// ftInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(ftInitCode!!)
// )
// Timber.e("ftInitCode observe = $error")
// ToastUtils.showToast(error)
// }
// })
// recognizeViewModel.frInitCode.observe(this, Observer { frInitCode: Int? ->
// if (frInitCode != ErrorInfo.MOK) {
// val error: String? = getString(
// R.string.specific_engine_init_failed, "frEngine",
// frInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(frInitCode!!)
// )
// Timber.e("frInitCode observe = $error")
// ToastUtils.showToast(error)
// }
// })
// recognizeViewModel.flInitCode.observe(this, Observer { flInitCode: Int? ->
// if (flInitCode != ErrorInfo.MOK) {
// val error: String? = getString(
// R.string.specific_engine_init_failed, "flEngine",
// flInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(flInitCode!!)
// )
// Timber.e("flInitCode observe = $error")
// ToastUtils.showToast(error)
// }
// })
//
// recognizeViewModel.recognizeConfiguration
// .observe(this, Observer { recognizeConfiguration: RecognizeConfiguration? ->
// Timber.i("recognizeConfiguration: observe = ${recognizeConfiguration.toString()}")
// })
// recognizeViewModel.recognizeNotice.observe(this, Observer { notice: String? ->
// Timber.i("recognizeNotice observe notice = $notice")
// })
//
// recognizeViewModel.recognizeUserId.observe(this, Observer { result: CompareResult? ->
// loadLogInfo("collectFace,recognizeUserId observe userId = ${result?.faceEntity?.userName}")
// if (faceUseSwitch) {
// loadFaceRecognizeResult2(result)
// return@Observer
// }
// loadFaceRecognizeResult(result)
// })
//
// recognizeViewModel.drawRectInfoText.observe(this, Observer { info ->
// Timber.i("drawRectInfoText observe info = $info")
// })
// }
//
// /**
// * 人脸识别开关,true-loadFaceRecognizeResult2false-loadFaceRecognizeResult
// */
// private var faceUseSwitch = true
//
// /**
// * 识别失败重试次数
// */
// private var retryFailCount = 0
//
// /**
// * 识别失败最大次数
// */
// private val maxFailCount = 5
//
// /**
// * 人脸失败阈值
// */
// private val faceFailThreshold = 0.3
//
// /**
// * 配置阈值,默认值为0.8
// */
// private val faceSuccessThreshold by lazy { ConfigUtil.getRecognizeThreshold(this) }
//
//
// private val openPlateDebouncer = Debouncer(5000)
// private val isCollecting = AtomicBoolean(false)
//
// /**
// * 根据相似度similar>设定阈值0.8作为识别成功判断,
// * 如果similar<0.3作为识别失败判断,
// * 如果similar>=0.3&&similar<=0.8时,暂时不作为失败情况,再识别5次如果similar<0.8则作为识别失败处理
// */
// private fun loadFaceRecognizeResult2(result: CompareResult?) {
// // 无有效结果直接忽略
// val similar = result?.similar ?: return
//
// binding.tvSimilarValue.text = "$similar${retryFailCount}次"
// loadLogInfo(
// "collectFace,recognizeUserId 阈值 success=$faceSuccessThreshold, " +
// "fail=$faceFailThreshold, similar=$similar, retry=$retryFailCount"
// )
//
// if (similar >= faceSuccessThreshold) {
// val userId = result.faceEntity?.userName
// if (userId.isNullOrBlank()) {
// loadLogInfo("collectFace,recognizeUserId 识别成功但 userId 为空,忽略")
// return
// }
//
// loadLogInfo(
// "collectFace,recognizeUserId 识别成功 userId=$userId, time=${DateTimeUtils.getDateTimeString()}"
// )
//
// // 成功后必须重置状态
// retryFailCount = 0
// isCollecting.set(false)
// isCollectFace = false
// currentUserId = userId
//
// // 防抖,防止重复开闸
// openPlateDebouncer.debounce {
// openPlate(userId)
// }
// return
// }
//
// if (isCollectFace) {
// loadLogInfo("collectFace,recognizeUserId 正在采集中,忽略失败结果 similar=$similar")
// return
// }
//
// if (similar <= faceFailThreshold || retryFailCount >= maxFailCount) {
// loadLogInfo(
// "collectFace,recognizeUserId 识别失败,开始采集 similar=$similar, " +
// "retry=$retryFailCount, time=${DateTimeUtils.getDateTimeString()}"
// )
// if (!isCollecting.compareAndSet(false, true)) {
// loadLogInfo("collectFace 已在采集中,忽略重复触发")
// return
// }
//
// retryFailCount = 0
// isCollectFace = true
// collectFace()//采集人脸
// return
// }
//
// retryFailCount++
// loadLogInfo("collectFace,recognizeUserId 中间态重试,retryFailCount=$retryFailCount")
// }
//
//// private fun loadFaceRecognizeResult2(result: CompareResult?) {
////
//// if(isCollectFace){
//// return
//// }
////
//// val similar = result?.similar ?: 0f
//// binding.tvSimilarValue.text = "$similar${retryFailCount}次"
//// loadLogInfo("collectFace,recognizeUserId 人脸识别配置阈值为:${faceSuccessThreshold},失败阈值为:${faceFailThreshold},相似度为:${similar},失败次数为:${retryFailCount}")
//// if (similar > faceSuccessThreshold) {
//// loadLogInfo("collectFace,recognizeUserId 相似度为:${similar},识别成功时间:${DateTimeUtils.getDateTimeString()}")
//// //大 于识别阈值,作为识别成功判断
//// currentUserId = result?.faceEntity?.userName
//// Debouncer(5000).debounce { openPlate(currentUserId) }
//// return
//// }
//// loadLogInfo("collectFace,recognizeUserId 人脸识别相似度为:${similar},失败次数为:${retryFailCount}")
//// if (similar < faceFailThreshold || retryFailCount >= maxFailCount) {
//// //相似度低于识别错误阈值进行采集、识别失败次数大于等于最大失败次数,作为识别失败判断
//// if (isCollectFace.not()) {
//// //非采集中
//// loadLogInfo("collectFace,recognizeUserId 未识别到人脸信息,相似度为:${similar},开始采集时间:${DateTimeUtils.getDateTimeString()}")
//// isCollectFace = true
//// collectFace()
//// return
//// }
//// }
//// //满足similar >= faceFailThreshold && similar <= successThreshold时进行重试
//// retryFailCount++
//// loadLogInfo("collectFace,recognizeUserId 人脸识别失败次数为:${retryFailCount}")
//// }
//
// /**
// * 根据相似度similar>设定阈值0.8作为识别是否成功来加载人脸识别结果
// */
// private fun loadFaceRecognizeResult(result: CompareResult?) {
// val similar = result?.similar ?: 0f
// binding.tvSimilarValue.text = "$similar"
// val similarPass = result?.isSimilarPass ?: false
// if (similarPass.not()) {
// loadLogInfo("collectFace,recognizeUserId 未识别到人脸信息,开始采集")
// isCollectFace = true
// collectFace()
// return
// }
// currentUserId = result?.faceEntity?.userName
// if (tempUserId != currentUserId) {
// SoundPoolUtil.getInstance().play("success", 0)
// }
//// plateEnable = true
// Debouncer(5000).debounce { openPlate(currentUserId) }
// }
//
// /**
// * 查询餐盘数据
// */
// private fun getPlateData(block: (List<EquipmentUserInfo>) -> Unit) {
// MyApp.plateList?.let {
// block(it)
// }
// }
//
// private var currentPlateInfo: EquipmentUserInfo? = null
// private var plateEnable = false
//
// /**
// * 打开餐盘柜
// */
// private fun openPlate(userId: String?) {
// if (plateEnable.not()) return
// loadLogInfo("collectFace,openPlate: userId=$userId")
// getPlateData { items ->
// loadLogInfo("collectFace,openPlate: items:${Gson().toJson(items)}")
// // 未查询到当前用户,查询第一个未绑定的餐盘
// val item = items.firstOrNull { it.plateNumber.isNullOrEmpty().not() }
// loadLogInfo("collectFace,openPlate: item2:${item}")
// if (item == null) {
// //暂无餐盘
// loadLogInfo("collectFace,openPlate: 暂无餐盘,请联系管理人员,isCollectFace=$isCollectFace")
// //ToastUtils.showToast("暂无餐盘,请联系管理人员")
// SoundPoolUtil.getInstance().play(NO_PLATE_REMIND, 0)
// goInitActivity()
// return@getPlateData
// }
//// if (tempUserId != currentUserId) {
// SoundPoolUtil.getInstance().play(RECOGNIZE_SUCCESS, 0)
//// } else {
//// SoundPoolUtil.getInstance().play(COLLECT_SUCCESS, 0)
//// }
// loadLogInfo("collectFace,openPlate: item3:${item}")
// item.faceId = userId
// item.updateTime = DateTimeUtils.getDateTimeString()
// item.plateNumber = null
// plateEnable = false
// Debouncer(5000).debounce {
// //打开柜子
// PlateUtils.open(this, item.equipmentBoxCode, false)
// }
// currentPlateInfo = item
// MyApp.plateList = items
// SpTool.savePlateData(items)
// }
// }
//
//// private val debouncer by lazy { Debouncer(3000) }
//
// private fun initArcView() {
// //在布局结束后才做初始化操作
// binding.dualCameraTexturePreviewRgb.getViewTreeObserver().addOnGlobalLayoutListener(this)
// recognizeViewModel.getCompareResultList().getValue()
// }
//
// override fun onDestroy() {
// loadLogInfo("onDestroy")
// instance = null
// if (rgbCameraHelper != null) {
// rgbCameraHelper!!.release()
// rgbCameraHelper = null
// }
//
// recognizeViewModel.destroy()
//
// EventBus.getDefault().unregister(this)
// super.onDestroy()
// }
//
//
// /**
// * 调整View的宽高,使预览显示正常且采集框固定为380x380
// *
// * @param rgbPreview RGB预览View
// * @param previewView 显示预览数据的view
// * @param faceRectView 画框的view (380x380)
// * @param previewSize 预览大小
// * @param displayOrientation 相机旋转角度
// * @param scale 缩放比例
// * @return 调整后的LayoutParams
// */
// private fun adjustPreviewViewSize(
// rgbPreview: View,
// previewView: View,
// faceRectView: FaceRectView,
// previewSize: Camera.Size,
// displayOrientation: Int,
// scale: Float
// ): ViewGroup.LayoutParams {
// val layoutParams = previewView.layoutParams
// val measuredWidth = previewView.measuredWidth
// val measuredHeight = previewView.measuredHeight
// var ratio = (previewSize.height.toFloat()) / previewSize.width.toFloat()
// if (ratio > 1) {
// ratio = 1 / ratio
// }
// if (displayOrientation % 180 == 0) {
// layoutParams.width = measuredWidth
// layoutParams.height = (measuredWidth * ratio).toInt()
// } else {
// layoutParams.height = measuredHeight
// layoutParams.width = (measuredHeight * ratio).toInt()
// }
// if (scale < 1f) {
// val rgbParam = rgbPreview.getLayoutParams()
// layoutParams.width = (rgbParam.width * scale).toInt()
// layoutParams.height = (rgbParam.height * scale).toInt()
// } else {
// layoutParams.width = (layoutParams.width * scale).toInt()
// layoutParams.height = (layoutParams.height * scale).toInt()
// }
//
// val metrics = DisplayMetrics()
// windowManager.defaultDisplay.getMetrics(metrics)
//
// if (layoutParams.width >= metrics.widthPixels) {
// val viewRatio = layoutParams.width / (metrics.widthPixels.toFloat())
// layoutParams.width = (layoutParams.width / viewRatio).toInt()
// layoutParams.height = (layoutParams.height / viewRatio).toInt()
// }
// if (layoutParams.height >= metrics.heightPixels) {
// val viewRatio = layoutParams.height / (metrics.heightPixels.toFloat())
// layoutParams.width = (layoutParams.width / viewRatio).toInt()
// layoutParams.height = (layoutParams.height / viewRatio).toInt()
// }
//
// previewView.setLayoutParams(layoutParams)
// faceRectView.setLayoutParams(layoutParams)
// return layoutParams
// }
//
// private fun initRgbCamera() {
// val cameraListener: CameraListener = object : CameraListener {
// override fun onCameraOpened(
// camera: Camera,
// cameraId: Int,
// displayOrientation: Int,
// isMirror: Boolean
// ) {
// loadLogInfo("initRgbCamera Rgb onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation")
// runOnUiThread({
// val previewSizeRgb = camera.getParameters().getPreviewSize()
// val layoutParams = adjustPreviewViewSize(
// binding.dualCameraTexturePreviewRgb,
// binding.dualCameraTexturePreviewRgb, binding.dualCameraFaceRectView,
// previewSizeRgb, displayOrientation, 0.6f
// )
// loadLogInfo("initRgbCamera previewSizeRgb = ${previewSizeRgb.width}-${previewSizeRgb.height}")
// loadLogInfo("initRgbCamera layoutParams = ${layoutParams.width}-${layoutParams.height}")
// loadLogInfo(
// "initRgbCamera isMirror = ${isMirror}, isDrawRgbRectHorizontalMirror = ${
// ConfigUtil.isDrawRgbRectHorizontalMirror(
// context
// )
// }, isDrawRgbRectVerticalMirror = ${
// ConfigUtil.isDrawRgbRectVerticalMirror(
// context
// )
// }"
// )
// rgbFaceRectTransformer = FaceRectTransformer(
// previewSizeRgb.width,
// previewSizeRgb.height,
// layoutParams.width,
// layoutParams.height,
// 90,
// cameraId,
// isMirror,
// true,
// true
// )
//
// recognizeViewModel.onRgbCameraOpened(camera)
// recognizeViewModel.setRgbFaceRectTransformer(rgbFaceRectTransformer)
// })
// }
//
// @RequiresApi(api = Build.VERSION_CODES.Q)
// override fun onPreview(nv21: ByteArray?, camera: Camera?) {
// if (!isRecognition) {
// return
// }
// binding.dualCameraFaceRectView.clearFaceInfo()
// facePreviewInfoList = recognizeViewModel.onPreviewFrame(nv21, true)
// if (facePreviewInfoList != null && rgbFaceRectTransformer != null) {
// drawPreviewInfo(facePreviewInfoList)
// }
// if (facePreviewInfoList.isNullOrEmpty()) {
// currentPlateInfo = null
// currentUserId = null
// userFaceInfo = null
// }
// recognizeViewModel.clearLeftFace(facePreviewInfoList)
// }
//
// override fun onCameraClosed() {
// Timber.i("initRgbCamera onCameraClosed: ")
// }
//
// override fun onCameraError(e: java.lang.Exception) {
// Timber.i("initRgbCamera onCameraError: %s", e.message)
// e.printStackTrace()
// }
//
// override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) {
// Timber.i("initRgbCamera onCameraConfigurationChanged: threadName = ${Thread.currentThread().name}")
// if (rgbFaceRectTransformer != null) {
// rgbFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation
// }
// Timber.i("initRgbCamera onCameraConfigurationChanged: $cameraID $displayOrientation")
// }
// }
//
// val previewConfig: PreviewConfig = recognizeViewModel.previewConfig
// rgbCameraHelper = DualCameraHelper.Builder()
// .previewViewSize(
// Point(
// binding.dualCameraTexturePreviewRgb.measuredWidth,
// binding.dualCameraTexturePreviewRgb.measuredHeight
// )
// )
// .rotation(windowManager.defaultDisplay.rotation)
// .additionalRotation(0) // 角度
// .previewSize(recognizeViewModel.loadPreviewSize())
// .specificCameraId(previewConfig.rgbCameraId)
// .isMirror(true)
// .previewOn(binding.dualCameraTexturePreviewRgb)
// .cameraListener(cameraListener)
// .build()
// rgbCameraHelper!!.init()
// rgbCameraHelper!!.start()
// }
//
// /**
// * 绘制RGB、IR画面的实时人脸信息
// *
// * @param facePreviewInfoList RGB画面的实时人脸信息
// */
// private fun drawPreviewInfo(facePreviewInfoList: MutableList<FacePreviewInfo>?) {
// if (rgbFaceRectTransformer != null) {
// val rgbDrawInfoList: MutableList<DrawInfo>? = recognizeViewModel.getDrawInfo(
// facePreviewInfoList, LivenessType.RGB, openRectInfoDraw
// )
// // 识别成功
// binding.dualCameraFaceRectView.drawRealtimeFaceInfo(rgbDrawInfoList)
// }
// }
//
// fun openRectInfoDraw(view: View?) {
// openRectInfoDraw = !openRectInfoDraw
// recognizeViewModel.setDrawRectInfoTextValue(openRectInfoDraw)
// }
//
//
// override fun onGlobalLayout() {
// loadLogInfo("onGlobalLayout")
// binding.dualCameraTexturePreviewRgb.getViewTreeObserver().removeOnGlobalLayoutListener(this)
// // 请求权限
// checkCameraPermission()
// }
//
// private fun resumeCamera() {
// isRecognition = true
// if (rgbCameraHelper != null && rgbCameraHelper!!.isStopped) {
// loadLogInfo("collectFace,resumeCamera: ")
// rgbCameraHelper!!.start()
// } else {
// recognizeViewModel.onPreviewFrame(ByteArray(1382400), true)
// }
// }
//
// /**
// * 临时用户id
// */
// private var tempUserId: String = ""
// override fun onResume() {
// super.onResume()
// registerHandler = RegisterCallbackHandler()
// try {
// if (isFirstOpen) {
// goInitActivity()
// binding.root.postDelayed({
// isFirstOpen = false
// }, 1000)
// return
// }
// retryFailCount = 0
// binding.tvSimilarValue.text = ""
// isCollecting.set(false)
// isCollectFace = false
// plateEnable = true
// collectCount = 0
// tempUserId = "${System.currentTimeMillis()}"
// resumeCamera()
// userViewModel.resetUserInfo()
// countDownTimer?.let {
// it.cancel()
// it.start()
// }
//// initFaceScheduler()
//// checkFaceState()
// } catch (e: Exception) {
// e.printStackTrace()
// }
// }
//
// private var isFirstOpen = true
//
// protected override fun onPause() {
// pauseCamera()
// super.onPause()
// countDownTimer?.cancel()
// }
//
// private fun pauseCamera() {
// isRecognition = false
// recognizeViewModel.onPreviewFrame(ByteArray(1382400), true)
// }
//
// override fun handleLoginSuccess(equipmentUserInfo: EquipmentUserInfo, isAdmin: Boolean) {
// super.handleLoginSuccess(equipmentUserInfo, isAdmin)
//// goInitActivity()
// }
//
// private val totalTimeInMillis = 15 * 1000L
// fun initCountTime() {
// countDownTimer = object : CountDownTimer(totalTimeInMillis, 1000) {
// override fun onTick(millisUntilFinished: Long) {
// loadLogInfo("initCountTime onTick = ${(millisUntilFinished / 1000).toInt()}")
// binding.tvToInit.text = "${(millisUntilFinished / 1000).toInt() + 1}"
// }
//
// override fun onFinish() {
// goInitActivity()
// }
// }
// }
//
// fun goInitActivity() {
// val intent = Intent(context, InitActivity::class.java)
// startActivity(intent)
// }
//
// /**
// * 采集人脸
// */
// private fun collectFace() {
// loadLogInfo("collectFace: -----------1,开始采集人脸,开始时间:${DateTimeUtils.getDateTimeString()}----------")
// if (isFirstOpen) return
// loadLogInfo("collectFace: ----------2,isFirstOpen=false-----------")
// loadLogInfo("collectFace: ----------4,检测到人脸信息-----------")
// currentUserId = null
// userFaceInfo = null
// recognizeViewModel.updateRegisterStatus(REGISTER_STATUS_READY)
// loadLogInfo("collectFace: ----------5,人脸采集中-----------")
// }
//
// private val uploadFaceDebouncer = Debouncer(10000)
//
// /**
// * 改为上传人脸数据,不需要发生人脸数据到收银端
// */
// private fun sendFaceData(faceData: String, headBmp: Bitmap) {
// loadLogInfo("collectFace,sendFaceData: ----------11,发送人脸数据-----------")
//
// val imageFile = BitmapSaver.saveToAppFilesDir(
// headBmp, this, "IMG_CROP_${System.currentTimeMillis()}.jpg"
// )
// loadLogInfo("collectFace,sendFaceData:${imageFile?.absolutePath}")
// if (imageFile == null) {
// ToastUtils.showToast("获取人脸照片失败")
// return
// }
// uploadFaceDebouncer.debounce {
// addUserFace(faceData, imageFile)
// }
//// // 给某个客户端发送
//// val jsonObject = JSONObject().also {
//// it.put("type", "faceFeature")
//// it.put("content", faceData)
//// }
//// tcpClient?.send(jsonObject)
// }
//
// private fun addUserFace(faceData: String, imageFile: File) {
// 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,发送人脸数据成功但接口返回为nullfaceData=${faceData}")
// return@addUserFace
// }
// Thread {
// saveFaceInfo(faceModel)
// uploadFaceDebouncer.reset()
// }.start()
// }
// }
//
// private fun getFaceDataBlock(block: (String, Bitmap) -> Unit) {
// if (userFaceInfo == null || userFaceInfo?.faceFeature?.featureData == null) {
// return
// }
// val faceData: String? = Base64.encode(userFaceInfo?.faceFeature?.featureData)
// if (faceData.isNullOrBlank()) {
// return
// }
// loadLogInfo("collectFace,getFaceData: ----------7,拿到人脸提取特征数据时间:${DateTimeUtils.getDateTimeString()}-----------")
// block(faceData, userFaceInfo!!.headBmp)
// }
//
// /**
// * 加载日志
// * @param msg 日志内容
// */
// private fun loadLogInfo(msg: String) {
// Timber.tag(TAG).d(msg)
// LogFileUtil.saveLog(msg)
// }
//
// private var facePreviewInfoList: MutableList<FacePreviewInfo>? = null
// private var userFaceInfo: UserFaceInfo? = null
//
// private var currentUserId: String? = null
//
// private fun saveFaceInfo(faceModel: UserFaceModel) {
// try {
// loadLogInfo("collectFace,saveFaceInfo: ----------8,保存人脸取特征数据-----------")
// val faceDao = FaceDatabase.getInstance(this).faceDao()
// val faceEntity = FaceEntity(
// faceModel.userId,
// null,
// Base64.decode(faceModel.faceFeatureStr)
// ).also {
// //1-会员、2-临时用户
// it.userType = if (faceModel.isMember) "1" else "2"
// }
// faceDao.insert(faceEntity)
// loadLogInfo("collectFace,saveFaceInfo: ----------10,人脸数据保存完成-----------")
// recognizeViewModel.refreshFaceList()
// loadLogInfo("collectFace,saveFaceInfo: ----------11,刷新人脸数据-----------")
// runOnUiThread { recognizeViewModel.onPreviewFrame(ByteArray(1382400), true) }
// loadLogInfo("---------------------------------")
// collectCount = 0
// } catch (e: Exception) {
// e.printStackTrace()
// loadLogInfo("collectFace,saveFaceInfo: ----------13,人脸采集异常:${e.message}-----------")
// collectCount++
// loadLogInfo("collectFace,saveFaceInfo: ----------14,${collectCount}次人脸采集异常:${e.message}-----------")
// ToastUtils.showToast("人脸采集异常:${e.message}")
// }
// }
//
// private var collectCount = 0
//
//// private var 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 {
// val faceDao = FaceDatabase.getInstance(this).faceDao()
// faceDao.deleteTempUserFaceData()
// recognizeViewModel.refreshFaceList()
// loadLogInfo("collectFace,TcpClientListener收到消息,已删除并刷新人脸数据")
// }.start()
// }
//
// private var isCollectFace = false
//
//}
@@ -1,62 +0,0 @@
//package com.sw.platecabinet.activity
//
//import android.content.Intent
//import com.sw.plate.utils.ToastUtils
//import com.sw.platecabinet.databinding.ActivityLoginByPwdBinding
//import com.sw.platecabinet.databinding.ItemTitleTimeBinding
//import com.sw.platecabinet.dialog.BalanceNotEnoughDialog
//import com.sw.platecabinet.model.request.LoginParam
//import com.sw.platecabinet.model.response.EquipmentUserInfo
//import com.sw.platecabinet.utils.KeyboardUtils
//
///**
// * 密码登录
// */
//class LoginByPwdActivity : BaseActivity<ActivityLoginByPwdBinding>() {
//
// override fun inflateViewBinding(): ActivityLoginByPwdBinding {
// return ActivityLoginByPwdBinding.inflate(layoutInflater)
// }
//
// override fun inflateTitleBinding(): ItemTitleTimeBinding {
// return binding.includeHeader
// }
//
// override fun initialize() {
// binding.btnLogin.setOnClickListener {
// val phone = binding.etPhone.text.toString()
// val pwd = binding.etPwd.text.toString()
// if (phone.isEmpty() || pwd.isEmpty()) {
// ToastUtils.showToast("手机或校验码不能为空")
// return@setOnClickListener
// }
// val loginParam = LoginParam(
// //equipmentId = equipmentId,
// phone = phone,
// password = pwd
// )
// viewModel.loginWithPwd(loginParam)
// KeyboardUtils.hideKeyboard(this)
// }
// binding.tvFaceRec.setOnClickListener {
// val intent = Intent(this, LoginByFaceActivity::class.java)
// startActivity(intent)
// }
// }
//
// override fun onResume() {
// super.onResume()
// viewModel.resetUserInfo()
// }
//
// override fun handleLoginSuccess(equipmentUserInfo: EquipmentUserInfo, isAdmin: Boolean) {
// super.handleLoginSuccess(equipmentUserInfo, isAdmin)
//// val cardBalance = equipmentUserInfo.cardBalance?:0.toDouble()
//// val balanceIsNotEnough = cardBalance <= 0.toDouble()
// if (equipmentUserInfo.isIntercept) {
// return
// }
// LoginByFaceActivity.goInitActivity()
// finish()
// }
//}
@@ -2,14 +2,11 @@ package com.sw.platecabinet.activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
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.utils.FragmentHelper import com.sw.platecabinet.utils.FragmentHelper
typealias Callback = (String) -> Unit typealias Callback = (String) -> Unit
@@ -65,7 +62,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
pageType = param?.let { PageType.valueOf(param) } ?: PageType.SETTING_LIST pageType = param?.let { PageType.valueOf(param) } ?: PageType.SETTING_LIST
fragmentHelper = FragmentHelper(supportFragmentManager, R.id.fragment_container) fragmentHelper = FragmentHelper(supportFragmentManager, R.id.fragment_container)
val page = when (pageType) { val page = when (pageType) {
PageType.SETTING_LIST -> SettingListFragment() // PageType.SETTING_LIST -> SettingListFragment()
// PageType.BIND_PLATE -> BindPlateFragment.newInstance(equipmentUserInfo) // PageType.BIND_PLATE -> BindPlateFragment.newInstance(equipmentUserInfo)
// PageType.PLATE_TIP -> PlateOpenFragment.newInstance(equipmentUserInfo, isAdmin) // PageType.PLATE_TIP -> PlateOpenFragment.newInstance(equipmentUserInfo, isAdmin)
PageType.PLATE_TIP -> PlateOpenFragment.newInstance(isScanCode) PageType.PLATE_TIP -> PlateOpenFragment.newInstance(isScanCode)
@@ -86,7 +83,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
override fun onRightDoubleClick() { override fun onRightDoubleClick() {
if (pageType == PageType.SETTING_LIST) { if (pageType == PageType.SETTING_LIST) {
startActivity(Intent(this, FunActivity::class.java)) // startActivity(Intent(this, FunActivity::class.java))
} }
} }
@@ -0,0 +1,388 @@
package com.sw.platecabinet.activity
import android.content.Context
import android.content.Intent
import android.view.inputmethod.InputMethodManager
import androidx.lifecycle.lifecycleScope
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.facedb.FaceDatabase
import com.sw.plate.utils.arcface.faceserver.FaceServer
import com.sw.platecabinet.databinding.ActivityPlcSettingBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.dialog.EnvSwitchDialog
import com.sw.platecabinet.ext.clickWithDebounce
import com.sw.platecabinet.utils.mego.PlcCallback
import com.sw.platecabinet.utils.mego.PlcHelper
import com.sw.platecabinet.utils.mego.PlcStatus
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* PLC 控制设置界面
* 通过右侧时间双击(onRightDoubleClick)打开,提供全套 PLC 控制操作入口
*/
class PlcSettingActivity : BaseActivity<ActivityPlcSettingBinding>() {
companion object {
/** Intent Extra 键:是否显示 tips 界面 */
private const val EXTRA_SHOW_TIPS = "extra_show_tips"
/**
* 启动 PlcSettingActivity
* @param showTips 是否显示 tips 界面(暂无餐盘提示)
*/
fun start(context: Context, showTips: Boolean = false) {
val intent = Intent(context, PlcSettingActivity::class.java)
intent.putExtra(EXTRA_SHOW_TIPS, showTips)
context.startActivity(intent)
}
}
override fun inflateViewBinding(): ActivityPlcSettingBinding {
return ActivityPlcSettingBinding.inflate(layoutInflater)
}
override fun inflateTitleBinding(): ItemTitleTimeBinding {
return binding.includeHeader
}
override fun initialize() {
// 根据传入参数控制 tips 界面显示
val showTips = intent.getBooleanExtra(EXTRA_SHOW_TIPS, false)
binding.layoutTips.visibility =
if (showTips) android.view.View.VISIBLE else android.view.View.GONE
initConnectButtons()
initOutPlateButton()
initPlatformButtons()
initPlatformResetButton()
initAlarmButton()
initParamButtons()
initStatusButton()
initFaceCount()
binding.btnSwitchEnv.setOnClickListener {
EnvSwitchDialog(context = this, onEnvChanged = {
showWaitingDialog("重置人脸数据中……")
lifecycleScope.launch {
withContext(Dispatchers.IO) {
FaceServer.getInstance().clearAllFaces()
}
}
netViewModelV2.getUserFaceCache(pageNo = 1, callback = { state, errMsg ->
hideWaitingDialog()
if (state) {
recognizeViewModel.refreshFaceList()
ToastUtils.showToast("人脸数据已更新")
} else {
ToastUtils.showToast(errMsg)
}
})
}).show()
}
binding.tvTips.clickWithDebounce {
binding.layoutTips.visibility = android.view.View.GONE
}
}
// ==================== 连接控制 ====================
/**
* 初始化连接控制区按钮:打开串口、打开自动模式
*/
private fun initConnectButtons() {
binding.btnOpenSerialPort.clickWithDebounce {
showWaitingDialog("正在打开串口……")
PlcHelper.getInstance(this).openSerialPort(object : PlcCallback<String> {
override fun onSuccess(result: String) {
hideWaitingDialog()
ToastUtils.showToast("打开串口成功:$result")
}
override fun onError(message: String) {
hideWaitingDialog()
ToastUtils.showToast("打开串口失败:$message")
}
})
}
binding.btnOpenAutoMode.clickWithDebounce {
showWaitingDialog("正在打开自动模式……")
PlcHelper.getInstance(this).openAutoMode(object : PlcCallback<String> {
override fun onSuccess(result: String) {
hideWaitingDialog()
ToastUtils.showToast("打开自动模式成功:$result")
}
override fun onError(message: String) {
hideWaitingDialog()
ToastUtils.showToast("打开自动模式失败:$message")
}
})
}
}
// ==================== 出盘操作 ====================
/**
* 初始化出盘操作按钮
*/
private fun initOutPlateButton() {
binding.btnOutPlate.clickWithDebounce {
showWaitingDialog("正在出盘……")
PlcHelper.getInstance(this).outPlate(object : PlcCallback<String> {
override fun onSuccess(result: String) {
hideWaitingDialog()
ToastUtils.showToast("出盘成功:$result")
}
override fun onError(message: String) {
hideWaitingDialog()
ToastUtils.showToast("出盘失败:$message")
}
})
}
}
// ==================== 平台控制 ====================
/**
* 初始化平台控制区按钮:平台下降、平台上升、返回营业
*/
private fun initPlatformButtons() {
binding.btnPlatformDown.clickWithDebounce {
showWaitingDialog("正在执行平台下降……")
PlcHelper.getInstance(this).platformDown(object : PlcCallback<String> {
override fun onSuccess(result: String) {
hideWaitingDialog()
ToastUtils.showToast("平台下降成功:$result")
}
override fun onError(message: String) {
hideWaitingDialog()
ToastUtils.showToast("平台下降失败:$message")
}
})
}
binding.btnPlatformUp.clickWithDebounce {
showWaitingDialog("正在执行平台上升……")
PlcHelper.getInstance(this).platformUp(object : PlcCallback<String> {
override fun onSuccess(result: String) {
hideWaitingDialog()
ToastUtils.showToast("平台上升成功:$result")
}
override fun onError(message: String) {
hideWaitingDialog()
ToastUtils.showToast("平台上升失败:$message")
}
})
}
binding.btnBackToBusiness.clickWithDebounce {
showWaitingDialog("正在返回营业状态……")
PlcHelper.getInstance(this).backToBusiness(object : PlcCallback<String> {
override fun onSuccess(result: String) {
hideWaitingDialog()
ToastUtils.showToast("返回营业成功:$result")
finish()
}
override fun onError(message: String) {
hideWaitingDialog()
ToastUtils.showToast("返回营业失败:$message")
}
})
}
}
// ==================== 故障处理 ====================
/**
* 初始化平台复位按钮:先执行平台上升,成功后自动执行返回营业
*/
private fun initPlatformResetButton() {
binding.btnPlatformReset.clickWithDebounce {
showWaitingDialog("正在复位平台……")
PlcHelper.getInstance(this).platformUp(object : PlcCallback<String> {
override fun onSuccess(result: String) {
// 平台上升完成,继续执行返回营业
PlcHelper.getInstance(this@PlcSettingActivity)
.backToBusiness(object : PlcCallback<String> {
override fun onSuccess(result: String) {
hideWaitingDialog()
ToastUtils.showToast("平台复位成功")
finish()
}
override fun onError(message: String) {
hideWaitingDialog()
ToastUtils.showToast("返回营业失败:$message")
}
})
}
override fun onError(message: String) {
hideWaitingDialog()
ToastUtils.showToast("平台上升失败:$message")
}
})
}
}
/**
* 初始化故障复位按钮
*/
private fun initAlarmButton() {
binding.btnAlarmReset.clickWithDebounce {
showWaitingDialog("正在故障复位……")
PlcHelper.getInstance(this).alarmReset(object : PlcCallback<String> {
override fun onSuccess(result: String) {
hideWaitingDialog()
ToastUtils.showToast("故障复位成功:$result")
}
override fun onError(message: String) {
hideWaitingDialog()
ToastUtils.showToast("故障复位失败:$message")
}
})
}
}
// ==================== 参数设置 ====================
/**
* 初始化参数设置按钮
* 读取参数类型和参数值输入框内容,调用 setParams
*/
private fun initParamButtons() {
binding.btnSetParams.clickWithDebounce {
val paramType = binding.etParamType.text.toString().trim()
val paramValueStr = binding.etParamValue.text.toString().trim()
if (paramType.isEmpty()) {
ToastUtils.showToast("请输入参数类型")
return@clickWithDebounce
}
if (paramValueStr.isEmpty()) {
ToastUtils.showToast("请输入参数值")
return@clickWithDebounce
}
val paramValue = paramValueStr.toIntOrNull()
if (paramValue == null) {
ToastUtils.showToast("参数值须为整数")
return@clickWithDebounce
}
// 隐藏键盘
hideKeyboard()
showWaitingDialog("正在设置参数……")
PlcHelper.getInstance(this).setParams(
paramType,
paramValue,
object : PlcCallback<String> {
override fun onSuccess(result: String) {
hideWaitingDialog()
ToastUtils.showToast("参数设置成功:$result")
}
override fun onError(message: String) {
hideWaitingDialog()
ToastUtils.showToast("参数设置失败:$message")
}
}
)
}
}
// ==================== 状态显示 ====================
/**
* 初始化状态刷新按钮,获取 PLC 状态并格式化展示
*/
private fun initStatusButton() {
binding.btnRefreshStatus.clickWithDebounce {
showWaitingDialog("正在获取状态……")
PlcHelper.getInstance(this).getStatus(object : PlcCallback<PlcStatus> {
override fun onSuccess(result: PlcStatus) {
hideWaitingDialog()
binding.tvStatusInfo.text = buildStatusText(result)
}
override fun onError(message: String) {
hideWaitingDialog()
binding.tvStatusInfo.text = "获取状态失败:$message"
ToastUtils.showToast("获取状态失败:$message")
}
})
}
}
/**
* 将 PlcStatus 格式化为多行可读文本
*/
private fun buildStatusText(status: PlcStatus): String {
return """
空盘状态:${if (status.isEmptyPlate) "是(无餐盘)" else "否(有餐盘)"}
平台下位:${if (status.isPlateformDown) "是" else "否"}
平台上位:${if (status.isPlateformUp) "是" else "否"}
取盘红外:${if (status.isTakePlateInfrared) "已触发" else "未触发"}
报警代码:${if (status.alarmCode == 0) "正常(0" else "异常(${status.alarmCode}"}
""".trimIndent()
}
// ==================== 人脸库统计 ====================
/**
* 初始化人脸库统计区:页面加载时查询并显示人脸总数,绑定刷新按钮
*/
private fun initFaceCount() {
refreshFaceCount()
binding.btnRefreshFaceCount.clickWithDebounce {
refreshFaceCount()
}
}
/**
* 从数据库查询人脸总数并更新 UI
*/
private fun refreshFaceCount() {
lifecycleScope.launch {
val count = withContext(Dispatchers.IO) {
FaceDatabase.getInstance(this@PlcSettingActivity).faceDao().faceCount
}
binding.tvFaceCount.text = "人脸总数:${count}"
}
}
// ==================== 生命周期 ====================
/**
* 左侧日期双击 → 关闭设置界面
*/
override fun onLeftDoubleClick() {
finish()
}
/**
* 右侧时间双击 → 不做操作,防止循环打开
*/
override fun onRightDoubleClick() {
// 空实现,避免在设置页再次触发跳转
}
// ==================== 工具方法 ====================
/**
* 隐藏软键盘
*/
private fun hideKeyboard() {
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(currentFocus?.windowToken, 0)
}
}
@@ -1,97 +0,0 @@
package com.sw.platecabinet.activity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.sw.platecabinet.R
import kotlin.math.ceil
class TestActivity : AppCompatActivity() {
private lateinit var parentRecyclerView: RecyclerView
private lateinit var pageAdapter: PageAdapter
private val itemsPerPage = 22 // 每行2个,每列11个,共22个
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test)
parentRecyclerView = findViewById(R.id.parentRecyclerView)
parentRecyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.HORIZONTAL, false)
// 假设有100个数据项
val allItems = (1..30).map { "Item $it" }
val pages = allItems.chunked(itemsPerPage)
pageAdapter = PageAdapter(pages)
parentRecyclerView.adapter = pageAdapter
}
private fun convertToColumnFirst(original: List<String>, cols: Int): List<String> {
val rows = ceil(original.size.toDouble() / cols).toInt()
return List(original.size) { pos ->
val row = pos % rows
val col = pos / rows
val originalPos = col + row * cols
original.getOrElse(originalPos) { "" }
}
}
inner class PageAdapter(private val pages: List<List<String>>) :
RecyclerView.Adapter<PageAdapter.PageViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.page_item_layout, parent, false)
return PageViewHolder(view)
}
override fun onBindViewHolder(holder: PageViewHolder, position: Int) {
holder.bind(pages[position])
}
override fun getItemCount(): Int = pages.size
inner class PageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val childRecyclerView: RecyclerView =
itemView.findViewById(R.id.childRecyclerView)
fun bind(items: List<String>) {
childRecyclerView.layoutManager = GridLayoutManager(itemView.context, 2)
childRecyclerView.adapter = ChildAdapter(items)
}
}
}
inner class ChildAdapter(private val items: List<String>) :
RecyclerView.Adapter<ChildAdapter.ItemViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_bind_view, parent, false)
return ItemViewHolder(view)
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
holder.bind(position, items[position])
}
override fun getItemCount(): Int = items.size
inner class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val llRoot: LinearLayout = itemView.findViewById(R.id.ll_root)
private val tvNum: TextView = itemView.findViewById(R.id.tv_num)
fun bind(position: Int, item: String) {
tvNum.text = item
llRoot.setBackgroundResource(if (position % 3 == 0) R.drawable.grid_item_bind else R.drawable.grid_item_unbind)
}
}
}
}
@@ -1,57 +0,0 @@
//package com.sw.platecabinet.activity
//
//import android.graphics.Color
//import android.view.Gravity
//import android.view.ViewGroup
//import androidx.core.graphics.drawable.toDrawable
//import androidx.core.view.WindowCompat
//import com.sw.platecabinet.GlobalKey
//import com.sw.platecabinet.databinding.ActivityUnbindDialogBinding
//import com.sw.platecabinet.databinding.ItemTitleTimeBinding
//import com.sw.platecabinet.ext.dp
//import com.sw.platecabinet.model.response.EquipmentUserInfo
//
///**
// * 解绑确认弹窗界面
// */
//class UnBindDialogActivity : BaseActivity<ActivityUnbindDialogBinding>() {
// private var equipmentUserInfo: EquipmentUserInfo? = null
//
// override fun inflateViewBinding(): ActivityUnbindDialogBinding {
// return ActivityUnbindDialogBinding.inflate(layoutInflater)
// }
//
// override fun inflateTitleBinding(): ItemTitleTimeBinding? {
// return null
// }
//
// override fun initialize() {
// window?.apply {
// WindowCompat.setDecorFitsSystemWindows(this, false)
// setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
//
// attributes = attributes.apply {
// y = 314.dp
// width = (context.resources.displayMetrics.widthPixels * 0.8).toInt()
// height = ViewGroup.LayoutParams.MATCH_PARENT
// gravity = Gravity.CENTER
// }
// }
// equipmentUserInfo =
// intent.getParcelableExtra<EquipmentUserInfo>(GlobalKey.PARAM_EQUIPMENT_INFO)
// if (equipmentUserInfo == null) return
// binding.tvTipInfo.text = "即将解除【${equipmentUserInfo!!.name}】的餐盘"
// binding.tvCancel.setOnClickListener {
// finish()
// }
// binding.tvConfirm.setOnClickListener {
// setResult(RESULT_OK)
// finish()
// }
// }
//
// override fun finish() {
// super.finish()
// overridePendingTransition(0, 0) // 确保退出也无动画
// }
//}
@@ -1,36 +1,36 @@
package com.sw.platecabinet.dialog //package com.sw.platecabinet.dialog
//
import android.app.Dialog //import android.app.Dialog
import android.graphics.drawable.ColorDrawable //import android.graphics.drawable.ColorDrawable
import android.os.Bundle //import android.os.Bundle
import android.view.Window //import android.view.Window
import androidx.fragment.app.FragmentActivity //import androidx.fragment.app.FragmentActivity
import com.sw.platecabinet.databinding.DialogBalanceNotEnoughBinding //import com.sw.platecabinet.databinding.DialogBalanceNotEnoughBinding
import com.sw.platecabinet.ext.dp //import com.sw.platecabinet.ext.dp
//
open class BalanceNotEnoughDialog( //open class BalanceNotEnoughDialog(
private var activity: FragmentActivity // private var activity: FragmentActivity
) : Dialog(activity) { //) : Dialog(activity) {
//
private lateinit var binding: DialogBalanceNotEnoughBinding // private lateinit var binding: DialogBalanceNotEnoughBinding
//
override fun onCreate(savedInstanceState: Bundle?) { // override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) // super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE) // requestWindowFeature(Window.FEATURE_NO_TITLE)
binding = DialogBalanceNotEnoughBinding.inflate(layoutInflater) // binding = DialogBalanceNotEnoughBinding.inflate(layoutInflater)
setContentView(binding.root) // setContentView(binding.root)
window?.run { // window?.run {
attributes = attributes.apply { // attributes = attributes.apply {
width = 480.dp // width = 480.dp
} // }
setBackgroundDrawable(ColorDrawable()) // setBackgroundDrawable(ColorDrawable())
setCancelable(false) // setCancelable(false)
} // }
addListener() // addListener()
} // }
//
private fun addListener() { // private fun addListener() {
binding.tvConfirm.setOnClickListener { dismiss() } // binding.tvConfirm.setOnClickListener { dismiss() }
} // }
//
} //}
@@ -0,0 +1,38 @@
package com.sw.platecabinet.dialog
import android.app.Dialog
import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.Gravity
import android.view.View
import com.sw.platecabinet.R
import com.sw.platecabinet.ext.dp
import com.sw.platecabinet.ext.hideKeyboard
abstract class BaseDialog(
context: Context,
var defWidth: Int = 400.dp,
var defHeight: Int = 300.dp
) : Dialog(context, R.style.MyDialog) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val rootView = getRootView()
setContentView(rootView)
setCancelable(false)
setCanceledOnTouchOutside(false)
window?.apply {
setLayout(defWidth, defHeight)
setBackgroundDrawable(ColorDrawable())
setGravity(Gravity.CENTER)
}
rootView.setOnClickListener { it.hideKeyboard() }
initView()
}
abstract fun getRootView(): View
abstract fun initView()
}
@@ -0,0 +1,69 @@
package com.sw.platecabinet.dialog
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.databinding.DialogEnvSwitchBinding
import com.sw.platecabinet.ext.dp
import com.sw.platecabinet.utils.SpTool
/**
* 环境切换弹窗
*
* 提供 TEST / UAT / PROD 三套环境的切换功能,
* 初始化时自动根据 [GlobalData.appBaseUrl] 选中当前环境,
* 确认后更新 [GlobalData.appBaseUrl] 并通过 [onEnvChanged] 回调通知外部。
*
* @param context 上下文
* @param onEnvChanged 确认切换后的回调,参数为新的 baseUrl
*/
class EnvSwitchDialog(
context: Context,
private val onEnvChanged: (newBaseUrl: String) -> Unit = {}
) : BaseDialog(
context,
defWidth = 600.dp,
defHeight = WindowManager.LayoutParams.WRAP_CONTENT
) {
private lateinit var binding: DialogEnvSwitchBinding
override fun getRootView(): View {
binding = DialogEnvSwitchBinding.inflate(LayoutInflater.from(context))
return binding.root
}
override fun initView() {
// 根据当前 appBaseUrl 预选对应 RadioButton,不匹配则不选
when (GlobalData.appBaseUrl) {
GlobalData.LOCAL_BASE_URL -> binding.rbTest.isChecked = true
GlobalData.TEST_BASE_URL -> binding.rbUat.isChecked = true
GlobalData.PROD_BASE_URL -> binding.rbProd.isChecked = true
}
// 取消按钮
binding.btnCancel.setOnClickListener { dismiss() }
// 确认按钮
binding.btnConfirm.setOnClickListener {
// 未选中任何选项时提示用户
if (binding.rgEnv.checkedRadioButtonId == -1) {
ToastUtils.showToast("请选择环境")
return@setOnClickListener
}
val newUrl = when (binding.rgEnv.checkedRadioButtonId) {
binding.rbTest.id -> GlobalData.LOCAL_BASE_URL
binding.rbUat.id -> GlobalData.TEST_BASE_URL
binding.rbProd.id -> GlobalData.PROD_BASE_URL
else -> return@setOnClickListener
}
GlobalData.appBaseUrl = newUrl
SpTool.baseUrl = newUrl
onEnvChanged(newUrl)
dismiss()
}
}
}
@@ -1,38 +1,38 @@
package com.sw.platecabinet.dialog //package com.sw.platecabinet.dialog
//
import android.app.Dialog //import android.app.Dialog
import android.graphics.drawable.ColorDrawable //import android.graphics.drawable.ColorDrawable
import android.os.Bundle //import android.os.Bundle
import android.view.Window //import android.view.Window
import androidx.fragment.app.FragmentActivity //import androidx.fragment.app.FragmentActivity
import com.sw.platecabinet.databinding.DialogUserBindRemindBinding //import com.sw.platecabinet.databinding.DialogUserBindRemindBinding
import com.sw.platecabinet.ext.dp //import com.sw.platecabinet.ext.dp
//
open class UserBindRemindDialog( //open class UserBindRemindDialog(
private var activity: FragmentActivity, // private var activity: FragmentActivity,
var content:String // var content:String
) : Dialog(activity) { //) : Dialog(activity) {
//
private lateinit var binding: DialogUserBindRemindBinding // private lateinit var binding: DialogUserBindRemindBinding
//
override fun onCreate(savedInstanceState: Bundle?) { // override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) // super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE) // requestWindowFeature(Window.FEATURE_NO_TITLE)
binding = DialogUserBindRemindBinding.inflate(layoutInflater) // binding = DialogUserBindRemindBinding.inflate(layoutInflater)
setContentView(binding.root) // setContentView(binding.root)
window?.run { // window?.run {
attributes = attributes.apply { // attributes = attributes.apply {
width = 480.dp // width = 480.dp
} // }
setBackgroundDrawable(ColorDrawable()) // setBackgroundDrawable(ColorDrawable())
setCancelable(false) // setCancelable(false)
} // }
binding.tvBindContent.text = content // binding.tvBindContent.text = content
addListener() // addListener()
} // }
//
private fun addListener() { // private fun addListener() {
binding.tvConfirm.setOnClickListener { dismiss() } // binding.tvConfirm.setOnClickListener { dismiss() }
} // }
//
} //}
@@ -1,8 +1,10 @@
package com.sw.platecabinet.ext package com.sw.platecabinet.ext
import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.annotation.Dimension import androidx.annotation.Dimension
/** /**
@@ -64,3 +66,11 @@ fun View.invisible() {
fun View.gone() { fun View.gone() {
visibility = View.GONE visibility = View.GONE
} }
/**
* 隐藏软键盘
*/
fun View.hideKeyboard() {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(windowToken, 0)
}
@@ -8,21 +8,16 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.sw.platecabinet.R import com.sw.platecabinet.R
import com.sw.platecabinet.view.CustomLoadingDialog import com.sw.platecabinet.view.CustomLoadingDialog
import com.sw.platecabinet.viewmodel.SettingViewModel
import kotlinx.coroutines.launch
abstract class BaseFragment<VB : ViewBinding>( abstract class BaseFragment<VB : ViewBinding>(
private val bindingInflater: (inflater: LayoutInflater, parent: ViewGroup?, attachToParent: Boolean) -> VB private val bindingInflater: (inflater: LayoutInflater, parent: ViewGroup?, attachToParent: Boolean) -> VB
) : Fragment() { ) : Fragment() {
private var _binding: VB? = null protected var _binding: VB? = null
protected val binding get() = _binding!! protected val binding get() = _binding!!
private var mDialogWaiting: CustomLoadingDialog? = null private var mDialogWaiting: CustomLoadingDialog? = null
protected val viewModel by viewModels<SettingViewModel>()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@@ -39,15 +34,7 @@ abstract class BaseFragment<VB : ViewBinding>(
* 注册数据变化监听 * 注册数据变化监听
*/ */
open fun registerDataChange() { open fun registerDataChange() {
lifecycleScope.launch {
viewModel.showLoading.collect {
if (it) {
showWaitingDialog("")
} else {
hideWaitingDialog()
}
}
}
} }
abstract fun initialize() abstract fun initialize()
@@ -11,6 +11,7 @@ import timber.log.Timber
class PlateCabinetFullFragment : class PlateCabinetFullFragment :
BaseFragment<FragmentPlateCabinetFullBinding>(FragmentPlateCabinetFullBinding::inflate) { BaseFragment<FragmentPlateCabinetFullBinding>(FragmentPlateCabinetFullBinding::inflate) {
private var totalTimeInMillis: Long = Constants.AUTO_CLOSE_TIME * 1000 private var totalTimeInMillis: Long = Constants.AUTO_CLOSE_TIME * 1000
private var countDownTimer: CountDownTimer? = null
override fun initialize() { override fun initialize() {
Timber.d("initialize") Timber.d("initialize")
@@ -18,15 +19,22 @@ class PlateCabinetFullFragment :
} }
fun initCountTime() { fun initCountTime() {
object : CountDownTimer(totalTimeInMillis, 1000) { countDownTimer = object : CountDownTimer(totalTimeInMillis, 1000) {
override fun onTick(millisUntilFinished: Long) { override fun onTick(millisUntilFinished: Long) {
binding.tvAutoClose.text = "${(millisUntilFinished / 1000).toInt() + 1}秒后返回主屏" _binding?.tvAutoClose?.text = "${(millisUntilFinished / 1000).toInt() + 1}秒后返回主屏"
} }
override fun onFinish() { override fun onFinish() {
activity?.finish() activity?.finish()
} }
}.start() }
countDownTimer?.start()
}
override fun onDestroyView() {
countDownTimer?.cancel()
countDownTimer = null
super.onDestroyView()
} }
} }
@@ -7,7 +7,6 @@ import com.sw.platecabinet.Constants
import com.sw.platecabinet.R import com.sw.platecabinet.R
import com.sw.platecabinet.activity.InitActivity import com.sw.platecabinet.activity.InitActivity
import com.sw.platecabinet.databinding.FragmentPlateOpenBinding import com.sw.platecabinet.databinding.FragmentPlateOpenBinding
import com.sw.platecabinet.model.response.EquipmentUserInfo
/** /**
* 餐盘柜提示界面 * 餐盘柜提示界面
@@ -21,6 +20,7 @@ class PlateOpenFragment :
// private var isAdmin: Boolean = false // private var isAdmin: Boolean = false
private var isScanCode = false private var isScanCode = false
private var countDownTimer: CountDownTimer? = null
companion object { companion object {
const val IS_SCAN_CODE = "isScanCode" const val IS_SCAN_CODE = "isScanCode"
// @JvmStatic // @JvmStatic
@@ -76,19 +76,23 @@ class PlateOpenFragment :
} }
fun initCountTime() { fun initCountTime() {
object : CountDownTimer(totalTimeInMillis, 1000) { countDownTimer = object : CountDownTimer(totalTimeInMillis, 1000) {
override fun onTick(millisUntilFinished: Long) { override fun onTick(millisUntilFinished: Long) {
binding.tvAutoClose.text = ((millisUntilFinished / 1000).toInt() + 1).toString() _binding?.tvAutoClose?.text = ((millisUntilFinished / 1000).toInt() + 1).toString()
} }
override fun onFinish() { override fun onFinish() {
val ctx = context ?: return
activity?.finish() activity?.finish()
// if (!isAdmin) { ctx.startActivity(Intent(ctx, InitActivity::class.java))
val intent = Intent(context, InitActivity::class.java)
startActivity(intent)
// }
} }
}.start() }
countDownTimer?.start()
}
override fun onDestroyView() {
countDownTimer?.cancel()
countDownTimer = null
super.onDestroyView()
} }
} }
@@ -1,241 +1,241 @@
package com.sw.platecabinet.fragment //package com.sw.platecabinet.fragment
//
import android.view.View //import android.view.View
import androidx.core.view.isVisible //import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope //import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager //import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager //import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView //import androidx.recyclerview.widget.RecyclerView
import com.sw.inbound.utils.DateTimeUtils //import com.sw.inbound.utils.DateTimeUtils
import com.sw.plate.utils.ToastUtils //import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.comn.SerialApi //import com.sw.plate.utils.comn.SerialApi
import com.sw.plate.utils.comn.SerialPortManager //import com.sw.plate.utils.comn.SerialPortManager
import com.sw.platecabinet.GlobalData //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.activity.BaseActivity //import com.sw.platecabinet.activity.BaseActivity
import com.sw.platecabinet.activity.MainActivity //import com.sw.platecabinet.activity.MainActivity
import com.sw.platecabinet.activity.PageType //import com.sw.platecabinet.activity.PageType
import com.sw.platecabinet.adapter.GenericItemAdapter //import com.sw.platecabinet.adapter.GenericItemAdapter
import com.sw.platecabinet.adapter.GenericPageAdapter //import com.sw.platecabinet.adapter.GenericPageAdapter
import com.sw.platecabinet.adapter.GridSpacingItemDecoration //import com.sw.platecabinet.adapter.GridSpacingItemDecoration
import com.sw.platecabinet.adapter.dpToPx //import com.sw.platecabinet.adapter.dpToPx
import com.sw.platecabinet.databinding.FragmentSettingListBinding //import com.sw.platecabinet.databinding.FragmentSettingListBinding
import com.sw.platecabinet.databinding.ItemBindViewBinding //import com.sw.platecabinet.databinding.ItemBindViewBinding
import com.sw.platecabinet.ext.formatNumber //import com.sw.platecabinet.ext.formatNumber
import com.sw.platecabinet.ext.maskName //import com.sw.platecabinet.ext.maskName
import com.sw.platecabinet.model.response.EquipmentUserInfo //import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.utils.ListArrangementUtil //import com.sw.platecabinet.utils.ListArrangementUtil
import com.sw.platecabinet.utils.PlateUtils //import com.sw.platecabinet.utils.PlateUtils
import com.sw.platecabinet.utils.SpTool //import com.sw.platecabinet.utils.SpTool
import kotlinx.coroutines.Dispatchers //import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch //import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext //import kotlinx.coroutines.withContext
import timber.log.Timber //import timber.log.Timber
//
/** ///**
* 餐盘柜绑定的用户列表界面 // * 餐盘柜绑定的用户列表界面
*/ // */
class SettingListFragment : //class SettingListFragment :
BaseFragment<FragmentSettingListBinding>(FragmentSettingListBinding::inflate) { // BaseFragment<FragmentSettingListBinding>(FragmentSettingListBinding::inflate) {
//
/** // /**
* 需要横向滚动的adapter // * 需要横向滚动的adapter
*/ // */
private var pageAdapter: GenericPageAdapter<EquipmentUserInfo, ItemBindViewBinding>? = null // private var pageAdapter: GenericPageAdapter<EquipmentUserInfo, ItemBindViewBinding>? = null
//
/** // /**
* 垂直滚动的adapter // * 垂直滚动的adapter
*/ // */
private var itemAdapter: GenericItemAdapter<EquipmentUserInfo, ItemBindViewBinding>? = null // private var itemAdapter: GenericItemAdapter<EquipmentUserInfo, ItemBindViewBinding>? = null
//
override fun registerDataChange() { // override fun registerDataChange() {
super.registerDataChange() // super.registerDataChange()
// lifecycleScope.launch { //// lifecycleScope.launch {
// viewModel.equipmentList.collect { it -> //// viewModel.equipmentList.collect { it ->
// Timber.d("registerDateChange updateAdapter it = $it") //// Timber.d("registerDateChange updateAdapter it = $it")
// updateAdapter(it) //// updateAdapter(it)
// } //// }
// }
}
override fun initialize() {
binding.mainRecyclerView.apply {
if (GlobalData.arrayCross > 2) {
layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
pageAdapter = createPageAdapter()
adapter = pageAdapter
} else {
layoutManager = GridLayoutManager(context, 2)
// 添加间距装饰(12dp)
addItemDecoration(
GridSpacingItemDecoration(
spanCount = 2,
spacing = requireContext().dpToPx(12),
includeEdge = true // 包含边缘间距
)
)
itemAdapter = createItemAdapter()
adapter = itemAdapter
}
}
}
override fun onResume() {
super.onResume()
// viewModel.getEquipmentList{
// updateAdapter(it)
// }
MyApp.plateList?.let {
updateAdapter(it)
}
}
// private fun getPlateData(block: (List<EquipmentUserInfo>) -> Unit) {
// MyApp.plateList?.let {
// block(it)
// }
//// viewModel.getEquipmentList { items ->
//// block(items)
//// } //// }
// } // }
//
private fun createItemAdapter(): GenericItemAdapter<EquipmentUserInfo, ItemBindViewBinding> { // override fun initialize() {
return GenericItemAdapter( // binding.mainRecyclerView.apply {
items = emptyList(), // if (GlobalData.arrayCross > 2) {
bindingInflater = ItemBindViewBinding::inflate, // layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
bindCallback = { item, position -> // pageAdapter = createPageAdapter()
setItemInfo(item, position) // adapter = pageAdapter
}
)
}
private fun createPageAdapter(): GenericPageAdapter<EquipmentUserInfo, ItemBindViewBinding> {
return GenericPageAdapter(
pages = emptyList(),
pageLayoutId = R.layout.page_item_layout,
itemBindingInflater = ItemBindViewBinding::inflate,
itemBindCallback = { item, position ->
setItemInfo(item, position)
}
)
}
private fun ItemBindViewBinding.setItemInfo(
item: EquipmentUserInfo,
position: Int
) {
// if (item.isPlaceholder()) {
// this.llRoot.isVisible = false
// return
// }
this.llRoot.isVisible = true
this.tvNum.text = formatNumber(item.equipmentBoxCode ?: "")
if (item.plateNumber.isNullOrBlank().not()) {
// val date = DateTimeUtils.parseDateTime(item.updateTime)
// val timeInMillis = date?.time ?: 0L
// val isOldTime = DateTimeUtils.isMoreThanHoursFromNow(timeInMillis)
this.llRoot.setBackgroundResource(R.drawable.grid_item_bind)
this.tvNum.setTextColor(resources.getColor(R.color.bind_4E535D))
// this.tvLastTime.setTextColor(
// if (isOldTime) resources.getColor(R.color.bind_time_old) else resources.getColor(
// R.color.bind_585868
// )
// )
this.llOpen.setBackgroundResource(R.drawable.grid_button_bind)
this.tvOpen.setTextColor(resources.getColor(R.color.bind_FFCC99))
this.llBindInfo.visibility = View.VISIBLE
this.tvUnbind.visibility = View.GONE
// this.tvName.text = item.name.maskName()
this.tvName.text = item.plateNumber
// this.tvLastTime.text = DateTimeUtils.getTimeAgo(date)
} else {
this.llRoot.setBackgroundResource(R.drawable.grid_item_unbind)
this.tvNum.setTextColor(resources.getColor(R.color.bind_FFCC99))
this.llOpen.setBackgroundResource(R.drawable.grid_button_unbind)
this.tvOpen.setTextColor(resources.getColor(R.color.unbind_32283C))
this.llBindInfo.visibility = View.GONE
this.tvUnbind.visibility = View.VISIBLE
// this.tvUnbind.text = "未绑定"
}
// this.llRoot.setOnClickListener {
//// Timber.d("itemClick llRoot ${item.name}, position = $position")
// if (item.isBound()) {
// MainActivity.start(
// requireContext(),
// pageType = PageType.UNBIND_PLATE,
// equipmentUserInfo = item
// )
// } else { // } else {
// MainActivity.start( // layoutManager = GridLayoutManager(context, 2)
// requireContext(), // // 添加间距装饰(12dp)
// pageType = PageType.BIND_PLATE, // addItemDecoration(
// equipmentUserInfo = item // GridSpacingItemDecoration(
// spanCount = 2,
// spacing = requireContext().dpToPx(12),
// includeEdge = true // 包含边缘间距
// )
// )
// itemAdapter = createItemAdapter()
// adapter = itemAdapter
// }
// }
//
// }
//
// override fun onResume() {
// super.onResume()
//// viewModel.getEquipmentList{
//// updateAdapter(it)
//// }
// MyApp.plateList?.let {
// updateAdapter(it)
// }
// }
//
//// private fun getPlateData(block: (List<EquipmentUserInfo>) -> Unit) {
//// MyApp.plateList?.let {
//// block(it)
//// }
////// viewModel.getEquipmentList { items ->
////// block(items)
////// }
//// }
//
// private fun createItemAdapter(): GenericItemAdapter<EquipmentUserInfo, ItemBindViewBinding> {
// return GenericItemAdapter(
// items = emptyList(),
// bindingInflater = ItemBindViewBinding::inflate,
// bindCallback = { item, position ->
// setItemInfo(item, position)
// }
// )
// }
//
// private fun createPageAdapter(): GenericPageAdapter<EquipmentUserInfo, ItemBindViewBinding> {
// return GenericPageAdapter(
// pages = emptyList(),
// pageLayoutId = R.layout.page_item_layout,
// itemBindingInflater = ItemBindViewBinding::inflate,
// itemBindCallback = { item, position ->
// setItemInfo(item, position)
// }
// )
// }
//
// private fun ItemBindViewBinding.setItemInfo(
// item: EquipmentUserInfo,
// position: Int
// ) {
//// if (item.isPlaceholder()) {
//// this.llRoot.isVisible = false
//// return
//// }
// this.llRoot.isVisible = true
// this.tvNum.text = formatNumber(item.equipmentBoxCode ?: "")
// if (item.plateNumber.isNullOrBlank().not()) {
//// val date = DateTimeUtils.parseDateTime(item.updateTime)
//// val timeInMillis = date?.time ?: 0L
//// val isOldTime = DateTimeUtils.isMoreThanHoursFromNow(timeInMillis)
//
// this.llRoot.setBackgroundResource(R.drawable.grid_item_bind)
// this.tvNum.setTextColor(resources.getColor(R.color.bind_4E535D))
//// this.tvLastTime.setTextColor(
//// if (isOldTime) resources.getColor(R.color.bind_time_old) else resources.getColor(
//// R.color.bind_585868
//// )
//// )
// this.llOpen.setBackgroundResource(R.drawable.grid_button_bind)
// this.tvOpen.setTextColor(resources.getColor(R.color.bind_FFCC99))
// this.llBindInfo.visibility = View.VISIBLE
// this.tvUnbind.visibility = View.GONE
//
//// this.tvName.text = item.name.maskName()
// this.tvName.text = item.plateNumber
//
//// this.tvLastTime.text = DateTimeUtils.getTimeAgo(date)
// } else {
// this.llRoot.setBackgroundResource(R.drawable.grid_item_unbind)
// this.tvNum.setTextColor(resources.getColor(R.color.bind_FFCC99))
// this.llOpen.setBackgroundResource(R.drawable.grid_button_unbind)
// this.tvOpen.setTextColor(resources.getColor(R.color.unbind_32283C))
// this.llBindInfo.visibility = View.GONE
// this.tvUnbind.visibility = View.VISIBLE
//// this.tvUnbind.text = "未绑定"
// }
//// this.llRoot.setOnClickListener {
////// Timber.d("itemClick llRoot ${item.name}, position = $position")
//// if (item.isBound()) {
//// MainActivity.start(
//// requireContext(),
//// pageType = PageType.UNBIND_PLATE,
//// equipmentUserInfo = item
//// )
//// } else {
//// MainActivity.start(
//// requireContext(),
//// pageType = PageType.BIND_PLATE,
//// equipmentUserInfo = item
//// )
//// }
//// }
// this.llOpen.setOnClickListener {
//// item.faceId = null
// PlateUtils.open(
// fragmentActivity = requireActivity(),
// code = item.equipmentBoxCode,
// isScanCode = false,
// isSetting = true
// ) // )
// } // }
// } // }
this.llOpen.setOnClickListener { //
// item.faceId = null // fun updateAdapter(items: List<EquipmentUserInfo>) {
PlateUtils.open( // val itemsPerPage = 2 * GlobalData.arrayVertical // 每行2个,每列11个,共22个
fragmentActivity = requireActivity(), // if (GlobalData.arrayCross > 2) {
code = item.equipmentBoxCode, // lifecycleScope.launch(Dispatchers.Default) {
isScanCode = false, // // 多recyclerview
isSetting = true // var pages = if (items.size > itemsPerPage) {
) // if (GlobalData.arrayMode == 0) {
} // val pageList = items.chunked(itemsPerPage)
} // pageList.mapIndexed { index, it ->
// var itemList: MutableList<EquipmentUserInfo> = it.toMutableList()
fun updateAdapter(items: List<EquipmentUserInfo>) { // if (index == pageList.size - 1) {
val itemsPerPage = 2 * GlobalData.arrayVertical // 每行2个,每列11个,共22个 // // 补全剩余item
if (GlobalData.arrayCross > 2) { // itemList = (itemList + List(itemsPerPage - itemList.size) {
lifecycleScope.launch(Dispatchers.Default) { // EquipmentUserInfo()
// 多recyclerview // }) as MutableList<EquipmentUserInfo>
var pages = if (items.size > itemsPerPage) { // }
if (GlobalData.arrayMode == 0) { // ListArrangementUtil.convertToColumnFirst(
val pageList = items.chunked(itemsPerPage) // itemList,
pageList.mapIndexed { index, it -> // GlobalData.arrayVertical
var itemList: MutableList<EquipmentUserInfo> = it.toMutableList() // )
if (index == pageList.size - 1) { // }
// 补全剩余item // } else {
itemList = (itemList + List(itemsPerPage - itemList.size) { // val newItems =
EquipmentUserInfo() // ListArrangementUtil.horizontalSortPageItem(items, GlobalData.arrayCross)
}) as MutableList<EquipmentUserInfo> // newItems.chunked(itemsPerPage)
} // }
ListArrangementUtil.convertToColumnFirst( // } else {
itemList, // listOf(
GlobalData.arrayVertical // items
) // )
} // }
} else { // withContext(Dispatchers.Main) {
val newItems = // pageAdapter?.updatePages(pages)
ListArrangementUtil.horizontalSortPageItem(items, GlobalData.arrayCross) // }
newItems.chunked(itemsPerPage) // }
} // } else {
} else { // // 单 recyclerview
listOf( // lifecycleScope.launch(Dispatchers.Default) {
items // var pages = items
) // if (GlobalData.arrayMode == 0) {
} // pages = ListArrangementUtil.verticalSortItem(pages)
withContext(Dispatchers.Main) { // }
pageAdapter?.updatePages(pages) // withContext(Dispatchers.Main) {
} // itemAdapter?.updateData(pages)
} // }
} else { // }
// 单 recyclerview // }
lifecycleScope.launch(Dispatchers.Default) { // binding.mainRecyclerView.scrollToPosition(0)
var pages = items // }
if (GlobalData.arrayMode == 0) { //}
pages = ListArrangementUtil.verticalSortItem(pages)
}
withContext(Dispatchers.Main) {
itemAdapter?.updateData(pages)
}
}
}
binding.mainRecyclerView.scrollToPosition(0)
}
}
@@ -1,19 +1,19 @@
package com.sw.platecabinet.model package com.sw.platecabinet.model
import com.sw.platecabinet.model.response.EquipmentUserInfo import com.google.gson.annotations.SerializedName
/** /**
* 错误信息 code == 200 成功 * 错误信息 code == 200 成功
*/ */
data class ErrorInfo( //data class ErrorInfo(
val code: String = "00000", // val code: String = "00000",
val msg: String = "", // val msg: String = "",
val equipmentUserInfo: EquipmentUserInfo? = null // val equipmentUserInfo: EquipmentUserInfo? = null
) { //) {
fun isSuccess(): Boolean { // fun isSuccess(): Boolean {
return code == "00000" // return code == "00000"
} // }
} //}
data class DeviceConfig( data class DeviceConfig(
var arcsoftAppId: String? = null, var arcsoftAppId: String? = null,
@@ -21,6 +21,27 @@ data class DeviceConfig(
var arcsoftActiveKey: String? = null var arcsoftActiveKey: String? = null
) )
data class DeviceConfigV2(
/** 业务服务器 BASE URL */
val appPackageUrl: String? = null,
/** 食堂/餐厅名称 */
val canteenName: String? = null,
/** 食堂/餐厅 ID(后续所有业务 API 的 restId */
val canteenId: String? = null,
/** 虹软人脸 SDK App ID */
val arcsoftAppId: String? = null,
/** 虹软人脸 SDK Key */
val arcsoftSdkKey: String? = null,
/** 虹软人脸 SDK Active Key */
val arcsoftActiveKey: String? = null,
/** MQTT 客户端服务器 IP */
val clientServerIp: String? = null,
/** 智慧食堂服务器 IP(MQTT 端口) */
val zhstServerIp: String? = null
)
data class CodeMsg( data class CodeMsg(
val code: String? = "", val code: String? = "",
val msg: String? = "" val msg: String? = ""
@@ -0,0 +1,6 @@
package com.sw.platecabinet.model.request
data class FeatureBody(
val url:String,
val featureChar:String
)
@@ -0,0 +1,13 @@
package com.sw.platecabinet.model.request
/**
* 通用 ID 请求体
*
* 用于按用户 id(餐盘号)查询或上报类的接口,例如:
* - P-09a 取餐盘(/neglect/pickup/plate-pickup
*
* @property id 用户 id(餐盘号 = userId
*/
data class IdDTO(
val id: Long
)
@@ -1,72 +1,72 @@
package com.sw.platecabinet.model.response //package com.sw.platecabinet.model.response
//
//
import android.os.Parcelable //import android.os.Parcelable
import com.google.gson.annotations.SerializedName //import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize //import kotlinx.parcelize.Parcelize
//
@Parcelize //@Parcelize
data class EquipmentInfo( //data class EquipmentInfo(
@SerializedName("appPackageLocalUrl") // @SerializedName("appPackageLocalUrl")
val appPackageLocalUrl: String? = "", // val appPackageLocalUrl: String? = "",
@SerializedName("appPackageUrl") // @SerializedName("appPackageUrl")
val appPackageUrl: String? = "", // val appPackageUrl: String? = "",
@SerializedName("arcsoftActiveKey") // @SerializedName("arcsoftActiveKey")
val arcsoftActiveKey: String? = "", // val arcsoftActiveKey: String? = "",
@SerializedName("arcsoftAppId") // @SerializedName("arcsoftAppId")
val arcsoftAppId: String? = "", // val arcsoftAppId: String? = "",
@SerializedName("arcsoftSdkKey") // @SerializedName("arcsoftSdkKey")
val arcsoftSdkKey: String? = "", // val arcsoftSdkKey: String? = "",
@SerializedName("arrayCross") // @SerializedName("arrayCross")
val arrayCross: Int? = 0, // val arrayCross: Int? = 0,
@SerializedName("arrayMode") // @SerializedName("arrayMode")
val arrayMode: String? = "", // val arrayMode: String? = "",
@SerializedName("arrayVertical") // @SerializedName("arrayVertical")
val arrayVertical: Int? = 0, // val arrayVertical: Int? = 0,
@SerializedName("canteenId") // @SerializedName("canteenId")
val canteenId: String? = "", // val canteenId: String? = "",
@SerializedName("canteenName") // @SerializedName("canteenName")
val canteenName: String? = "", // val canteenName: String? = "",
@SerializedName("clientServerIp") // @SerializedName("clientServerIp")
val clientServerIp: String? = "", // val clientServerIp: String? = "",
@SerializedName("createBy") // @SerializedName("createBy")
val createBy: String? = "", // val createBy: String? = "",
@SerializedName("createTime") // @SerializedName("createTime")
val createTime: String? = "", // val createTime: String? = "",
@SerializedName("customerName") // @SerializedName("customerName")
val customerName: String? = "", // val customerName: String? = "",
@SerializedName("equipmentCode") // @SerializedName("equipmentCode")
val equipmentCode: String? = "", // val equipmentCode: String? = "",
@SerializedName("equipmentName") // @SerializedName("equipmentName")
val equipmentName: String? = "", // val equipmentName: String? = "",
@SerializedName("equipmentName_dictText") // @SerializedName("equipmentName_dictText")
val equipmentNameDictText: String? = "", // val equipmentNameDictText: String? = "",
@SerializedName("id") // @SerializedName("id")
val id: String? = "", // val id: String? = "",
@SerializedName("isSync") // @SerializedName("isSync")
val isSync: Int? = 0, // val isSync: Int? = 0,
@SerializedName("mqName") // @SerializedName("mqName")
val mqName: String? = "", // val mqName: String? = "",
@SerializedName("mqPassword") // @SerializedName("mqPassword")
val mqPassword: String? = "", // val mqPassword: String? = "",
@SerializedName("orgCode") // @SerializedName("orgCode")
val orgCode: String? = "", // val orgCode: String? = "",
@SerializedName("owningTrack") // @SerializedName("owningTrack")
val owningTrack: Int? = 0, // val owningTrack: Int? = 0,
@SerializedName("owningTrackOrder") // @SerializedName("owningTrackOrder")
val owningTrackOrder: Int? = 0, // val owningTrackOrder: Int? = 0,
@SerializedName("screenHtml") // @SerializedName("screenHtml")
val screenHtml: String? = "", // val screenHtml: String? = "",
@SerializedName("serviceRequestAddress") // @SerializedName("serviceRequestAddress")
val serviceRequestAddress: String? = "", // val serviceRequestAddress: String? = "",
@SerializedName("status") // @SerializedName("status")
val status: Int? = 0, // val status: Int? = 0,
@SerializedName("sysOrgCode") // @SerializedName("sysOrgCode")
val sysOrgCode: String? = "", // val sysOrgCode: String? = "",
@SerializedName("updateBy") // @SerializedName("updateBy")
val updateBy: String? = "", // val updateBy: String? = "",
@SerializedName("updateTime") // @SerializedName("updateTime")
val updateTime: String? = "", // val updateTime: String? = "",
@SerializedName("zhstServerIp") // @SerializedName("zhstServerIp")
val zhstServerIp: String? = "" // val zhstServerIp: String? = ""
) : Parcelable //) : Parcelable
@@ -1,90 +1,90 @@
package com.sw.platecabinet.model.response //package com.sw.platecabinet.model.response
//
import android.os.Parcelable //import android.os.Parcelable
import android.text.TextUtils //import android.text.TextUtils
//import com.google.gson.annotations.SerializedName ////import com.google.gson.annotations.SerializedName
import com.sw.platecabinet.GlobalData //import com.sw.platecabinet.GlobalData
import kotlinx.parcelize.Parcelize //import kotlinx.parcelize.Parcelize
//
/** ///**
* 设备绑定用户信息 // * 设备绑定用户信息
*/
@Parcelize
data class EquipmentUserInfo(
// /**
// * 主键
// */ // */
// val id: Long? = 0, //@Parcelize
//data class EquipmentUserInfo(
//// /**
//// * 主键
//// */
//// val id: Long? = 0,
//// /**
//// *
//// * 设备Id
//// */
//// val equipmentId: String? = "",
//// /**
//// * 设备编号
//// */
//// var equipmentCode: String? = "",
// /**
// * 设备盒子编号
// */
// var equipmentBoxCode: String? = "",
// /**
// * 会员Id
// */
// var faceId: String? = "",
// /**
// * 餐盘编号
// */
// var plateNumber: String? = "",
//// /**
//// * 设备名称
//// */
//// val equipmentName: String? = "",
//
//// val orderNo: String? = "",
// /** // /**
// * // *
// * 设备Id // * 更新时间
// */ // */
// val equipmentId: String? = "", // var updateTime: String? = "",
// /**
// * 设备编号
// */
// var equipmentCode: String? = "",
/**
* 设备盒子编号
*/
var equipmentBoxCode: String? = "",
/**
* 会员Id
*/
var faceId: String? = "",
/**
* 餐盘编号
*/
var plateNumber: String? = "",
// /**
// * 设备名称
// */
// val equipmentName: String? = "",
// val orderNo: String? = "",
/**
*
* 更新时间
*/
var updateTime: String? = "",
// val name: String? = "",
// val phone: String? = "",
// val faceUrl: String? = "",
// val mealTime: String? = "",
// val mealTimeInterval: String? = "",
// val openTime: String? = "",
// val openTimeInterval: String? = "",
// val eatCount: Int? = 0,
// //
// var cardBalance: Double? = 0.toDouble(), //// val name: String? = "",
//// val phone: String? = "",
// /** //// val faceUrl: String? = "",
// * 用于cardBalance后续拦截判断 //// val mealTime: String? = "",
// */ //// val mealTimeInterval: String? = "",
// var showBalanceNotEnoughDialog: Boolean = false, //// val openTime: String? = "",
// var isIntercept: Boolean = false //// val openTimeInterval: String? = "",
) : Parcelable { //// val eatCount: Int? = 0,
////
// /** //// var cardBalance: Double? = 0.toDouble(),
// * 是否已绑定
// */
// fun isBound(): Boolean {
// return faceId.isNullOrBlank().not()
// }
// /**
// * 是否是其他设备
// */
// fun isOtherEquipment(): Boolean {
// return equipmentCode != null && equipmentCode != "" && GlobalData.globalEquipmentCode != equipmentCode
// }
// fun hasEquipmentBox(): Boolean {
// return !TextUtils.isEmpty(equipmentBoxCode)
// }
// //
// fun isPlaceholder(): Boolean { //// /**
// return equipmentBoxCode.isNullOrBlank() //// * 用于cardBalance后续拦截判断
// } //// */
} //// var showBalanceNotEnoughDialog: Boolean = false,
//// var isIntercept: Boolean = false
//) : Parcelable {
//
//// /**
//// * 是否已绑定
//// */
//// fun isBound(): Boolean {
//// return faceId.isNullOrBlank().not()
//// }
//
//// /**
//// * 是否是其他设备
//// */
//// fun isOtherEquipment(): Boolean {
//// return equipmentCode != null && equipmentCode != "" && GlobalData.globalEquipmentCode != equipmentCode
//// }
//
//// fun hasEquipmentBox(): Boolean {
//// return !TextUtils.isEmpty(equipmentBoxCode)
//// }
////
//// fun isPlaceholder(): Boolean {
//// return equipmentBoxCode.isNullOrBlank()
//// }
//}
@@ -1,85 +1,85 @@
package com.sw.platecabinet.model.response //package com.sw.platecabinet.model.response
//
//
import android.os.Parcelable //import android.os.Parcelable
import com.google.gson.annotations.SerializedName //import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize //import kotlinx.parcelize.Parcelize
//
/** ///**
* 搜索会员信息结果 // * 搜索会员信息结果
*/ // */
@Parcelize //@Parcelize
data class SearchResult( //data class SearchResult(
@SerializedName("endRow") // @SerializedName("endRow")
val endRow: Int? = 0, // val endRow: Int? = 0,
@SerializedName("hasNextPage") // @SerializedName("hasNextPage")
val hasNextPage: Boolean? = false, // val hasNextPage: Boolean? = false,
@SerializedName("hasPreviousPage") // @SerializedName("hasPreviousPage")
val hasPreviousPage: Boolean? = false, // val hasPreviousPage: Boolean? = false,
@SerializedName("isFirstPage") // @SerializedName("isFirstPage")
val isFirstPage: Boolean? = false, // val isFirstPage: Boolean? = false,
@SerializedName("isLastPage") // @SerializedName("isLastPage")
val isLastPage: Boolean? = false, // val isLastPage: Boolean? = false,
@SerializedName("list") // @SerializedName("list")
val list: List<Member>? = listOf(), // val list: List<Member>? = listOf(),
@SerializedName("navigateFirstPage") // @SerializedName("navigateFirstPage")
val navigateFirstPage: Int? = 0, // val navigateFirstPage: Int? = 0,
@SerializedName("navigateLastPage") // @SerializedName("navigateLastPage")
val navigateLastPage: Int? = 0, // val navigateLastPage: Int? = 0,
@SerializedName("navigatePages") // @SerializedName("navigatePages")
val navigatePages: Int? = 0, // val navigatePages: Int? = 0,
@SerializedName("navigatepageNums") // @SerializedName("navigatepageNums")
val navigatepageNums: List<Int?>? = listOf(), // val navigatepageNums: List<Int?>? = listOf(),
@SerializedName("nextPage") // @SerializedName("nextPage")
val nextPage: Int? = 0, // val nextPage: Int? = 0,
@SerializedName("pageNum") // @SerializedName("pageNum")
val pageNum: Int? = 0, // val pageNum: Int? = 0,
@SerializedName("pageSize") // @SerializedName("pageSize")
val pageSize: Int? = 0, // val pageSize: Int? = 0,
@SerializedName("pages") // @SerializedName("pages")
val pages: Int? = 0, // val pages: Int? = 0,
@SerializedName("prePage") // @SerializedName("prePage")
val prePage: Int? = 0, // val prePage: Int? = 0,
@SerializedName("size") // @SerializedName("size")
val size: Int? = 0, // val size: Int? = 0,
@SerializedName("startRow") // @SerializedName("startRow")
val startRow: Int? = 0, // val startRow: Int? = 0,
@SerializedName("total") // @SerializedName("total")
val total: Int? = 0 // val total: Int? = 0
) : Parcelable { //) : Parcelable {
@Parcelize // @Parcelize
data class Member( // data class Member(
// @SerializedName("cardBalance") //// @SerializedName("cardBalance")
// val cardBalance: String? = "", //// val cardBalance: String? = "",
// @SerializedName("cardCode") //// @SerializedName("cardCode")
// val cardCode: String? = "", //// val cardCode: String? = "",
// @SerializedName("consumptionCount") //// @SerializedName("consumptionCount")
// val consumptionCount: Int? = 0, //// val consumptionCount: Int? = 0,
// @SerializedName("consumptionTotal") //// @SerializedName("consumptionTotal")
// val consumptionTotal: String? = "", //// val consumptionTotal: String? = "",
// @SerializedName("createTime") //// @SerializedName("createTime")
// val createTime: String? = "", //// val createTime: String? = "",
// @SerializedName("firstTopUpTime") //// @SerializedName("firstTopUpTime")
// val firstTopUpTime: String? = "", //// val firstTopUpTime: String? = "",
// @SerializedName("id") //// @SerializedName("id")
// val id: Int? = 0, //// val id: Int? = 0,
// @SerializedName("integralBalance") //// @SerializedName("integralBalance")
// val integralBalance: Int? = 0, //// val integralBalance: Int? = 0,
// @SerializedName("lastConsumptionTime") //// @SerializedName("lastConsumptionTime")
// val lastConsumptionTime: String? = "", //// val lastConsumptionTime: String? = "",
// @SerializedName("memberFrom") //// @SerializedName("memberFrom")
// val memberFrom: Int? = 0, //// val memberFrom: Int? = 0,
// @SerializedName("name") //// @SerializedName("name")
//// val name: String? = "",
//// @SerializedName("phone")
//// val phone: String? = "",
//// @SerializedName("rewardBalance")
//// val rewardBalance: String? = "",
//// @SerializedName("topUpBalance")
//// val topUpBalance: String? = ""
// val id: String? = "",
// val faceId: String? = "",
// val name: String? = "", // val name: String? = "",
// @SerializedName("phone")
// val phone: String? = "", // val phone: String? = "",
// @SerializedName("rewardBalance") // ) : Parcelable
// val rewardBalance: String? = "", //}
// @SerializedName("topUpBalance")
// val topUpBalance: String? = ""
val id: String? = "",
val faceId: String? = "",
val name: String? = "",
val phone: String? = "",
) : Parcelable
}
@@ -2,7 +2,6 @@ package com.sw.platecabinet.model.response
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
/** /**
@@ -10,19 +9,6 @@ import kotlinx.parcelize.Parcelize
*/ */
@Parcelize @Parcelize
data class UserFaceModel( data class UserFaceModel(
// @SerializedName("faceFeature")
// val faceFeature: String? = "",
// @SerializedName("faceFeatureString")
// val faceFeatureString: String? = "",
// @SerializedName("faceType")
// val faceType: String? = "",
// @SerializedName("userFaceId")
// val userFaceId: String? = "",
// @SerializedName("userId")
// val userId: String? = "",
// @SerializedName("updateTimeStamp")
// val updateTimeStamp: Long = 0,
val userId: String? = "", val userId: String? = "",
val faceFeatureStr: String? = "", val faceFeatureStr: String? = "",
val faceUpdateTimestamp: Long? = null, val faceUpdateTimestamp: Long? = null,
@@ -33,10 +19,10 @@ data class UserFaceModel(
/** /**
* 会员编号 * 会员编号
*/ */
val cardNo: String, val cardNo: String? = null,
/** /**
* 是否会员 * 是否会员
*/ */
val isMember: Boolean val member: Boolean? = null
) : Parcelable ) : Parcelable
@@ -0,0 +1,44 @@
package com.sw.platecabinet.model.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import kotlinx.parcelize.Parcelize
/**
* 用户人脸信息
*/
@Parcelize
data class UserFaceModelV2(
val userFaceId: String? = null,
val userId: String? = null,
val faceFeature: String? = null,
val faceFeatureStr: String? = null,
val faceFeatureString: String? = null,
val faceUpdateTimestamp: Long? = null,
val cardNo: String? = null,
val member: Boolean? = false,
val faceDeleted: Boolean? = false,
val personType: String? = null
) : Parcelable {
fun resolveFeatureStr(): String? {
return faceFeature?.takeIf { it.isNotEmpty() }
?: faceFeatureStr?.takeIf { it.isNotEmpty() }
?: faceFeatureString?.takeIf { it.isNotEmpty() }
}
fun toFaceEntity(): FaceEntity {
val featureBase64 = resolveFeatureStr()
return FaceEntity(
userId,
android.util.Base64.decode(featureBase64, android.util.Base64.DEFAULT),
personType,
cardNo,
userId,
userFaceId,
member ?: false,
faceUpdateTimestamp ?: 0L
)
}
}
@@ -3,6 +3,7 @@ package com.sw.platecabinet.network
import com.sw.platecabinet.GlobalData import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.MyApp import com.sw.platecabinet.MyApp
import com.sw.platecabinet.network.api.ApiService import com.sw.platecabinet.network.api.ApiService
import com.sw.platecabinet.network.api.ApiServiceV2
import com.sw.platecabinet.network.interceptor.RequestInterceptor import com.sw.platecabinet.network.interceptor.RequestInterceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
@@ -37,4 +38,8 @@ object ApiClient {
val apiService: ApiService by lazy { val apiService: ApiService by lazy {
retrofit.create(ApiService::class.java) retrofit.create(ApiService::class.java)
} }
val apiServiceV2: ApiServiceV2 by lazy {
retrofit.create(ApiServiceV2::class.java)
}
} }
@@ -2,173 +2,28 @@ package com.sw.platecabinet.network.api
import com.sw.platecabinet.GlobalData 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.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.SearchResult
import com.sw.platecabinet.model.response.UserFaceModel import com.sw.platecabinet.model.response.UserFaceModel
import okhttp3.MultipartBody 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.Multipart import retrofit2.http.Multipart
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.Part import retrofit2.http.Part
import retrofit2.http.Query
import retrofit2.http.Url import retrofit2.http.Url
interface ApiService { interface ApiService {
// /**
// * device获取token
// */
// @GET("sys/getEquipmentToken")
// suspend fun getDeviceToken(
// @Query("qrcodeId") qrcodeId: String,
// @Query("appVersion") appVersion: String = GlobalData.appVersion
// ): ApiResponse<String>
//
// /**
// *获取配置信息
// */
// @GET("equipment/stEquipment/queryByEquipmentCode")
// suspend fun getDeviceInfo(
// @Query("equipmentCode") equipmentCode: String,
// @Query("appVersion") appVersion: String = GlobalData.appVersion,
// @Header("X-Access-Token") token: String
// ): ApiResponse<EquipmentInfo>
//
// /**
// * 生成token
// */
// @GET//("/shuwei-zhct/scales/generateToken")
// suspend fun generateToken(
// @Url url: String = "${GlobalData.appBaseUrl}/shuwei-zhct/scales/generateToken",
// @Query("deviceId") deviceId: String,
// @Query("appVersion") appVersion: String,
// @Query("equipmentCode") equipmentCode: String
// ): ApiResponse<String>
/** /**
* 获取人脸数据 * 获取人脸缓存数据
*/ */
// @GET//("/shuwei-zhct/scales/getUserFaceCache")
// suspend fun getUserFaceCache(
// @Url url: String = "${GlobalData.appBaseUrl}/shuwei-zhct/scales/getUserFaceCache",
// @Query("appVersion") appVersion: String,
// @Query("equipmentCode") equipmentCode: String
// ): ApiResponse<List<UserFaceModel>>
@POST @POST
suspend fun getUserFaceCache( suspend fun getUserFaceCache(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/faceFeature/list", @Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/faceFeature/list",
@Body param: Map<String, Int> @Body param: Map<String, Int>
): ApiResponse<List<UserFaceModel>> ): ApiResponse<List<UserFaceModel>>
// /**
// * 餐盘用户信息获取
// */
// @POST//("/shuwei-zhct/swEquipmentRelUser/equipmentBoxLogin")
// suspend fun equipmentBoxLogin(
// @Url url: String = "${GlobalData.appBaseUrl}/shuwei-zhct/swEquipmentRelUser/equipmentBoxLogin",
// @Body param: LoginParam
// ): ApiResponse<EquipmentUserInfo>
/**
* 餐盘用户信息获取
*/
@POST//("/shuwei-zhct/swEquipmentRelUser/equipmentBoxLogin")
suspend fun equipmentBoxLogin(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/sideboard/app/getPlateBoxUserInfo",
@Body param: LoginParam
): ApiResponse<EquipmentUserInfo>
/**
* 餐盘用户信息列表查询
*/
// @POST//("/shuwei-zhct/swEquipmentRelUser/list")
// suspend fun getEquipmentList(
// @Url url: String = "${GlobalData.appBaseUrl}/shuwei-zhct/swEquipmentRelUser/list",
// @Body param: EquipmentParam
// ): ApiResponse<List<EquipmentUserInfo>>
@GET
suspend fun getEquipmentList(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/sideboard/app/getYxMemberRefPlateByEquipmentCode",
): ApiResponse<List<EquipmentUserInfo>>
// /**
// * 餐盘用户信息绑定解绑
// */
// @POST//("/shuwei-zhct/swEquipmentRelUser/addOrEdit")
// suspend fun bindEquipment(
// @Url url: String = "${GlobalData.appBaseUrl}/shuwei-zhct/swEquipmentRelUser/addOrEdit",
// @Body param: BindParam
// ): ApiResponse<EquipmentUserInfo?>
/**
* 餐盘绑定
*/
@POST
suspend fun plateBind(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/sideboard/app/plateBinding",
@Body param: BindParam
): ApiResponse<EquipmentUserInfo?>
/**
* 餐盘解绑
*/
@POST
@FormUrlEncoded
suspend fun plateUnbind(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/sideboard/app/plateUnbind",
@Field("id") id: Long?
): ApiResponse<Any?>
/**
* 用户信息模糊搜索
*/
@POST//("/shuwei-user/swclientUserInfoShop/selectList")
suspend fun searchUser(
// @Url url: String = "${GlobalData.appBaseUrl}/shuwei-user/swclientUserInfoShop/selectList",
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/getUserInfoByNameOrPhone",
@Body param: SearchParam
): ApiResponse<List<SearchResult.Member>?>
// /**
// * 通过餐盘号获取信息
// */
// @POST//("/shuwei-zhct/swEquipmentRelUser/findByPlateNumber")
// suspend fun findByPlateNumber(
// @Url url: String = "${GlobalData.appBaseUrl}/shuwei-zhct/swEquipmentRelUser/findByPlateNumber",
// @Body param: BindParam
// ): ApiResponse<EquipmentUserInfo>
/**
* 通过餐盘号获取信息
*/
@GET
suspend fun findByPlateNumber(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/sideboard/app/getMemberRefPlateByPlateNumber",
@Query("plateNumber") plateNumber: String
): ApiResponse<EquipmentUserInfo>
/**
* 获取人脸增量数据
*
* @param timeDate 时间戳(毫秒级)
*/
// @GET//("/shuwei-zhct/scales/getHeartbeatInterface")
// suspend fun getHeartbeatInterface(
// @Url url: String = "${GlobalData.appBaseUrl}/shuwei-zhct/scales/getHeartbeatInterface",
// @Query("timeDate") timeDate: Long
// ): ApiResponse<List<UserFaceModel>>
/** /**
* 获取人脸增量数据 * 获取人脸增量数据
*/ */
@@ -193,10 +48,13 @@ interface ApiService {
@FormUrlEncoded @FormUrlEncoded
suspend fun addUserFace( suspend fun addUserFace(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/addUserByFace", @Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/addUserByFace",
@Field("featureChar") faceData:String, @Field("featureChar") faceData: String,
@Field("url") imageUr:String @Field("url") imageUr: String
): ApiResponse<UserFaceModel?> ): ApiResponse<UserFaceModel?>
/**
* 上传图片
*/
@Multipart @Multipart
@POST @POST
suspend fun uploadImage( suspend fun uploadImage(
@@ -0,0 +1,78 @@
package com.sw.platecabinet.network.api
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.model.DeviceConfigV2
import com.sw.platecabinet.model.request.FeatureBody
import com.sw.platecabinet.model.request.IdDTO
import com.sw.platecabinet.model.response.ApiResponse
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.model.response.UserFaceModelV2
import okhttp3.MultipartBody
import retrofit2.http.Body
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.Url
interface ApiServiceV2 {
/**
* 获取人脸缓存数据
*/
@POST
suspend fun getUserFaceCache(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/common/face/page",
@Body param: Map<String, Int>
): ApiResponse<List<UserFaceModelV2>>
/**
* 获取人脸增量数据
*/
@POST
suspend fun getFaceIncrementList(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/common/face/increment",
@Body param: Map<String, Long>
): ApiResponse<List<UserFaceModelV2>?>
/**
* 获取设备配置数据
*/
@GET
suspend fun getDeviceConfig(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/pickup/device/config"
): ApiResponse<DeviceConfigV2?>
/**
* 添加人脸数据
*/
@POST
suspend fun addUserFace(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/user/add-by-face",
@Body faceData: FeatureBody
): ApiResponse<UserFaceModelV2?>
/**
* 上传图片
*/
@Multipart
@POST
suspend fun uploadImage(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/upload",
@Part file: MultipartBody.Part,
): ApiResponse<String?>
/**
* 取餐盘(P-09a):记录用户开始就餐时刻
* 调用一次即 upsert 到主单 plate_pickup_time,是 P-09 提交就餐记录的强前置。
* @param param IdDTOid 为用户 id(餐盘号 = userId
* @return ApiResponse<String?>data 为主单 recordNo
*/
@POST
suspend fun platePickup(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/pickup/plate-pickup",
@Body param: IdDTO
): ApiResponse<String?>
}
@@ -1,10 +1,7 @@
package com.sw.platecabinet.repository package com.sw.platecabinet.repository
import com.sw.platecabinet.model.DeviceConfig import com.sw.platecabinet.model.DeviceConfig
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.EquipmentUserInfo
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 com.sw.platecabinet.utils.FileUtils
@@ -17,42 +14,6 @@ class RemoteRepository constructor(
private val apiService: ApiService private val apiService: ApiService
) : BaseRepository() { ) : BaseRepository() {
// /**
// * 生成token
// */
// suspend fun getDeviceToken(qrcodeId: String): ApiResponse<String> {
// return safeApiCall {
// apiService.getDeviceToken(
// qrcodeId
// )
// }
// }
//
// /**
// * 获取设备信息
// */
// suspend fun getDeviceInfo(equipmentCode: String, token: String): ApiResponse<EquipmentInfo> {
// return safeApiCall {
// apiService.getDeviceInfo(
// equipmentCode,
// token = token
// )
// }
// }
//
// /**
// * 生成token
// */
// suspend fun generateToken(deviceId: String): ApiResponse<String> {
// return safeApiCall {
// apiService.generateToken(
// deviceId = deviceId,
// appVersion = GlobalData.appVersion,
// equipmentCode = GlobalData.globalEquipmentCode
// )
// }
// }
/** /**
* 获取人脸数据 * 获取人脸数据
*/ */
@@ -70,57 +31,6 @@ class RemoteRepository constructor(
} }
} }
// /**
// * 登录
// */
// suspend fun equipmentBoxLogin(param: LoginParam): ApiResponse<EquipmentUserInfo> {
// return safeApiCall { apiService.equipmentBoxLogin(param = param) }
// }
// /**
// * 获取设备绑定用户列表
// */
// suspend fun getEquipmentList(): ApiResponse<List<EquipmentUserInfo>> {
// return safeApiCall { apiService.getEquipmentList() }
// }
// /**
// * 餐盘用户信息绑定解绑
// */
// suspend fun bindEquipment(param: BindParam): ApiResponse<EquipmentUserInfo?> {
// return safeApiCall { apiService.bindEquipment(param = param) }
// }
// suspend fun plateBind(param: BindParam): ApiResponse<EquipmentUserInfo?> {
// return safeApiCall { apiService.plateBind(param = param) }
// }
// suspend fun plateUnbind(id: Long?): ApiResponse<Any?> {
// return safeApiCall { apiService.plateUnbind(id = id) }
// }
/**
* 用户信息模糊搜索
*/
suspend fun searchUser(param: SearchParam): ApiResponse<List<SearchResult.Member>?> {
return safeApiCall { apiService.searchUser(param = param) }
}
/**
* 通过餐盘编号获取绑定信息
*/
suspend fun findByPlateNumber(plateNumber: String): ApiResponse<EquipmentUserInfo> {
return safeApiCall { apiService.findByPlateNumber(plateNumber = plateNumber) }
}
// /**
// * 获取人脸增量数据
// *
// * @param timeDate 时间戳(毫秒级)
// */
// suspend fun getHeartbeatInterface(timeDate: Long): ApiResponse<List<UserFaceModel>> {
// return safeApiCall { apiService.getHeartbeatInterface(timeDate = timeDate) }
// }
suspend fun getFaceIncrementList( suspend fun getFaceIncrementList(
pageNum: Long, pageNum: Long,
@@ -142,7 +52,7 @@ class RemoteRepository constructor(
return safeApiCall { apiService.getDeviceConfig() } return safeApiCall { apiService.getDeviceConfig() }
} }
suspend fun addUserFace(faceData: String, imageUr:String): ApiResponse<UserFaceModel?> { suspend fun addUserFace(faceData: String, imageUr: String): ApiResponse<UserFaceModel?> {
return safeApiCall { apiService.addUserFace(faceData = faceData, imageUr = imageUr) } return safeApiCall { apiService.addUserFace(faceData = faceData, imageUr = imageUr) }
} }
@@ -0,0 +1,82 @@
package com.sw.platecabinet.repository
import com.sw.platecabinet.model.DeviceConfigV2
import com.sw.platecabinet.model.request.FeatureBody
import com.sw.platecabinet.model.request.IdDTO
import com.sw.platecabinet.model.response.ApiResponse
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.model.response.UserFaceModelV2
import com.sw.platecabinet.network.api.ApiServiceV2
import com.sw.platecabinet.utils.FileUtils
import java.io.File
/**
* 远程数据处理
*/
class RemoteRepositoryV2 constructor(
private val apiService: ApiServiceV2
) : BaseRepository() {
/**
* 获取人脸数据
*/
suspend fun getUserFaceCache(
pageNum: Int,
pageSize: Int = 100,
): ApiResponse<List<UserFaceModelV2>> {
return safeApiCall {
apiService.getUserFaceCache(
param = mapOf(
"pageNum" to pageNum, "pageSize" to pageSize
)
)
}
}
suspend fun getFaceIncrementList(
pageNum: Long, pageSize: Long = 100L, timestamp: Long
): ApiResponse<List<UserFaceModelV2>?> {
return safeApiCall {
apiService.getFaceIncrementList(
param = mapOf(
"pageNum" to pageNum, "pageSize" to pageSize, "timestamp" to timestamp
)
)
}
}
suspend fun getDeviceConfig(): ApiResponse<DeviceConfigV2?> {
return safeApiCall { apiService.getDeviceConfig() }
}
suspend fun addUserFace(faceData: String, imageUrl: String): ApiResponse<UserFaceModelV2?> {
return safeApiCall {
apiService.addUserFace(
faceData = FeatureBody(
url = imageUrl, featureChar = faceData
)
)
}
}
suspend fun uploadImage(file: File): ApiResponse<String?> {
return safeApiCall {
val part = FileUtils.genRequestPart(file)
if (part == null) {
ApiResponse(code = "-1", msg = "解析图片失败")
} else {
apiService.uploadImage(file = part)
}
}
}
/**
* 取餐盘(P-09a):上报用户开始就餐时刻
* @param userId 用户 id(餐盘号 = userId
* @return ApiResponse<String?>data 为主单 recordNo
*/
suspend fun platePickup(userId: Long): ApiResponse<String?> {
return safeApiCall { apiService.platePickup(param = IdDTO(id = userId)) }
}
}
@@ -1,135 +0,0 @@
package com.sw.platecabinet.utils
import android.os.CountDownTimer
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.Log
/**
* 无状态残留的CountDownTimer封装(解决时间残留问题)
* @param millisInFuture 总时长(毫秒)
* @param countDownInterval 倒计时间隔(毫秒)
* @param onTick 倒计时回调(剩余毫秒)
* @param onFinish 结束回调
*/
class SafeCountDownTimer(
private val millisInFuture: Long,
private val countDownInterval: Long,
private val onTick: (Long) -> Unit,
private val onFinish: () -> Unit
) {
// 独立Handler,避免与其他CountDownTimer复用消息队列
private val handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
synchronized(this@SafeCountDownTimer) {
// 仅当timer未取消时处理消息,避免残留消息触发
if (!isCancelled) {
super.handleMessage(msg)
}
}
}
}
// 核心:标记是否已取消,避免残留消息触发
@Volatile
private var isCancelled = false
// 实际的CountDownTimer实例
private var timer: CountDownTimer? = null
/**
* 启动倒计时(启动前自动清空旧状态)
*/
fun start() {
// 第一步:强制取消旧实例+清空所有残留消息(核心!)
cancel()
// 第二步:重置取消标记
isCancelled = false
// 第三步:新建CountDownTimer实例,绑定独立Handler
timer = object : CountDownTimer(millisInFuture, countDownInterval) {
override fun onTick(millisUntilFinished: Long) {
// 双重校验:仅未取消时触发回调
if (!isCancelled) {
onTick.invoke(millisUntilFinished)
}
}
override fun onFinish() {
if (!isCancelled) {
onFinish.invoke()
// 结束后主动清空,避免残留
cancel()
}
}
}
// 第四步:启动新实例
timer?.start()
Log.d("SafeCountDownTimer", "倒计时启动,总时长:$millisInFuture 毫秒")
}
/**
* 取消倒计时(彻底清空所有状态)
*/
fun cancel() {
// 标记为已取消,拦截后续消息
isCancelled = true
// 取消timer实例
timer?.cancel()
timer = null
// 清空Handler所有残留消息(核心:解决旧消息触发问题)
handler.removeCallbacksAndMessages(null)
Log.d("SafeCountDownTimer", "倒计时已取消,清空所有残留状态")
}
}
// ========== 封装成工具类,方便全局调用 ==========
object CountDownManager {
// 保存当前倒计时实例,避免重复启动
private var currentTimer: SafeCountDownTimer? = null
/**
* 启动/重启倒计时(适配人脸识别防抖场景)
* @param totalSeconds 总秒数(如15
* @param onTick 每秒回调剩余秒数
* @param onFinish 结束回调
*/
fun startCountDown(
totalSeconds: Int = 15,
onTick: (Int) -> Unit = {},
onFinish: () -> Unit
) {
// 先取消旧倒计时
cancelCountDown()
// 新建安全倒计时实例
currentTimer = SafeCountDownTimer(
millisInFuture = totalSeconds * 1000L,
countDownInterval = 1000L,
onTick = { millis ->
// 转换为秒数回调
val sec = (millis / 1000).toInt() + 1
onTick.invoke(sec)
},
onFinish = {
onFinish.invoke()
}
)
// 启动倒计时
currentTimer?.start()
}
/**
* 取消当前倒计时
*/
fun cancelCountDown() {
currentTimer?.cancel()
currentTimer = null
}
}
@@ -1,33 +1,26 @@
package com.sw.platecabinet.utils package com.sw.platecabinet.utils
import android.util.Log
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import android.util.Log
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlin.ranges.downTo
/** /**
* 人脸识别防抖倒计时工具类(单例+防重复+无死循环 * 基于协程的倒计时工具类(实例化使用,各调用方持有独立实例,互不干扰
* 修复:onCompletion 区分正常结束与取消,手动取消时不触发 onFinish
*/ */
object CountDownUtil { class CountDownUtil {
// 原子布尔值:标记是否正在倒计时(防重复启动)
private val isCounting = AtomicBoolean(false)
// 当前倒计时Job(用于取消) private val isCounting = AtomicBoolean(false)
private var currentJob: Job? = null private var currentJob: Job? = null
/** /**
* 启动/重启防抖倒计时(同一时间仅一个倒计时运行 * 启动倒计时(若已在运行则忽略,需重启请先调用 cancelCountDown
* @param totalSeconds 倒计时总秒数(如15 * @param total 总秒数
* @param lifecycleScope Activity/FragmentlifecycleScope(必须) * @param lifecycleScope Activity/FragmentlifecycleScope
* @param onStart 启动回调(主线程)
* @param onTick 每秒回调剩余秒数(主线程) * @param onTick 每秒回调剩余秒数(主线程)
* @param onFinish 倒计时结束回调(主线程) * @param onFinish 正常结束回调(主线程,手动取消时不触发
*/ */
fun startCountDown( fun startCountDown(
total: Int = 15, total: Int = 15,
@@ -36,46 +29,37 @@ object CountDownUtil {
onTick: (Int) -> Unit = {}, onTick: (Int) -> Unit = {},
onFinish: () -> Unit onFinish: () -> Unit
) { ) {
// 1. 防重复:如果正在倒计时,先取消旧任务 if (isCounting.get()) return
if (isCounting.get()) {
//cancelCountDown()
return
}
// 2. 标记为正在倒计时
isCounting.set(true) isCounting.set(true)
// 3. 启动新倒计时
currentJob = flow { currentJob = flow {
Log.d("CountDownUtil", "倒计时启动,总时长:$total")
for (i in total downTo 1) { for (i in total downTo 1) {
emit(i) emit(i)
delay(1000) // IO线程等待1秒,无死循环 delay(1000L)
} }
} }
.flowOn(Dispatchers.IO) // 上游IO线程,避免阻塞主线程 .flowOn(Dispatchers.IO)
.onStart { .onStart {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) { onStart() }
onStart.invoke()
}
} }
.onEach { remaining -> .onEach { remaining ->
// 主线程回调剩余时间 withContext(Dispatchers.Main) { onTick(remaining) }
withContext(Dispatchers.Main) {
onTick.invoke(remaining)
} }
} .onCompletion { cause ->
.onCompletion {
// 倒计时结束:重置状态+回调
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
isCounting.set(false) // 重置标记 isCounting.set(false)
currentJob = null // 置空Job currentJob = null
// cause == null 为正常结束,有异常(含 CancellationException)时不触发 onFinish
if (cause == null) {
Log.d("CountDownUtil", "倒计时正常结束") Log.d("CountDownUtil", "倒计时正常结束")
onFinish.invoke() onFinish()
} else {
Log.d("CountDownUtil", "倒计时被取消:${cause.message}")
}
} }
} }
.catch { e -> .catch { e ->
// 捕获异常:避免崩溃,重置状态
Log.e("CountDownUtil", "倒计时异常:${e.message}", e) Log.e("CountDownUtil", "倒计时异常:${e.message}", e)
isCounting.set(false) isCounting.set(false)
currentJob = null currentJob = null
@@ -84,12 +68,12 @@ object CountDownUtil {
} }
/** /**
* 取消倒计时(手动停止 * 取消倒计时(不触发 onFinish
*/ */
fun cancelCountDown() { fun cancelCountDown() {
currentJob?.cancel() // 取消Job currentJob?.cancel()
currentJob = null // 置空,避免残留 currentJob = null
isCounting.set(false) // 重置标记 isCounting.set(false)
Log.d("CountDownUtil", "倒计时被手动取消") Log.d("CountDownUtil", "倒计时取消")
} }
} }
@@ -154,7 +154,7 @@ class CrashHandler private constructor(private val context: Context) :
// 设备信息 // 设备信息
sb.append("Vendor: ${Build.MANUFACTURER}\n") sb.append("Vendor: ${Build.MANUFACTURER}\n")
sb.append("Model: ${Build.MODEL}\n") sb.append("Model: ${Build.MODEL}\n")
sb.append("CPU ABI: ${Build.SUPPORTED_ABIS[0]}\n") sb.append("CPU ABI: ${Build.SUPPORTED_ABIS.firstOrNull() ?: "unknown"}\n")
// 其他信息 // 其他信息
sb.append("Locale: ${Locale.getDefault()}\n") sb.append("Locale: ${Locale.getDefault()}\n")
@@ -28,10 +28,10 @@ object DateTimeUtils {
} }
/** /**
* 获取带时间的完整中文格式(示例:2025年6月11日 星期三 14:30 * 获取带时间的完整中文格式(示例:2025年06月11日 星期三 14:30
*/ */
fun getChineseDateTimeString(date: Date = Date()): String { fun getChineseDateTimeString(date: Date = Date()): String {
return SimpleDateFormat("yyyy年Md日 EEEE HH:mm:ss", Locale.CHINA).format(date) return SimpleDateFormat("yyyy年MM月dd日 EEEE HH:mm", Locale.CHINA).format(date)
} }
// 使用线程安全的日期格式化(避免 SimpleDateFormat 的线程安全问题) // 使用线程安全的日期格式化(避免 SimpleDateFormat 的线程安全问题)
@@ -52,7 +52,7 @@ object DateTimeUtils {
*/ */
fun realTimeChineseDateFlow(intervalMillis: Long = 1000) = flow { fun realTimeChineseDateFlow(intervalMillis: Long = 1000) = flow {
while (true) { while (true) {
emit(getChineseDateTimePair()) emit(getChineseDateTimeString())
delay(intervalMillis) delay(intervalMillis)
} }
} }
@@ -1,6 +1,11 @@
package com.sw.platecabinet.utils package com.sw.platecabinet.utils
import timber.log.Timber
class Debouncer(private val delayMillis: Long) { class Debouncer(private val delayMillis: Long) {
companion object {
const val TAG = "Debouncer"
}
private var lastActionTime = 0L private var lastActionTime = 0L
/** /**
@@ -8,13 +13,15 @@ class Debouncer(private val delayMillis: Long) {
* @param action 要执行的操作 * @param action 要执行的操作
* @return Boolean 是否执行了操作 (true=已执行, false=被防抖) * @return Boolean 是否执行了操作 (true=已执行, false=被防抖)
*/ */
fun debounce(action: () -> Unit): Boolean { fun debounce(tag:String = TAG, action: () -> Unit): Boolean {
val currentTime = System.currentTimeMillis() val currentTime = System.currentTimeMillis()
if (currentTime - lastActionTime >= delayMillis) { if (currentTime - lastActionTime >= delayMillis) {
Timber.tag(tag).d("collectFace,recognizeUserId防止重复操作:执行action回调")
lastActionTime = currentTime lastActionTime = currentTime
action() action()
return true return true
} }
Timber.tag(tag).d("collectFace,recognizeUserId防止重复操作-----------------------")
return false return false
} }
@@ -1,88 +1,88 @@
package com.sw.platecabinet.utils //package com.sw.platecabinet.utils
//
import com.sw.platecabinet.model.response.EquipmentUserInfo //import com.sw.platecabinet.model.response.EquipmentUserInfo
import timber.log.Timber //import timber.log.Timber
import kotlin.math.ceil //import kotlin.math.ceil
//
/** ///**
* 列表排序工具类 // * 列表排序工具类
*/ // */
object ListArrangementUtil { //object ListArrangementUtil {
//
/** // /**
* page item水平排序 // * page item水平排序
*/ // */
fun horizontalSortPageItem( // fun horizontalSortPageItem(
list: List<EquipmentUserInfo>, // list: List<EquipmentUserInfo>,
columns: Int, // columns: Int,
defaultValue: EquipmentUserInfo = EquipmentUserInfo() // defaultValue: EquipmentUserInfo = EquipmentUserInfo()
): List<EquipmentUserInfo> { // ): List<EquipmentUserInfo> {
Timber.d("horizontalSortPageItem") // Timber.d("horizontalSortPageItem")
require(columns > 0) { "Columns must be positive" } // require(columns > 0) { "Columns must be positive" }
//
val filledList = list.padToMultiple(columns, defaultValue) // val filledList = list.padToMultiple(columns, defaultValue)
val chunks = filledList.chunked(columns) // val chunks = filledList.chunked(columns)
//
return (0 until columns) // return (0 until columns)
.windowed(2, 2, partialWindows = true) // .windowed(2, 2, partialWindows = true)
.flatMap { group -> // .flatMap { group ->
when (group.size) { // when (group.size) {
2 -> chunks.flatMap { listOf(it[group[0]], it[group[1]]) } // 2 -> chunks.flatMap { listOf(it[group[0]], it[group[1]]) }
1 -> chunks.flatMap { // 1 -> chunks.flatMap {
listOf(it[group[0]], EquipmentUserInfo(equipmentBoxCode = null)) // listOf(it[group[0]], EquipmentUserInfo(equipmentBoxCode = null))
}.dropLast(1) // }.dropLast(1)
else -> error("Unexpected group size") // else -> error("Unexpected group size")
} // }
} // }
} // }
//
/** // /**
* item 垂直排序 // * item 垂直排序
*/ // */
fun verticalSortItem(list: List<EquipmentUserInfo>): List<EquipmentUserInfo> { // fun verticalSortItem(list: List<EquipmentUserInfo>): List<EquipmentUserInfo> {
Timber.d("verticalSortItem") // Timber.d("verticalSortItem")
val users = list.padToEvenSize() // val users = list.padToEvenSize()
val half = users.size / 2 // val half = users.size / 2
//
return users.take(half) // return users.take(half)
.zip(users.drop(half)) { a, b -> listOf(a, b) } // .zip(users.drop(half)) { a, b -> listOf(a, b) }
.flatten() // .flatten()
} // }
//
/** // /**
* page item垂直排序 // * page item垂直排序
*/ // */
fun convertToColumnFirst( // fun convertToColumnFirst(
original: List<EquipmentUserInfo>, // original: List<EquipmentUserInfo>,
cols: Int // cols: Int
): List<EquipmentUserInfo> { // ): List<EquipmentUserInfo> {
Timber.d("convertToColumnFirst") // Timber.d("convertToColumnFirst")
require(cols > 0) { "Columns must be positive" } // require(cols > 0) { "Columns must be positive" }
//
val rows = ceil(original.size.toDouble() / cols).toInt() // val rows = ceil(original.size.toDouble() / cols).toInt()
return List(original.size) { pos -> // return List(original.size) { pos ->
original.getOrNull(pos % rows * cols + pos / rows) ?: EquipmentUserInfo() // original.getOrNull(pos % rows * cols + pos / rows) ?: EquipmentUserInfo()
} // }
} // }
//
// 提取的扩展函数 // // 提取的扩展函数
private fun List<EquipmentUserInfo>.padToMultiple( // private fun List<EquipmentUserInfo>.padToMultiple(
multiple: Int, // multiple: Int,
defaultValue: EquipmentUserInfo // defaultValue: EquipmentUserInfo
): List<EquipmentUserInfo> { // ): List<EquipmentUserInfo> {
return if (size % multiple != 0) { // return if (size % multiple != 0) {
this + List(multiple - (size % multiple)) { defaultValue } // this + List(multiple - (size % multiple)) { defaultValue }
} else { // } else {
this // this
} // }
} // }
//
private fun List<EquipmentUserInfo>.padToEvenSize(): List<EquipmentUserInfo> { // private fun List<EquipmentUserInfo>.padToEvenSize(): List<EquipmentUserInfo> {
return if (size % 2 != 0) { // return if (size % 2 != 0) {
Timber.d("列表长度必须是偶数") // Timber.d("列表长度必须是偶数")
this + EquipmentUserInfo() // this + EquipmentUserInfo()
} else { // } else {
this // this
} // }
} // }
} //}
@@ -19,10 +19,15 @@ import java.io.StringReader;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class LogFileUtil { 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 = new File(App.getContext().getFilesDir(), "logs").getAbsolutePath();
// 单线程执行器,保证日志串行写入,避免多线程并发竞争
private static final ExecutorService LOG_EXECUTOR = Executors.newSingleThreadExecutor();
public static final int SIZETYPE_B = 1;//获取文件大小单位为B的double值 public static final int SIZETYPE_B = 1;//获取文件大小单位为B的double值
public static final int SIZETYPE_KB = 2;//获取文件大小单位为KB的double值 public static final int SIZETYPE_KB = 2;//获取文件大小单位为KB的double值
@@ -610,10 +615,8 @@ public class LogFileUtil {
if (!isSaveLog) { if (!isSaveLog) {
return; return;
} }
new Thread() { // 提交到单线程队列,串行执行,避免并发写入竞争
@Override LOG_EXECUTOR.execute(() -> {
public void run() {
super.run();
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
for (String log : logs) { for (String log : logs) {
sb.append("===") sb.append("===")
@@ -625,9 +628,7 @@ public class LogFileUtil {
} }
String fileName = "log" + AppUtil.formatDateGetDay(System.currentTimeMillis()) + ".txt"; String fileName = "log" + AppUtil.formatDateGetDay(System.currentTimeMillis()) + ".txt";
LogFileUtil.saveStrFile(sb.toString(), fileName, FILE_STR_PATH, true); LogFileUtil.saveStrFile(sb.toString(), fileName, FILE_STR_PATH, true);
});
}
}.start();
} }
private static final String[][] MIME_MapTable = { private static final String[][] MIME_MapTable = {
@@ -77,11 +77,7 @@ class PermissionHelper private constructor(
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null) data = Uri.fromParts("package", context.packageName, null)
} }
if (context is Activity) {
context.startActivity(intent) context.startActivity(intent)
} else if (context is Fragment) {
context.startActivity(intent)
}
} }
} }
@@ -1,13 +1,7 @@
package com.sw.platecabinet.utils package com.sw.platecabinet.utils
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.comn.SerialApi
import com.sw.plate.utils.comn.SerialPortManager
import com.sw.platecabinet.activity.BaseActivity import com.sw.platecabinet.activity.BaseActivity
import com.sw.platecabinet.activity.MainActivity
import com.sw.platecabinet.activity.PageType
import timber.log.Timber
object PlateUtils { object PlateUtils {
@@ -19,48 +13,54 @@ object PlateUtils {
callback: () -> Unit = {} callback: () -> Unit = {}
) { ) {
val activity = fragmentActivity as BaseActivity<*> val activity = fragmentActivity as BaseActivity<*>
Timber.d("itemClick llOpen equipmentBoxCode = ${code}")
val equipmentBoxCode: Int = code?.toInt() ?: -1 activity.window.decorView.postDelayed({
if (equipmentBoxCode < 0) {
ToastUtils.showToast("餐盘柜编号错误")
return
}
activity.showWaitingDialog("请稍等")
SerialApi.openPlate(equipmentBoxCode, object : SerialPortManager.SendCallback {
override fun onSuccess() {
activity.hideWaitingDialog()
if (isSetting.not()) {
MainActivity.start(
activity,
pageType = PageType.PLATE_TIP,
isScanCode = isScanCode
)
} else {
ToastUtils.showToast("柜门打开成功")
callback() callback()
} }, 1500)
// activity.showWaitingDialog("请稍等")
// Timber.d("itemClick llOpen equipmentBoxCode = ${code}")
// val equipmentBoxCode: Int = code?.toInt() ?: -1
// if (equipmentBoxCode < 0) {
// ToastUtils.showToast("餐盘柜编号错误")
// return
// }
// activity.showWaitingDialog("请稍等")
// SerialApi.openPlate(equipmentBoxCode, object : SerialPortManager.SendCallback {
// override fun onSuccess() {
// activity.hideWaitingDialog()
// if (isSetting.not()) {
// MainActivity.start(
// activity,
// pageType = PageType.PLATE_TIP,
// isScanCode = isScanCode
// )
// } else {
// ToastUtils.showToast("柜门打开成功")
// callback()
// }
// }
//
// override fun onFail(e: Exception?) {
// activity.hideWaitingDialog()
// ToastUtils.showToast("柜门打开失败")
// callback()
// }
// })
} }
override fun onFail(e: Exception?) { // fun openDirect(boxCode: String?) {
activity.hideWaitingDialog() // val boxNumber = boxCode?.toIntOrNull() ?: -1
ToastUtils.showToast("柜门打开失败") // if (boxNumber < 0) return
callback() // SerialApi.openPlate(boxNumber, object : SerialPortManager.SendCallback {
} // override fun onSuccess() {
}) //
} // }
//
fun openDirect(boxCode: String?) { // override fun onFail(e: Exception?) {
val boxNumber = boxCode?.toIntOrNull() ?: -1 //
if (boxNumber < 0) return // }
SerialApi.openPlate(boxNumber, object : SerialPortManager.SendCallback { // })
override fun onSuccess() { // }
}
override fun onFail(e: Exception?) {
}
})
}
} }
@@ -1,15 +1,16 @@
package com.sw.platecabinet.utils package com.sw.platecabinet.utils
import com.google.gson.Gson
import com.sw.inbound.utils.GsonUtils
import com.sw.inbound.utils.SPUtil import com.sw.inbound.utils.SPUtil
import com.sw.inbound.utils.SPUtil.Companion.getInstance import com.sw.inbound.utils.SPUtil.Companion.getInstance
import com.sw.platecabinet.GlobalKey
import com.sw.platecabinet.MyApp import com.sw.platecabinet.MyApp
import com.sw.platecabinet.model.response.EquipmentUserInfo
object SpTool { object SpTool {
const val LAST_FACE_TIMESTAMP: String = "faceTimestamp" const val LAST_FACE_TIMESTAMP: String = "faceTimestamp"
const val PLATE_BOX_DATA: String = "plateBoxBind" const val PLATE_BOX_DATA: String = "plateBoxBind"
const val IS_FIRST_GET_FACE = "isFirstGetFace"
private var spUtil: SPUtil? = null private var spUtil: SPUtil? = null
init { init {
@@ -26,17 +27,29 @@ object SpTool {
spUtil?.put(LAST_FACE_TIMESTAMP, timestamp) spUtil?.put(LAST_FACE_TIMESTAMP, timestamp)
} }
fun savePlateData(items: List<EquipmentUserInfo>) { // fun savePlateData(items: List<EquipmentUserInfo>) {
val jsonData = Gson().toJson(items) // val jsonData = Gson().toJson(items)
putString(PLATE_BOX_DATA, jsonData) // putString(PLATE_BOX_DATA, jsonData)
// }
//
// fun getPlateData(): List<EquipmentUserInfo>? {
// val jsonData = spUtil?.get(PLATE_BOX_DATA, "")
// if (jsonData.isNullOrBlank()) {
// return null
// }
// return GsonUtils.fromJsonList(jsonData, EquipmentUserInfo::class.java)
// }
var firstGetFace: Boolean
get() = spUtil?.get(IS_FIRST_GET_FACE, true)?:true
set(value) {
spUtil?.boolean(IS_FIRST_GET_FACE, value)
} }
fun getPlateData(): List<EquipmentUserInfo>? { var baseUrl: String
val jsonData = spUtil?.get(PLATE_BOX_DATA, "") get() = spUtil?.get(GlobalKey.KEY_BASE_URL, "") ?: ""
if (jsonData.isNullOrBlank()) { set(value) {
return null spUtil?.put(GlobalKey.KEY_BASE_URL, value)
}
return GsonUtils.fromJsonList(jsonData, EquipmentUserInfo::class.java)
} }
} }
@@ -0,0 +1,16 @@
package com.sw.platecabinet.utils.mego
/**
* PLC 操作统一回调接口
*/
interface PlcCallback<T> {
/**
* 操作成功回调(主线程)
*/
fun onSuccess(result: T)
/**
* 操作失败回调(主线程)
*/
fun onError(message: String)
}
@@ -0,0 +1,229 @@
package com.sw.platecabinet.utils.mego
import android.content.Context
import android.os.Handler
import android.os.Looper
import com.megoai.plc_aar.SDK
import java.util.concurrent.atomic.AtomicBoolean
/**
* PLC 操作工具类(单例)
* 封装 SDK 所有接口,统一回调到主线程
*
* 注意:底层 PLC SDKFlutter MethodChannel 封装)的 Listener 只有 onMessage 一个回调,
* Flutter 侧 error()/notImplemented() 均为空实现,串口异常或引擎未就绪时不会回调。
* 因此本类对所有动作类、查询类接口统一加超时兜底:超时未响应则触发 onError,
* 保证上层回调链不会因“无响应”而永久挂起。
*/
class PlcHelper private constructor(context: Context) {
private val sdk: SDK = SDK.getInstance(context)
private val mainHandler = Handler(Looper.getMainLooper())
companion object {
@Volatile
private var instance: PlcHelper? = null
/**
* 获取单例实例
*/
fun getInstance(context: Context): PlcHelper {
return instance ?: synchronized(this) {
instance ?: PlcHelper(context.applicationContext).also { instance = it }
}
}
}
// ==================== 超时兜底封装 ====================
/**
* 包装 SDK 动作/查询调用:超时未响应时主动触发 onError。
*
* 背景:底层 SDK 的 Listener 仅在 Flutter 侧 success() 时回调 onMessage
* error()/notImplemented() 为空实现,串口异常时 Android 侧收不到任何回调,
* 会导致上层回调永久挂起。本方法用定时器兜底,并保证 onSuccess/onError 至多触发一次。
*
* @param timeoutMs 超时时长(毫秒)
* @param callback 上层回调(统一在主线程回调)
* @param action 实际发起 SDK 调用的逻辑,参数为经过防重入包装的“安全回调”,
* 内部所有成功/失败出口都应通过该回调触发
* @param <T> 回调结果类型
*/
private inline fun <T> withTimeout(
timeoutMs: Long,
callback: PlcCallback<T>,
crossinline action: (safeCallback: PlcCallback<T>) -> Unit
) {
// 标记回调是否已触发,保证 onSuccess/onError 至多执行一次
val fired = AtomicBoolean(false)
// 包装后的安全回调,所有出口都经过它,避免重复回调
val safeCallback = object : PlcCallback<T> {
override fun onSuccess(result: T) {
if (fired.compareAndSet(false, true)) callback.onSuccess(result)
}
override fun onError(message: String) {
if (fired.compareAndSet(false, true)) callback.onError(message)
}
}
// 超时定时器:到期未收到 SDK 回调则触发 onError
val timeoutRunnable = Runnable {
safeCallback.onError("PLC 操作超时(${timeoutMs}ms)未响应")
}
mainHandler.postDelayed(timeoutRunnable, timeoutMs)
// 发起实际 SDK 调用,所有成功/失败都通过 safeCallback 回调
action(object : PlcCallback<T> {
override fun onSuccess(result: T) {
// 成功后取消超时定时器
mainHandler.removeCallbacks(timeoutRunnable)
safeCallback.onSuccess(result)
}
override fun onError(message: String) {
// 失败同样取消超时定时器
mainHandler.removeCallbacks(timeoutRunnable)
safeCallback.onError(message)
}
})
}
// ==================== 连接与模式 ====================
/**
* 打开出盘串口(应用启动时调用)
*/
fun openSerialPort(callback: PlcCallback<String>) {
sdk.openPlcSerialPort { result ->
mainHandler.post { callback.onSuccess(result.toString()) }
}
}
/**
* 打开自动模式(应用启动时调用一次)
*/
fun openAutoMode(callback: PlcCallback<String>) {
withTimeout(5000, callback) { safe ->
sdk.actionOpenAutoMode { result ->
mainHandler.post { safe.onSuccess(result.toString()) }
}
}
}
/**
* 故障复位(出现 alarmCode 或补盘完成后调用)
*/
fun alarmReset(callback: PlcCallback<String>) {
withTimeout(5000, callback) { safe ->
sdk.actionAlarmReset { result ->
mainHandler.post { safe.onSuccess(result.toString()) }
}
}
}
// ==================== 出盘操作 ====================
/**
* 出盘(机械动作,超时 10s)
*/
fun outPlate(callback: PlcCallback<String>) {
withTimeout(10000, callback) { safe ->
sdk.actionOutPlate { result ->
mainHandler.post { safe.onSuccess(result.toString()) }
}
}
}
// ==================== 平台控制 ====================
/**
* 平台下降(开始补盘,超时 8s)
*/
fun platformDown(callback: PlcCallback<String>) {
withTimeout(8000, callback) { safe ->
sdk.actionPlatformDown { result ->
mainHandler.post { safe.onSuccess(result.toString()) }
}
}
}
/**
* 平台上升(补盘完成,超时 8s)
*/
fun platformUp(callback: PlcCallback<String>) {
withTimeout(8000, callback) { safe ->
sdk.actionPlatformDownStop { result ->
mainHandler.post { safe.onSuccess(result.toString()) }
}
}
}
/**
* 补盘完成返回营业(自动模式 + 故障复位,超时 5s)
*/
fun backToBusiness(callback: PlcCallback<String>) {
withTimeout(5000, callback) { safe ->
sdk.actionOpenAutoModeAndActionAlarmReset { result ->
mainHandler.post { safe.onSuccess(result.toString()) }
}
}
}
// ==================== 状态查询 ====================
/**
* 获取 PLC 状态(解析为 PlcStatus 对象,超时 3s
*/
fun getStatus(callback: PlcCallback<PlcStatus>) {
withTimeout(3000, callback) { safe ->
sdk.getPlcStatus { json ->
mainHandler.post {
if (json != null) {
safe.onSuccess(PlcStatus.Companion.fromJson(json.toString()))
} else {
safe.onError("获取状态失败")
}
}
}
}
}
/**
* 获取 PLC 状态(原始 JSON 字符串,超时 3s)
*/
fun getStatusJson(callback: PlcCallback<String>) {
withTimeout(3000, callback) { safe ->
sdk.getPlcStatus { json ->
mainHandler.post {
if (json != null) safe.onSuccess(json.toString())
else safe.onError("获取状态失败")
}
}
}
}
// ==================== 参数设置 ====================
/**
* 设置 PLC 参数(超时 3s)
* @param paramType 参数类型(如 "18FD" 表示主电机运行最长时间)
* @param paramValue 参数值(int,单位 0.1s
*/
fun setParams(paramType: String, paramValue: Int, callback: PlcCallback<String>) {
withTimeout(3000, callback) { safe ->
sdk.setPLCParams(
{ result -> mainHandler.post { safe.onSuccess(result.toString()) } },
paramType,
paramValue
)
}
}
// ==================== 生命周期 ====================
/**
* 释放资源,清除单例
*/
fun release() {
instance = null
}
}
@@ -0,0 +1,34 @@
package com.sw.platecabinet.utils.mego
import org.json.JSONObject
/**
* PLC 状态数据类
*/
data class PlcStatus(
val isEmptyPlate: Boolean = false, // 是否空盘
val isPlateformDown: Boolean = false, // 平台是否在下位
val isPlateformUp: Boolean = false, // 平台是否在上位
val isTakePlateInfrared: Boolean = false, // 取盘红外是否触发
val alarmCode: Int = 0 // 报警代码
) {
companion object {
/**
* 从 JSON 字符串解析状态
*/
fun fromJson(json: String): PlcStatus {
return try {
val obj = JSONObject(json)
PlcStatus(
isEmptyPlate = obj.optBoolean("isEmptyPlate", false),
isPlateformDown = obj.optBoolean("isPlateformDown", false),
isPlateformUp = obj.optBoolean("isPlateformUp", false),
isTakePlateInfrared = obj.optBoolean("isTakePlateInfrared", false),
alarmCode = obj.optInt("alarmCode", 0)
)
} catch (e: Exception) {
PlcStatus()
}
}
}
}
@@ -1,76 +1,74 @@
package com.sw.platecabinet.viewmodel //package com.sw.platecabinet.viewmodel
//
import androidx.lifecycle.ViewModel //import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope //import androidx.lifecycle.viewModelScope
import com.sw.plate.utils.ToastUtils //import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.GlobalData //import com.sw.platecabinet.model.response.ApiResponse
import com.sw.platecabinet.model.response.ApiResponse //import com.sw.platecabinet.network.ApiClient
import com.sw.platecabinet.model.response.EquipmentInfo //import com.sw.platecabinet.repository.RemoteRepository
import com.sw.platecabinet.network.ApiClient //import kotlinx.coroutines.flow.MutableStateFlow
import com.sw.platecabinet.repository.RemoteRepository //import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.MutableStateFlow //import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.StateFlow //import timber.log.Timber
import kotlinx.coroutines.launch //
import timber.log.Timber //abstract class BaseViewModel() : ViewModel() {
// protected val repository = RemoteRepository(ApiClient.apiService)
abstract class BaseViewModel() : ViewModel() { //
protected val repository = RemoteRepository(ApiClient.apiService) // // 显示进度条
// private val _showLoading = MutableStateFlow<Boolean>(false)
// 显示进度条 // val showLoading: StateFlow<Boolean> = _showLoading
private val _showLoading = MutableStateFlow<Boolean>(false) //
val showLoading: StateFlow<Boolean> = _showLoading // /**
// * 带进度条的请求
/** // */
* 带进度条的请求 // protected fun launchWithLoading(block: suspend () -> Unit) {
*/ // viewModelScope.launch {
protected fun launchWithLoading(block: suspend () -> Unit) { // try {
viewModelScope.launch { // _showLoading.value = true
try { // block()
_showLoading.value = true // } catch (e: Exception) {
block() // // 错误处理可被子类重写
} catch (e: Exception) { // handleError(e)
// 错误处理可被子类重写 // } finally {
handleError(e) // _showLoading.value = false
} finally { // }
_showLoading.value = false // }
} // }
} //
} // /**
// * 不带进度条的请求
/** // */
* 不带进度条的请求 // protected fun launch(block: suspend () -> Unit) {
*/ // viewModelScope.launch() {
protected fun launch(block: suspend () -> Unit) { // try {
viewModelScope.launch() { // block()
try { // } catch (e: Exception) {
block() // // 错误处理可被子类重写
} catch (e: Exception) { // handleError(e)
// 错误处理可被子类重写 // }
handleError(e) // }
} // }
} //
} // protected open fun parseResponse(response: ApiResponse<*>): Boolean {
// if (response.isSuccess()) {
protected open fun parseResponse(response: ApiResponse<*>): Boolean { // return true
if (response.isSuccess()) { // }
return true // Timber.d("msg = ${response.msg}, code = ${response.code}")
} // ToastUtils.showToast("${response.msg}(${response.code})")
Timber.d("msg = ${response.msg}, code = ${response.code}") // return false
ToastUtils.showToast("${response.msg}(${response.code})") // }
return false //
} //
// fun handleError(exception: Exception) {
// Timber.d("handleError ${exception.message}")
fun handleError(exception: Exception) { // }
Timber.d("handleError ${exception.message}") //
} //// fun parseEquipmentInfo(equipmentInfo: EquipmentInfo) {
////// GlobalData.appBaseUrl = equipmentInfo.appPackageUrl!!
fun parseEquipmentInfo(equipmentInfo: EquipmentInfo) { ////// GlobalData.appBaseUrl="https://vip.shuziweidao.com"
// GlobalData.appBaseUrl = equipmentInfo.appPackageUrl!! ////// GlobalData.sdkKey = equipmentInfo.arcsoftSdkKey!!
// GlobalData.appBaseUrl="https://vip.shuziweidao.com" ////// GlobalData.appId = equipmentInfo.arcsoftAppId!!
// GlobalData.sdkKey = equipmentInfo.arcsoftSdkKey!! ////// GlobalData.activeKey = equipmentInfo.arcsoftActiveKey!!
// GlobalData.appId = equipmentInfo.arcsoftAppId!! ////// GlobalData.restId = equipmentInfo.canteenId!!
// GlobalData.activeKey = equipmentInfo.arcsoftActiveKey!! //// }
// GlobalData.restId = equipmentInfo.canteenId!! //}
}
}
@@ -0,0 +1,325 @@
package com.sw.platecabinet.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.arcsoft.face.ErrorInfo
import com.sw.inbound.utils.SPUtil
import com.sw.plate.App
import com.sw.plate.utils.Base64
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.FaceApi
import com.sw.plate.utils.arcface.facedb.FaceDatabase
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.GlobalKey
import com.sw.platecabinet.model.DeviceConfigV2
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.model.response.UserFaceModelV2
import com.sw.platecabinet.network.ApiClient
import com.sw.platecabinet.repository.RemoteRepositoryV2
import com.sw.platecabinet.utils.SpTool
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
/**
* 用户viewmodel
*/
class NetViewModelV2 : ViewModel() {
companion object {
const val PAGE_SIZE = 100
}
private val faceApi: FaceApi = FaceApi()
private val repository = RemoteRepositoryV2(ApiClient.apiServiceV2)
//fun getUserFace(
// pageNo: Int,
// pageSize: Int,
// callback: (Boolean, List<UserFaceModelV2>) -> Unit
//) {
// viewModelScope.launch {
// val response = repository.getUserFaceCache(pageNum = pageNo, pageSize = pageSize)
// if (response.isSuccess()) callback(true, response.data ?: emptyList())
// else callback(false, emptyList())
// }
//}
/**
* 获取人脸数据
*/
fun getUserFaceCache(
pageNo: Int = 1,
pageSize: Int = PAGE_SIZE,
callback: (Boolean, String?) -> Unit = { _, _ -> }
) {
var currentPageNo = pageNo
viewModelScope.launch {
val response = repository.getUserFaceCache(pageNo, pageSize)
if (response.isSuccess().not()) {
callback(false, response.msg)
return@launch
}
// 获取成功一次后缓存状态
SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true)
val list = response.data ?: emptyList()
if (pageNo == 1 && list.isEmpty()) {
callback(false, "暂无数据")
return@launch
}
val faceEntityList = list.mapNotNull { face ->
val featureBase64 = face.resolveFeatureStr()
if (featureBase64.isNullOrBlank()) null
else {
val featureData = base64ToByteArray(featureBase64)
if (featureData == null) null
else face.toFaceEntity()
}
}
withContext(Dispatchers.Default) {
if (currentPageNo == 1) {
//第一页数据删除本地数据库,同时重置时间戳
faceApi.clearFaceData()
faceTimestamp = 0L
}
faceApi.updateFaceData(pageNo, faceEntityList)
if (list.size >= pageSize) {
// 取当前页最大时间戳,与已累积的比较取最大值
val pageMaxTimestamp = maxFaceTimestamp(list)
if (pageMaxTimestamp > faceTimestamp) {
faceTimestamp = pageMaxTimestamp
}
currentPageNo++
getUserFaceCache(
pageNo = currentPageNo, callback = callback
)
return@withContext
}
if (list.isNotEmpty()) {
// 最后一页:取当前页最大时间戳,与已累积的比较取最大值
val pageMaxTimestamp = maxFaceTimestamp(list)
if (pageMaxTimestamp > faceTimestamp) {
faceTimestamp = pageMaxTimestamp
}
}
SpTool.lastFaceTimestamp = faceTimestamp
callback(true, "查询完成")
}
}
}
private var faceTimestamp = 0L
/**
* 激活人脸识别引擎
*/
fun activeEngine() {
Timber.d("activeEngine")
faceApi.activeEngine(
App.getContext(),
GlobalData.appId,
GlobalData.sdkKey,
GlobalData.activeKey,
object : FaceApi.ActiveCallback {
override fun onSuccess(activeCode: Int) {
Timber.d("activeEngine activeCode = $activeCode")
// ThreadUtils.launch {
viewModelScope.launch(Dispatchers.Main) {
when (activeCode) {
ErrorInfo.MOK -> {
ToastUtils.showToast("激活引擎成功")
}
ErrorInfo.MERR_ASF_ALREADY_ACTIVATED -> {
// ToastUtils.showToast("引擎已激活,无需再次激活")
}
else -> {
ToastUtils.showToast("激活引擎失败($activeCode)")
}
}
}
}
override fun onFail(e: Exception?) {
viewModelScope.launch(Dispatchers.Main) {
ToastUtils.showToast("激活引擎异常,${e?.message}")
}
}
})
}
fun getDeviceConfig(onSuccess: (DeviceConfigV2?) -> Unit, onFailure: (String?) -> Unit) {
// Timber.tag(TAG).d("getDeviceConfig")
viewModelScope.launch {
val response = repository.getDeviceConfig()
if (response.isSuccess()) {
onSuccess(response.data)
} else {
onFailure(response.msg)
}
}
}
/**
* 增量获取人脸数据(支持分页递归,取所有页中的最大时间戳)
*/
fun getFaceIncrementList(
pageNo: Int = 1,
pageSize: Int = PAGE_SIZE,
timestamp: Long,
refreshFaceList: () -> Unit
) {
var currentPageNo = pageNo
viewModelScope.launch {
val response = repository.getFaceIncrementList(
pageNum = currentPageNo.toLong(),
pageSize = pageSize.toLong(),
timestamp = timestamp
)
if (response.isSuccess().not()) {
return@launch
}
withContext(Dispatchers.IO) {
val list = response.data ?: emptyList()
updateFaceData(list)
// 取当前页最大时间戳,与已累积的比较取最大值
val pageMaxTimestamp = maxFaceTimestamp(list)
if (pageMaxTimestamp > faceTimestamp) {
faceTimestamp = pageMaxTimestamp
}
if (list.size >= pageSize) {
// 还有下一页,继续递归拉取
currentPageNo++
getFaceIncrementList(
pageNo = currentPageNo,
pageSize = pageSize,
timestamp = timestamp,
refreshFaceList = refreshFaceList
)
return@withContext
}
// 所有页拉取完毕,保存最终时间戳(只升不降)
val storedTimestamp = SpTool.lastFaceTimestamp
if (faceTimestamp > storedTimestamp) {
SpTool.lastFaceTimestamp = faceTimestamp
}
try {
refreshFaceList()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
private fun updateFaceData(list: List<UserFaceModelV2>) {
try {
list.forEach { model ->
if (model.faceDeleted == true) {
//删除数据
// 仅删除 userFaceId 对应的单条人脸记录,避免误删该用户名下所有特征
val userFaceId = model.userFaceId
if (!userFaceId.isNullOrEmpty()) {
faceApi.deleteByUserFaceId(userFaceId)
}
} else {
// 优先按 userFaceId 精确判重,避免特征字段不一致导致重复入库
val userFaceId = model.userFaceId
if (!userFaceId.isNullOrEmpty() && faceApi.queryByUserFaceId(userFaceId) != null) {
// 本地已存在该 userFaceId,跳过插入
return@forEach
}
// userFaceId 为空时,回退到特征比较去重(修正字段来源,保证与入库一致)
val featureStr = model.resolveFeatureStr()
val existList = faceApi.queryAllByUserName(model.userId)
val alreadyExists = existList.any { entity ->
featureStr != null && Base64.encode(entity.featureData) == featureStr
}
if (!alreadyExists) {
faceApi.insert(model.toFaceEntity())
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
/**
* 添加人脸数据
*/
fun addUserFace(faceData: String, imageFile: File, block: (Boolean, UserFaceModelV2?) -> Unit) {
viewModelScope.launch {
try {
val imageResp = repository.uploadImage(imageFile)
val imageUrl = imageResp.data ?: ""
if (imageUrl.isBlank()) {
block(false, null)
return@launch
}
val faceResp = repository.addUserFace(faceData, imageUrl)
if (faceResp.isSuccess().not()) {
block(false, null)
return@launch
}
block(true, faceResp.data)
} catch (e: Exception) {
// 网络或解析异常时同样回调失败,避免 block 永不触发导致上层无反馈
e.printStackTrace()
block(false, null)
}
}
}
/**
* 取餐盘上报(P-09a):记录用户开始就餐时刻,作为后续就餐记录(P-09)的强前置。
* 失败仅回调通知,不阻断吐盘等业务主流程。
*
* @param userId 用户 id(餐盘号 = userId
* @param callback 上报结果回调:Boolean 是否成功,String? 成功为 recordNo、失败为错误信息
*/
fun platePickup(
userId: Long,
callback: (Boolean, String?) -> Unit = { _, _ -> }
) {
viewModelScope.launch {
try {
val response = repository.platePickup(userId)
if (response.isSuccess()) {
callback(true, response.data)
} else {
callback(false, response.msg)
}
} catch (e: Exception) {
e.printStackTrace()
callback(false, e.message)
}
}
}
private fun base64ToByteArray(base64: String?): ByteArray? {
base64 ?: return null
return try {
Base64.decode(base64)
} catch (e: Exception) {
null
}
}
/**
* 从人脸数据列表中取出最大的 faceUpdateTimestamp(跳过 null 值)
*/
private fun maxFaceTimestamp(list: List<UserFaceModelV2>): Long {
return list.mapNotNull { it.faceUpdateTimestamp }.maxOrNull() ?: 0L
}
}
@@ -1,250 +1,62 @@
package com.sw.platecabinet.viewmodel //package com.sw.platecabinet.viewmodel
//
import com.sw.platecabinet.GlobalData.globalEquipmentCode //import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.model.response.EquipmentUserInfo //import kotlinx.coroutines.Dispatchers
import com.sw.platecabinet.model.response.SearchResult //import kotlinx.coroutines.withContext
import com.sw.platecabinet.model.response.UserFaceModel //import java.io.File
import kotlinx.coroutines.Dispatchers //
import kotlinx.coroutines.flow.MutableStateFlow ///**
import kotlinx.coroutines.flow.StateFlow // * 管理员操作viewmodel
import kotlinx.coroutines.withContext // */
import java.io.File //class SettingViewModel : BaseViewModel() {
//
/** // companion object {
* 管理员操作viewmodel // const val PAGE_SIZE = 100
*/ // }
class SettingViewModel : BaseViewModel() { //
// 设备绑定列表 // /**
// private val _equipmentList = MutableStateFlow<List<EquipmentUserInfo>>(emptyList()) // * 获取人脸数据
// val equipmentList: StateFlow<List<EquipmentUserInfo>> = _equipmentList // */
// fun getFaceIncrementList(
private val _searchMemberList = MutableStateFlow<List<SearchResult.Member>>(emptyList()) // pageNo: Int = 1,
val searchMemberList: StateFlow<List<SearchResult.Member>> = _searchMemberList // pageSize: Int = PAGE_SIZE,
// timestamp: Long,
private val _searchUserInfo = MutableStateFlow<EquipmentUserInfo?>(null) // block: (List<UserFaceModel>) -> Unit
val searchUserInfo: StateFlow<EquipmentUserInfo?> = _searchUserInfo // ) {
// launch {
var currentPage = 1 // val response = repository.getFaceIncrementList(
var isLoading = false // pageNum = pageNo.toLong(),
var canLoadMore = true // pageSize = pageSize.toLong(),
// timestamp = timestamp
/** // )
* 绑定/解绑状态 <绑定/解绑, 错误信息,>
*/
// private val _bindStateChange = MutableStateFlow<Pair<Boolean?, ErrorInfo>>(null to ErrorInfo())
// val bindStateChange: StateFlow<Pair<Boolean?, ErrorInfo>> = _bindStateChange
/**
* 获取设备绑定用户信息列表
*/
// fun getEquipmentList(equipmentCode: String = globalEquipmentCode) {
// launchWithLoading {
// val param =
// EquipmentParam(appVersion = GlobalData.appVersion, equipmentCode = equipmentCode)
// val response = repository.getEquipmentList(param)
// if (parseResponse(response)) { // if (parseResponse(response)) {
// _equipmentList.value = response.data ?: emptyList() // withContext(Dispatchers.Default) {
// } // val list: List<UserFaceModel> = response.data ?: emptyList()
// }
//// val list = mutableListOf<EquipmentUserInfo>()
//// for (i in 1 .. 30){
//// list.add(EquipmentUserInfo(id = i, equipmentBoxCode = i.toString()))
//// }
//// _equipmentList.value = list
// }
// fun getEquipmentList(block: (List<EquipmentUserInfo>) -> Unit) {
// launchWithLoading {
// val response = repository.getEquipmentList()
// if (parseResponse(response)) {
// val list = response.data ?: emptyList()
// block(list) // block(list)
// } // }
// } // }
// } // }
// }
//
// /** // /**
// * 搜索会员信息列表 // * 添加人脸数据
// */ // */
// fun getSearchMemberList(param:String, pageNum: Int = 1, pageSize: Int = 50) { // fun addUserFace(faceData: String, imageFile: File, block: (Boolean, UserFaceModel?) -> Unit) {
// if (TextUtils.isEmpty(param)) return
// currentPage = pageNum
// var name =""
// var phone =""
// val num = param.toIntOrNull()
// if (num != null) {
// phone = param
// } else {
// name = param
// }
// launch { // launch {
// val searchParam = SearchParam( // val imageResp = repository.uploadImage(imageFile)
//// appVersion = GlobalData.appVersion, // val imageUrl = imageResp.data ?: ""
// name = name, // if (imageUrl.isBlank()) {
// phone = phone, // block(false, null)
// pageNum = pageNum, // return@launch
// 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++ // val faceResp = repository.addUserFace(faceData, imageUrl)
// val status = parseResponse(faceResp)
// if (status.not()) {
// block(false, null)
// return@launch
// }
// block(true, faceResp.data)
// } // }
// } // }
// } //
// } //}
// fun bindPlate(
// equipmentBoxCode: String,
// equipmentCode: String = globalEquipmentCode,
// memberId: Int,
// plateNumber: String
// ) {
// _bindStateChange.value = null to ErrorInfo()
// launchWithLoading {
// val bindParam = BindParam(
// appVersion = GlobalData.appVersion,
// equipmentBoxCode = equipmentBoxCode,
// equipmentCode = equipmentCode,
// memberId = memberId,
// plateNumber = plateNumber
// )
// val response = repository.bindEquipment(bindParam)
// if (parseResponse(response)) {
// var userInfo = response.data
// if (userInfo == null) {
// userInfo = EquipmentUserInfo()
// userInfo.equipmentBoxCode = equipmentBoxCode
// userInfo.plateNumber = plateNumber
// userInfo.equipmentCode = equipmentCode
// }
// _bindStateChange.value = true to ErrorInfo(equipmentUserInfo = userInfo)
// } else {
// _bindStateChange.value = true to ErrorInfo(response.code, response.msg.toString())
// }
// }
// }
// fun unbindPlate(
// equipmentBoxCode: String,
// equipmentCode: String = globalEquipmentCode
// ) {
// launchWithLoading {
// val bindParam = BindParam(
// appVersion = GlobalData.appVersion,
// equipmentBoxCode = equipmentBoxCode,
// equipmentCode = equipmentCode
// )
// val response = repository.bindEquipment(bindParam)
// if (parseResponse(response)) {
// _bindStateChange.value = false to ErrorInfo()
// } else {
// _bindStateChange.value = false to ErrorInfo(response.code, response.msg.toString())
// }
// }
// }
// fun unbindPlate(id: Long?, block:(Pair<Boolean?, ErrorInfo>)-> 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<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(
plateNumber: String,
equipmentCode: String = globalEquipmentCode
) {
// _searchUserInfo.value = null
// launchWithLoading {
//// val bindParam = BindParam(plateNumber = plateNumber)
// val response = repository.findByPlateNumber(plateNumber)
// if (response.code == "00000") {
// _searchUserInfo.value = response.data
// } else {
// _searchUserInfo.value = EquipmentUserInfo(
// plateNumber = plateNumber,
// equipmentCode = equipmentCode,
// updateTime = DateTimeUtils.getDateTimeString()
// )
// }
// }
}
val PAGE_SIZE = 100
/**
* 获取人脸数据
*/
fun getFaceIncrementList(
pageNo: Int = 1,
pageSize: Int = PAGE_SIZE,
timestamp: Long,
block: (List<UserFaceModel>) -> Unit
) {
launch {
val response = repository.getFaceIncrementList(
pageNum = pageNo.toLong(),
pageSize = pageSize.toLong(),
timestamp = timestamp
)
if (parseResponse(response)) {
withContext(Dispatchers.Default) {
val list: List<UserFaceModel> = response.data ?: emptyList()
block(list)
}
}
}
}
/**
* 添加人脸数据
*/
fun addUserFace(faceData: String, imageFile: File, block: (Boolean, UserFaceModel?) -> Unit) {
launch {
val imageResp = repository.uploadImage(imageFile)
val imageUrl = imageResp.data?:""
if (imageUrl.isBlank()) {
block(false, null)
return@launch
}
val faceResp = repository.addUserFace(faceData, imageUrl)
val status = parseResponse(faceResp)
if (status.not()) {
block(false, null)
return@launch
}
block(true, faceResp.data)
}
}
}
@@ -1,193 +1,201 @@
package com.sw.platecabinet.viewmodel //package com.sw.platecabinet.viewmodel
//
import androidx.lifecycle.viewModelScope //import androidx.lifecycle.viewModelScope
import com.arcsoft.face.ErrorInfo //import com.arcsoft.face.ErrorInfo
import com.sw.inbound.utils.SPUtil //import com.sw.inbound.utils.SPUtil
import com.sw.plate.App //import com.sw.plate.App
import com.sw.plate.utils.Base64 //import com.sw.plate.utils.Base64
import com.sw.plate.utils.ToastUtils //import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.FaceApi //import com.sw.plate.utils.arcface.FaceApi
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity //import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import com.sw.platecabinet.GlobalData //import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.GlobalData.globalEquipmentCode //import com.sw.platecabinet.GlobalKey
import com.sw.platecabinet.GlobalKey //import com.sw.platecabinet.model.DeviceConfig
import com.sw.platecabinet.model.DeviceConfig //import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.model.request.LoginParam //import com.sw.platecabinet.utils.SpTool
import com.sw.platecabinet.model.response.EquipmentUserInfo //import kotlinx.coroutines.Dispatchers
import com.sw.platecabinet.model.response.UserFaceModel //import kotlinx.coroutines.launch
import com.sw.platecabinet.utils.SpTool //import kotlinx.coroutines.withContext
import kotlinx.coroutines.Dispatchers //import timber.log.Timber
import kotlinx.coroutines.flow.MutableStateFlow //import java.io.File
import kotlinx.coroutines.flow.StateFlow //
import kotlinx.coroutines.launch ///**
import kotlinx.coroutines.withContext // * 用户viewmodel
import timber.log.Timber // */
//class UserViewModel : BaseViewModel() {
/** //
* 用户viewmodel // companion object {
*/ // const val PAGE_SIZE = 100
class UserViewModel : BaseViewModel() { // }
//
companion object { // private val faceApi: FaceApi = FaceApi()
const val PAGE_SIZE = 100 //
} //
// fun getUserFace(
// 通过人脸获取到的用户信息 // pageNo: Int,
// private val _currentUserInfo = MutableStateFlow<EquipmentUserInfo?>(null) // pageSize: Int,
// val currentUserInfo: StateFlow<EquipmentUserInfo?> = _currentUserInfo // callback: (List<UserFaceModel>) -> Unit
// ) {
private val faceApi: FaceApi = FaceApi()
/**
* 获取token
*/
// fun generateToken(deviceId: String = "SWSN:88:12:AC:4E:D9:CC") {
// launchWithLoading { // launchWithLoading {
// val response = repository.generateToken(deviceId) // val response = repository.getUserFaceCache(pageNum = pageNo, pageSize = pageSize)
// if (parseResponse(response)) { // val status = parseResponse(response)
// // 缓存token // if (status) callback(response.data ?: emptyList())
// SPUtil.getInstance().put(GlobalKey.KEY_TOKEN, response.data) // else emptyList<UserFaceModel>()
// // 首次运行获取人脸数据
// if (SPUtil.getInstance().get(GlobalKey.KEY_FIRST_RUN, false) != true) {
// getUserFaceCache()
// } // }
// } // }
// } //
// } // /**
// * 获取人脸数据
/** // */
* 获取人脸数据 // fun getUserFaceCache(
*/ // pageNo: Int = 1,
fun getUserFaceCache( // pageSize: Int = PAGE_SIZE,
pageNo: Int = 1, // callback: (Boolean, String?) -> Unit = { statue, msg -> }
pageSize: Int = PAGE_SIZE, // ) {
callback: (Boolean, String?) -> Unit = { statue, msg -> } // var currentPageNo = pageNo
) {
var currentPageNo = pageNo
launchWithLoading {
val response = repository.getUserFaceCache(pageNum = pageNo, pageSize = pageSize)
val status = parseResponse(response)
if (status.not()) {
callback(false, response.msg)
return@launchWithLoading
}
// 获取成功一次后缓存状态
SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true)
val list: List<UserFaceModel> = response.data ?: emptyList()
if (pageNo == 1 && list.isEmpty()) {
callback(true, "暂无数据")
return@launchWithLoading
}
val faceEntityList = list.map {
FaceEntity(it.userId, null, Base64.decode(it.faceFeatureStr))
}
withContext(Dispatchers.Default) {
faceApi.updateFaceData(pageNo, faceEntityList)
if (list.size >= pageSize) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0
currentPageNo++
getUserFaceCache(
pageNo = currentPageNo,
callback = callback
)
return@withContext
}
if (list.isNotEmpty()) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0
}
SpTool.lastFaceTimestamp = faceTimestamp
callback(true, "查询完成")
}
}
}
private var faceTimestamp = 0L
/**
* 激活人脸识别引擎
*/
fun activeEngine() {
Timber.d("activeEngine")
faceApi.activeEngine(
App.getContext(),
GlobalData.appId,
GlobalData.sdkKey,
GlobalData.activeKey,
object : FaceApi.ActiveCallback {
override fun onSuccess(activeCode: Int) {
Timber.d("activeEngine activeCode = $activeCode")
// ThreadUtils.launch {
viewModelScope.launch(Dispatchers.Main) {
when (activeCode) {
ErrorInfo.MOK -> {
ToastUtils.showToast("激活引擎成功")
}
ErrorInfo.MERR_ASF_ALREADY_ACTIVATED -> {
// ToastUtils.showToast("引擎已激活,无需再次激活")
}
else -> {
ToastUtils.showToast("激活引擎失败($activeCode)")
}
}
}
}
override fun onFail(e: Exception?) {
viewModelScope.launch(Dispatchers.Main) {
ToastUtils.showToast("激活引擎异常,${e?.message}")
}
}
})
}
/**
* 校验码登录
*/
fun loginWithPwd(loginParam: LoginParam) {
// launchWithLoading { // launchWithLoading {
// val response = repository.equipmentBoxLogin(loginParam) // val response = repository.getUserFaceCache(pageNum = pageNo, pageSize = pageSize)
// val status = parseResponse(response)
// if (status.not()) {
// callback(false, response.msg)
// return@launchWithLoading
// }
// // 获取成功一次后缓存状态
// SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true)
// val list: List<UserFaceModel> = response.data ?: emptyList()
// if (pageNo == 1 && list.isEmpty()) {
// callback(false, "暂无数据")
// return@launchWithLoading
// }
// val faceEntityList = list.map {
// FaceEntity(it.userId, null, Base64.decode(it.faceFeatureStr)).apply {
// userType = if (it.member == true) "1" else "2"
// }
// }
// withContext(Dispatchers.Default) {
// if (currentPageNo == 1) {
// //第一页数据删除本地数据库
// faceApi.clearFaceData()
// }
// faceApi.updateFaceData(pageNo, faceEntityList)
// if (list.size >= pageSize) {
// faceTimestamp = list.last().faceUpdateTimestamp ?: 0
// currentPageNo++
// getUserFaceCache(
// pageNo = currentPageNo,
// callback = callback
// )
// return@withContext
// }
// if (list.isNotEmpty()) {
// faceTimestamp = list.last().faceUpdateTimestamp ?: 0
// }
// SpTool.lastFaceTimestamp = faceTimestamp
// callback(true, "查询完成")
// }
// }
// }
//
// private var faceTimestamp = 0L
//
//
// /**
// * 激活人脸识别引擎
// */
// fun activeEngine() {
// Timber.d("activeEngine")
//
// faceApi.activeEngine(
// App.getContext(),
// GlobalData.appId,
// GlobalData.sdkKey,
// GlobalData.activeKey,
// object : FaceApi.ActiveCallback {
// override fun onSuccess(activeCode: Int) {
// Timber.d("activeEngine activeCode = $activeCode")
//// ThreadUtils.launch {
// viewModelScope.launch(Dispatchers.Main) {
// when (activeCode) {
// ErrorInfo.MOK -> {
// ToastUtils.showToast("激活引擎成功")
// }
//
// ErrorInfo.MERR_ASF_ALREADY_ACTIVATED -> {
//// ToastUtils.showToast("引擎已激活,无需再次激活")
// }
//
// else -> {
// ToastUtils.showToast("激活引擎失败($activeCode)")
// }
// }
// }
// }
//
// override fun onFail(e: Exception?) {
// viewModelScope.launch(Dispatchers.Main) {
// ToastUtils.showToast("激活引擎异常,${e?.message}")
// }
// }
// })
// }
//
//
// fun getDeviceConfig(block: (DeviceConfig?) -> Unit) {
//// Timber.tag(TAG).d("getDeviceConfig")
// launch {
// val response = repository.getDeviceConfig()
// if (parseResponse(response)) { // if (parseResponse(response)) {
// _currentUserInfo.value = response.data // block(response.data)
// } else {
// block(null)
// } // }
// } // }
} // }
//
/** //
* 通过用户id获取用户信息 // /**
*/ // * 获取人脸数据
// fun getUserInfoById(memberId: String?, action: (EquipmentUserInfo?) -> Unit = {}) { // */
// _currentUserInfo.value = null // fun getFaceIncrementList(
// launchWithLoading { // pageNo: Int = 1,
// val loginParam = LoginParam(faceId = memberId) // pageSize: Int = PAGE_SIZE,
// val response = repository.equipmentBoxLogin(loginParam) // timestamp: Long,
// block: (List<UserFaceModel>) -> Unit
// ) {
// launch {
// val response = repository.getFaceIncrementList(
// pageNum = pageNo.toLong(),
// pageSize = pageSize.toLong(),
// timestamp = timestamp
// )
// if (parseResponse(response)) { // if (parseResponse(response)) {
// _currentUserInfo.value = response.data // withContext(Dispatchers.Default) {
// action(response.data) // val list: List<UserFaceModel> = response.data ?: emptyList()
// block(list)
// } // }
// } // }
// } // }
// }
/** //
* 重置用户信息 // /**
*/ // * 添加人脸数据
fun resetUserInfo() { // */
Timber.d("resetUserInfo") // fun addUserFace(faceData: String, imageFile: File, block: (Boolean, UserFaceModel?) -> Unit) {
// _currentUserInfo.value = null // launch {
} // val imageResp = repository.uploadImage(imageFile)
// val imageUrl = imageResp.data ?: ""
fun getDeviceConfig(block: (DeviceConfig?) -> Unit) { // if (imageUrl.isBlank()) {
// Timber.tag(TAG).d("getDeviceConfig") // block(false, null)
launch { // return@launch
val response = repository.getDeviceConfig() // }
if (parseResponse(response)) { // val faceResp = repository.addUserFace(faceData, imageUrl)
block(response.data) // val status = parseResponse(faceResp)
} else { // if (status.not()) {
block(null) // block(false, null)
} // return@launch
} // }
} // block(true, faceResp.data)
// }
} // }
//
//}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#30000000">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/white" />
<corners android:radius="8dp" />
<stroke
android:width="1dp"
android:color="#5C77F7" />
</shape>
</item>
</ripple>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#30000000">
<item>
<shape android:shape="rectangle">
<solid android:color="#5C77F7" />
<corners android:radius="8dp" />
</shape>
</item>
</ripple>
+8 -22
View File
@@ -2,34 +2,20 @@
<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="1920px"
android:height="1024dp"> android:height="1080px">
<bitmap android:src="@drawable/bg" /> <bitmap
android:scaleType="centerCrop"
android:src="@drawable/bg_collect_page3" />
</item> </item>
<item <item
android:width="337dp" android:width="506dp"
android:height="322dp" android:height="483dp"
android:gravity="top|center_horizontal" android:gravity="top|center_horizontal"
android:top="112dp"> android:top="150dp">
<bitmap <bitmap
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:src="@drawable/img_init" /> 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>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/white"/>
<corners android:radius="12dp"/>
</shape>
+3 -3
View File
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<corners android:radius="12dp" /> <corners android:radius="16dp" />
<stroke <stroke
android:width="2dp" android:width="3dp"
android:color="#FFCC99" /> android:color="#FFC08A6F" />
</shape> </shape>
+15 -1
View File
@@ -65,7 +65,7 @@
android:layout_marginHorizontal="50dp" android:layout_marginHorizontal="50dp"
android:paddingHorizontal="30dp" android:paddingHorizontal="30dp"
android:background="@drawable/btn_outline" android:background="@drawable/btn_outline"
android:text="清除并重新拉取数据" android:text="清除并拉取全量数据"
android:textColor="#FFCC99" /> android:textColor="#FFCC99" />
<TextView <TextView
@@ -93,4 +93,18 @@
android:background="@drawable/btn_outline" android:background="@drawable/btn_outline"
android:text="模拟放置餐盘" android:text="模拟放置餐盘"
android:textColor="#FFCC99" /> android:textColor="#FFCC99" />
<TextView
android:id="@+id/btnTestFeature"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="30dp"
android:textSize="24sp"
android:gravity="center"
android:layout_marginHorizontal="50dp"
android:paddingHorizontal="30dp"
android:background="@drawable/btn_outline"
android:text="人脸特征比较"
android:textColor="#FFCC99"
android:visibility="gone"/>
</LinearLayout> </LinearLayout>
+10 -16
View File
@@ -4,7 +4,7 @@
android:id="@+id/main" android:id="@+id/main"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@drawable/bg" android:background="@drawable/bg_collect_page3"
android:orientation="vertical" android:orientation="vertical"
tools:context=".activity.InitActivity"> tools:context=".activity.InitActivity">
@@ -12,10 +12,10 @@
android:id="@+id/includeHeader" android:id="@+id/includeHeader"
layout="@layout/item_title_time" /> layout="@layout/item_title_time" />
<View <Space
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="2"/> android:layout_weight="1"/>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -26,34 +26,28 @@
<ImageView <ImageView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="142dp" android:layout_height="wrap_content"
android:src="@drawable/ic_init_text" android:src="@drawable/ic_init_text"
android:adjustViewBounds="true"/> android:adjustViewBounds="true"/>
<ImageView <ImageView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="345dp" android:layout_height="wrap_content"
android:layout_marginTop="90dp" android:layout_marginTop="60dp"
android:src="@drawable/ic_init_img" android:src="@drawable/ic_init_img"
android:adjustViewBounds="true" /> android:adjustViewBounds="true" />
<!-- <ImageView-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginTop="20dp"-->
<!-- android:src="@drawable/ic_init_press" />-->
<FrameLayout <FrameLayout
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="-35dp"> android:layout_marginTop="-35dp">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="360dp"
android:layout_height="60dp" android:layout_height="110dp"
android:text="点击取盘" android:text="点击取盘"
android:textColor="#FFCC99" android:textColor="#FFCC99"
android:textSize="24sp" android:textSize="36sp"
android:layout_marginHorizontal="60dp" android:layout_marginHorizontal="60dp"
android:layout_marginVertical="50dp" android:layout_marginVertical="50dp"
android:paddingHorizontal="45dp" android:paddingHorizontal="45dp"
@@ -63,7 +57,7 @@
</FrameLayout> </FrameLayout>
</LinearLayout> </LinearLayout>
<View <Space
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1"/> android:layout_weight="1"/>
+71 -172
View File
@@ -1,226 +1,125 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main" android:id="@+id/main"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@drawable/bg"
android:fitsSystemWindows="false" android:fitsSystemWindows="false"
android:orientation="vertical" tools:context=".activity.LoginByFaceActivity"
tools:context=".activity.LoginByFaceActivity"> tools:ignore="HardcodedText,ContentDescription,PxUsage">
<FrameLayout
android:layout_width="776px"
android:layout_height="582px"
android:layout_gravity="center">
<TextureView
android:id="@+id/dual_camera_texture_preview_rgb"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.sw.plate.utils.arcface.FaceRectView
android:id="@+id/dual_camera_face_rect_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:src="@drawable/bg_face_area" />
<include <include
android:id="@+id/includeHeader" android:id="@+id/includeHeader"
layout="@layout/item_title_time" /> layout="@layout/item_title_time" />
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="165dp"
android:layout_gravity="center_horizontal|top"
android:layout_marginTop="95dp"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:orientation="vertical"> android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:visibility="visible">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="0dp"
android:paddingTop="1dp">
<TextureView
android:id="@+id/dual_camera_texture_preview_rgb"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible" />
<com.sw.plate.utils.arcface.FaceRectView
android:id="@+id/dual_camera_face_rect_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible" />
</FrameLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginTop="0dp"
android:scaleType="centerCrop"
android:src="@drawable/bg_face"
android:visibility="visible" />
<TextView <TextView
android:id="@+id/tv_tip" android:id="@+id/tv_tip"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:layout_marginTop="129dp" android:maxLines="1"
android:text="请正视屏幕" android:text="请正视屏幕"
android:textColor="#FFCC99" android:textColor="#FFF0C8B4"
android:textSize="36sp" android:textSize="48sp"
android:textStyle="bold" /> android:textStyle="bold" />
<TextView <TextView
android:id="@+id/tvSimilarValue" android:id="@+id/tvSimilarValue"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="20dp"
android:textSize="26sp"
android:layout_marginTop="80dp"
tools:text="0.6183次"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:textColor="@color/white"/> android:layout_marginTop="0dp"
android:maxLines="1"
<!-- <LinearLayout--> android:textColor="@color/white"
<!-- android:id="@+id/ll_to_pwd"--> android:textSize="12sp"
<!-- android:layout_width="wrap_content"--> tools:text="0.6183次" />
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginStart="208dp"-->
<!-- android:layout_marginTop="730dp"-->
<!-- android:background="@drawable/btn_outline"-->
<!-- android:gravity="center"-->
<!-- android:paddingHorizontal="45dp"-->
<!-- android:paddingVertical="18dp"-->
<!-- android:visibility="gone">-->
<!-- <TextView-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="密码登录"-->
<!-- android:textColor="#FFCC99"-->
<!-- android:textSize="24sp" />-->
<!-- </LinearLayout>-->
<TextView <TextView
android:id="@+id/tv_to_init" android:id="@+id/tv_to_init"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="185dp" android:layout_marginTop="0dp"
android:gravity="center" android:gravity="center"
android:padding="12dp" android:includeFontPadding="false"
android:textColor="#FFCC99" android:maxLines="1"
android:textSize="36sp" android:textColor="#FFF0C8B4"
android:textSize="48sp"
tools:text="30" /> tools:text="30" />
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/llCollectButton" android:id="@+id/llCollectButton"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="265dp"
android:orientation="horizontal" android:layout_gravity="center_horizontal|bottom"
android:gravity="center" android:gravity="center"
android:layout_gravity="bottom" android:orientation="horizontal"
android:layout_marginBottom="100dp" android:visibility="visible">
android:visibility="gone">
<TextView <TextView
android:id="@+id/btnRetry" android:id="@+id/btnRetry"
android:layout_width="wrap_content" android:layout_width="360dp"
android:layout_height="60dp" android:layout_height="110dp"
android:text="再次重试" android:layout_marginHorizontal="20dp"
android:textColor="#FFCC99"
android:textSize="24sp"
android:layout_marginHorizontal="12dp"
android:layout_marginVertical="10dp"
android:paddingHorizontal="45dp"
android:gravity="center"
android:background="@drawable/btn_outline" android:background="@drawable/btn_outline"
android:gravity="center"
android:text="再次重试"
android:textColor="#FFC08A6F"
android:textSize="36sp"
tools:ignore="HardcodedText" /> tools:ignore="HardcodedText" />
<TextView <TextView
android:id="@+id/btnCollect" android:id="@+id/btnCollect"
android:layout_width="wrap_content" android:layout_width="360dp"
android:layout_height="60dp" android:layout_height="110dp"
android:text="新用户采集" android:layout_marginHorizontal="20dp"
android:textColor="#FFCC99"
android:textSize="24sp"
android:layout_marginHorizontal="12dp"
android:layout_marginVertical="10dp"
android:paddingHorizontal="45dp"
android:gravity="center"
android:background="@drawable/btn_outline" android:background="@drawable/btn_outline"
android:gravity="center"
android:text="新用户采集"
android:textColor="#FFC08A6F"
android:textSize="36sp"
tools:ignore="HardcodedText" /> tools:ignore="HardcodedText" />
</LinearLayout> </LinearLayout>
<!-- <LinearLayout-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:gravity="center_horizontal"-->
<!-- android:layout_gravity="bottom|center_horizontal"-->
<!-- android:layout_marginBottom="130dp"-->
<!-- android:orientation="vertical">-->
<!-- <LinearLayout-->
<!-- android:id="@+id/layoutState"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="50dp"-->
<!-- android:gravity="center_vertical"-->
<!-- android:orientation="horizontal"-->
<!-- android:visibility="invisible">-->
<!-- <ProgressBar-->
<!-- android:id="@+id/pbCollectLoading"-->
<!-- android:layout_width="32dp"-->
<!-- android:layout_height="32dp"-->
<!-- android:indeterminateTint="@color/white" />-->
<!-- <TextView-->
<!-- android:id="@+id/tvCollectState"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginStart="10dp"-->
<!-- android:text="采集中......"-->
<!-- android:textColor="@color/white"-->
<!-- android:textSize="24sp" />-->
<!-- </LinearLayout>-->
<!-- <TextView-->
<!-- android:id="@+id/btnCollectFace"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="60dp"-->
<!-- android:background="@drawable/btn_outline"-->
<!-- android:gravity="center"-->
<!-- android:layout_marginTop="10dp"-->
<!-- android:paddingHorizontal="45dp"-->
<!-- android:text="开始采集"-->
<!-- android:textColor="#FFCC99"-->
<!-- android:textSize="24sp"-->
<!-- tools:ignore="HardcodedText" />-->
<!-- </LinearLayout>-->
<!-- <androidx.recyclerview.widget.RecyclerView-->
<!-- android:id="@+id/rvLogInfo"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="match_parent"-->
<!-- android:layout_gravity="bottom"-->
<!-- android:overScrollMode="never"-->
<!-- android:visibility="gone"-->
<!-- android:background="@color/black"-->
<!-- tools:listitem="@layout/list_item_log_info" />-->
<!-- <ImageView-->
<!-- android:id="@+id/btnLogState"-->
<!-- android:layout_width="60dp"-->
<!-- android:layout_height="60dp"-->
<!-- android:layout_gravity="bottom|end"-->
<!-- android:background="@drawable/shape_white_circle"-->
<!-- android:padding="10dp"-->
<!-- android:layout_marginEnd="10dp"-->
<!-- android:layout_marginBottom="10dp"-->
<!-- android:src="@drawable/ic_log_show"-->
<!-- android:textSize="16sp" />-->
<ImageView <ImageView
android:id="@+id/ivCollectPhoto" android:id="@+id/ivCollectPhoto"
android:layout_width="200dp" android:layout_width="200dp"
android:layout_height="200dp" android:layout_height="200dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="64dp"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:layout_gravity="top|start" android:visibility="gone"
android:visibility="gone"/> tools:src="@drawable/logo_launcher" />
</FrameLayout>
</FrameLayout>
</LinearLayout>
</LinearLayout>
+1 -1
View File
@@ -5,7 +5,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:background="@drawable/bg" android:background="@drawable/bg_collect_page3"
tools:context=".activity.MainActivity"> tools:context=".activity.MainActivity">
<include <include
@@ -0,0 +1,378 @@
<?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="match_parent"
android:background="@drawable/bg"
android:orientation="vertical"
tools:context=".activity.PlcSettingActivity">
<include
android:id="@+id/includeHeader"
layout="@layout/item_title_time" />
<RelativeLayout
android:id="@+id/layout_tips"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@mipmap/img_error_tips"
tools:ignore="ContentDescription" />
<TextView android:id="@+id/tv_tips"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="144dp"
android:text="暂无餐盘 请联系管理人员"
android:textColor="#fff0c8b4"
android:textSize="32sp" />
</RelativeLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:clipToPadding="false"
android:paddingHorizontal="50dp"
android:paddingVertical="20dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- ===== 连接控制 ===== -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="连接控制"
android:textColor="#FFCC99"
android:textSize="20sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<TextView
android:id="@+id/btnOpenSerialPort"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_weight="1"
android:background="@drawable/btn_outline"
android:gravity="center"
android:text="打开串口"
android:textColor="#FFCC99"
android:textSize="22sp" />
<View
android:layout_width="20dp"
android:layout_height="1dp" />
<TextView
android:id="@+id/btnOpenAutoMode"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_weight="1"
android:background="@drawable/btn_outline"
android:gravity="center"
android:text="打开自动模式"
android:textColor="#FFCC99"
android:textSize="22sp" />
</LinearLayout>
<!-- ===== 出盘操作 ===== -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="出盘操作"
android:textColor="#FFCC99"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/btnOutPlate"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="10dp"
android:background="@drawable/btn_outline"
android:gravity="center"
android:text="出盘"
android:textColor="#FFCC99"
android:textSize="22sp" />
<!-- ===== 平台控制 ===== -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="平台控制"
android:textColor="#FFCC99"
android:textSize="20sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<TextView
android:id="@+id/btnPlatformDown"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_weight="1"
android:background="@drawable/btn_outline"
android:gravity="center"
android:text="平台下降"
android:textColor="#FFCC99"
android:textSize="22sp" />
<View
android:layout_width="20dp"
android:layout_height="1dp" />
<TextView
android:id="@+id/btnPlatformUp"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_weight="1"
android:background="@drawable/btn_outline"
android:gravity="center"
android:text="平台上升"
android:textColor="#FFCC99"
android:textSize="22sp" />
</LinearLayout>
<TextView
android:id="@+id/btnBackToBusiness"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="12dp"
android:background="@drawable/btn_outline"
android:gravity="center"
android:text="返回营业(补盘完成)"
android:textColor="#FFCC99"
android:textSize="22sp" />
<TextView
android:id="@+id/btnPlatformReset"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="12dp"
android:background="@drawable/btn_outline"
android:gravity="center"
android:text="餐盘平台复位"
android:textColor="#FFCC99"
android:textSize="22sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<!-- ===== 故障处理 ===== -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="故障处理"
android:textColor="#FFCC99"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/btnAlarmReset"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="10dp"
android:background="@drawable/btn_outline"
android:gravity="center"
android:text="故障复位"
android:textColor="#FFCC99"
android:textSize="22sp" />
<!-- ===== 参数设置 ===== -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="参数设置"
android:textColor="#FFCC99"
android:textSize="20sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<EditText
android:id="@+id/etParamType"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_weight="1"
android:background="@drawable/btn_outline"
android:gravity="center"
android:hint="参数类型(如 18FD"
android:inputType="text"
android:paddingHorizontal="12dp"
android:textColor="#FFCC99"
android:textColorHint="#99885566"
android:textSize="18sp" />
<View
android:layout_width="20dp"
android:layout_height="1dp" />
<EditText
android:id="@+id/etParamValue"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_weight="1"
android:background="@drawable/btn_outline"
android:gravity="center"
android:hint="参数值(整数)"
android:inputType="number"
android:paddingHorizontal="12dp"
android:textColor="#FFCC99"
android:textColorHint="#99885566"
android:textSize="18sp" />
</LinearLayout>
<TextView
android:id="@+id/btnSetParams"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="12dp"
android:background="@drawable/btn_outline"
android:gravity="center"
android:text="设置参数"
android:textColor="#FFCC99"
android:textSize="22sp" />
<!-- ===== 状态显示 ===== -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="PLC 状态"
android:textColor="#FFCC99"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/btnRefreshStatus"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="10dp"
android:background="@drawable/btn_outline"
android:gravity="center"
android:text="刷新状态"
android:textColor="#FFCC99"
android:textSize="22sp" />
<TextView
android:id="@+id/tvStatusInfo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:background="@drawable/btn_outline"
android:paddingHorizontal="16dp"
android:paddingVertical="14dp"
android:text="暂无状态数据"
android:textColor="#FFCC99"
android:textSize="16sp"
tools:text="空盘:false | 平台下位:false | 平台上位:true | 取盘红外:false | 报警码:0" />
<View
android:layout_width="match_parent"
android:layout_height="30dp" />
</LinearLayout>
<!-- ===== 出盘操作 ===== -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="环境"
android:textColor="#FFCC99"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/btnSwitchEnv"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="10dp"
android:background="@drawable/btn_outline"
android:gravity="center"
android:text="切换环境"
android:textColor="#FFCC99"
android:textSize="22sp" />
<!-- ===== 人脸库 ===== -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="人脸库"
android:textColor="#FFCC99"
android:textSize="20sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvFaceCount"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_weight="1"
android:background="@drawable/btn_outline"
android:gravity="center"
android:text="人脸总数:--"
android:textColor="#FFCC99"
android:textSize="20sp" />
<View
android:layout_width="20dp"
android:layout_height="1dp" />
<TextView
android:id="@+id/btnRefreshFaceCount"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_weight="1"
android:background="@drawable/btn_outline"
android:gravity="center"
android:text="刷新"
android:textColor="#FFCC99"
android:textSize="20sp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="30dp" />
</LinearLayout>
</ScrollView>
</LinearLayout>
@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="600dp"
android:layout_height="wrap_content"
android:background="@drawable/bg_white_radius12"
android:orientation="vertical"
android:paddingHorizontal="40dp"
android:paddingVertical="36dp">
<!-- 标题 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="环境切换"
android:textColor="#ff141428"
android:textSize="36sp"
android:textStyle="bold" />
<!-- 分割线 -->
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_marginTop="24dp"
android:background="#FFDCDCF0" />
<!-- 环境选项 -->
<RadioGroup
android:id="@+id/rgEnv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:orientation="vertical">
<RadioButton
android:id="@+id/rbTest"
android:layout_width="match_parent"
android:layout_height="72dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:text="测试环境"
android:textColor="#ff141428"
android:textSize="28sp" />
<RadioButton
android:id="@+id/rbUat"
android:layout_width="match_parent"
android:layout_height="72dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:text="UAT 环境"
android:textColor="#ff141428"
android:textSize="28sp" />
<RadioButton
android:id="@+id/rbProd"
android:layout_width="match_parent"
android:layout_height="72dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:text="生产环境"
android:textColor="#ff141428"
android:textSize="28sp" />
</RadioGroup>
<!-- 分割线 -->
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_marginTop="24dp"
android:background="#FFDCDCF0" />
<!-- 底部按钮 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="88dp"
android:layout_marginTop="24dp"
android:gravity="center"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnCancel"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginEnd="20dp"
android:layout_weight="1"
android:background="@drawable/bg_cancel"
android:text="取消"
android:textColor="#5C77F7"
android:textSize="28sp" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnConfirm"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="20dp"
android:layout_weight="1"
android:background="@drawable/bg_confirm"
android:text="确认"
android:textColor="#FFFFFF"
android:textSize="28sp" />
</LinearLayout>
</LinearLayout>
+7 -6
View File
@@ -4,15 +4,16 @@
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<LinearLayout <LinearLayout
android:layout_width="240dp" android:layout_width="wrap_content"
android:layout_height="80dp" android:layout_height="80dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:maxWidth="200dp"
android:background="@drawable/dialog_waiting"> android:background="@drawable/dialog_waiting">
<ProgressBar <ProgressBar
android:layout_width="wrap_content" android:layout_width="50dp"
android:layout_height="wrap_content" android:layout_height="50dp"
android:layout_marginStart="20dp" android:layout_marginStart="10dp"
android:indeterminateTint="@color/teal_200" android:indeterminateTint="@color/teal_200"
android:indeterminateTintMode="src_atop" /> android:indeterminateTintMode="src_atop" />
@@ -20,10 +21,10 @@
android:id="@+id/tvTip" android:id="@+id/tvTip"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="30dp" android:layout_marginHorizontal="10dp"
android:textColor="@color/white" android:textColor="@color/white"
android:text="请稍等" android:text="请稍等"
android:textSize="22sp" /> android:textSize="26sp" />
</LinearLayout> </LinearLayout>
</FrameLayout> </FrameLayout>
@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main" android:id="@+id/main"
android:layout_width="match_parent" android:layout_width="match_parent"
+29 -10
View File
@@ -3,30 +3,49 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="40dp" android:layout_height="100dp"
android:paddingHorizontal="24dp"> android:paddingHorizontal="65dp"
tools:ignore="HardcodedText,ContentDescription,UseCompoundDrawables">
<TextView <LinearLayout
android:id="@+id/tvLeftDate" android:id="@+id/tvLeftDate"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="64dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:textColor="#FFCC99" android:paddingHorizontal="12dp"
android:textSize="18sp" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent">
tools:text="2025年6月18日 星期三" />
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/logo_launcher" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:layout_marginStart="25dp"
android:textColor="#FFF0C8B4"
android:textSize="30sp"
android:text="欢迎光临数味餐厅" />
</LinearLayout>
<TextView <TextView
android:id="@+id/tvRightTime" android:id="@+id/tvRightTime"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical" android:gravity="center_vertical"
android:textColor="#FFCC99" android:minHeight="64dp"
android:textSize="18sp" android:paddingHorizontal="12dp"
android:textColor="#FFF0C8B4"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tvLeftDate" app:layout_constraintTop_toTopOf="@id/tvLeftDate"
app:layout_constraintBottom_toBottomOf="@id/tvLeftDate" app:layout_constraintBottom_toBottomOf="@id/tvLeftDate"
tools:text="14:02:39" /> tools:text="2026年03月27日 星期五 12:08" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

+1 -1
View File
@@ -1,5 +1,5 @@
<resources> <resources>
<string name="app_name">餐盘柜</string> <string name="app_name">发盘机</string>
<!-- TODO: Remove or change this placeholder text --> <!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string> <string name="hello_blank_fragment">Hello blank fragment</string>
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -13,7 +13,7 @@ android {
consumerProguardFiles("consumer-rules.pro") consumerProguardFiles("consumer-rules.pro")
ndk { ndk {
abiFilters.addAll(listOf("armeabi-v7a"/*, "arm64-v8a"*/)) abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a"))
} }
} }
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -87,10 +87,10 @@ public class ConfigUtil {
/** /**
* 默认相机分辨率 * 默认相机分辨率
*/ */
private static final String DEFAULT_PREVIEW_SIZE = "1280x720"; // private static final String DEFAULT_PREVIEW_SIZE = "1280x720";
private static final String DEFAULT_PREVIEW_SIZE = "1024x768";
// private static final String DEFAULT_PREVIEW_SIZE = "400x640"; // private static final String DEFAULT_PREVIEW_SIZE = "400x640";
/** /**
* 获取String类型的preference * 获取String类型的preference
* *
@@ -32,7 +32,7 @@ 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 = getFaceDao();
//if (index == 1) { //if (index == 1) {
// faceDao.deleteAll(); // faceDao.deleteAll();
// faceDao.resetId(); // faceDao.resetId();
@@ -40,6 +40,12 @@ public class FaceApi {
faceDao.insert(list); faceDao.insert(list);
} }
public void clearFaceData() {
FaceDao faceDao = getFaceDao();
faceDao.deleteAll();
faceDao.resetId();
}
/** /**
* 激活arcsoft 人脸 * 激活arcsoft 人脸
* *
@@ -82,29 +88,51 @@ public class FaceApi {
} }
public void deleteByUserName(String userName) { public void deleteByUserName(String userName) {
FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao(); getFaceDao().deleteFaceById(userName);
faceDao.deleteFaceById(userName); }
/**
* 按 userFaceId 删除单条人脸记录
*/
public void deleteByUserFaceId(String userFaceId) {
getFaceDao().deleteFaceByUserFaceId(userFaceId);
}
/**
* 按 userFaceId 查询单条人脸记录
*/
public FaceEntity queryByUserFaceId(String userFaceId) {
if (userFaceId == null || userFaceId.isEmpty()) {
return null;
}
return getFaceDao().queryByUserFaceId(userFaceId);
} }
public Long insert(FaceEntity entity) { public Long insert(FaceEntity entity) {
if (entity == null) { if (entity == null) {
return 0L; return 0L;
} }
FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao(); return getFaceDao().insert(entity);
return faceDao.insert(entity);
} }
public FaceEntity queryByUserName(String userName) { public FaceEntity queryByUserName(String userName) {
if (TextUtils.isEmpty(userName)) { if (TextUtils.isEmpty(userName)) {
return null; return null;
} }
FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao(); return getFaceDao().queryByUserName(userName);
return faceDao.queryByUserName(userName);
} }
public int queryFaceCount() { public int queryFaceCount() {
FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao(); return getFaceDao().getFaceCount();
return faceDao.getFaceCount(); }
public List<FaceEntity> queryAllByUserName(String userName) {
return getFaceDao().queryAllByUserName(userName);
}
public FaceDao getFaceDao() {
return FaceDatabase.getInstance(App.getContext()).faceDao();
} }
} }
@@ -39,7 +39,6 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -104,6 +103,10 @@ public class FaceHelper implements FaceListener {
* 活体检测引擎 * 活体检测引擎
*/ */
private FaceEngine flEngine; private FaceEngine flEngine;
/**
* 口罩检测引擎(5.0 独立引擎)
*/
private FaceEngine maskEngine;
private Camera.Size previewSize; private Camera.Size previewSize;
@@ -148,11 +151,6 @@ public class FaceHelper implements FaceListener {
*/ */
private boolean onlyDetectLiveness; private boolean onlyDetectLiveness;
/**
* 是否需要更新faceData
*/
private boolean needUpdateFaceData;
/** /**
* 识别的配置项 * 识别的配置项
*/ */
@@ -182,9 +180,9 @@ public class FaceHelper implements FaceListener {
} }
private FaceHelper(Builder builder) { private FaceHelper(Builder builder) {
needUpdateFaceData = builder.needUpdateFaceData;
onlyDetectLiveness = builder.onlyDetectLiveness; onlyDetectLiveness = builder.onlyDetectLiveness;
ftEngine = builder.ftEngine; ftEngine = builder.ftEngine;
maskEngine = builder.maskEngine;
trackedFaceCount = builder.trackedFaceCount; trackedFaceCount = builder.trackedFaceCount;
previewSize = builder.previewSize; previewSize = builder.previewSize;
frEngine = builder.frEngine; frEngine = builder.frEngine;
@@ -343,12 +341,20 @@ public class FaceHelper implements FaceListener {
refreshTrackId(faceInfoList); refreshTrackId(faceInfoList);
if (faceInfoList.isEmpty()) { if (faceInfoList.isEmpty()) {
return facePreviewInfoList; return facePreviewInfoList;
} else {
FaceInfo currentFaceInfo = faceInfoList.get(0);
// L.e("currentFaceInfo width=" + currentFaceInfo.getRect().width() +
// "---height" + currentFaceInfo.getRect().height());
if (currentFaceInfo.getRect().width() < 200) {//距离远(人脸小),不识别
clearLeftFace(facePreviewInfoList);
return facePreviewInfoList;
} }
if (!onlyDetectLiveness) { }
code = ftEngine.process(rgbNv21, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, faceInfoList, if (!onlyDetectLiveness && maskEngine != null) {
code = maskEngine.process(rgbNv21, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, faceInfoList,
FaceEngine.ASF_MASK_DETECT); FaceEngine.ASF_MASK_DETECT);
if (code == ErrorInfo.MOK) { if (code == ErrorInfo.MOK) {
code = ftEngine.getMask(maskInfoList); code = maskEngine.getMask(maskInfoList);
if (code != ErrorInfo.MOK) { if (code != ErrorInfo.MOK) {
onFail(new Exception("process getMask failed,code is " + code)); onFail(new Exception("process getMask failed,code is " + code));
return facePreviewInfoList; return facePreviewInfoList;
@@ -872,18 +878,7 @@ public class FaceHelper implements FaceListener {
int fdCode = flEngine.detectFaces(nv21Data, width, height, format, faceInfoList); int fdCode = flEngine.detectFaces(nv21Data, width, height, format, faceInfoList);
boolean isFaceExists = isFaceExists(faceInfoList, faceInfo); boolean isFaceExists = isFaceExists(faceInfoList, faceInfo);
if (fdCode == ErrorInfo.MOK && isFaceExists) { if (fdCode == ErrorInfo.MOK && isFaceExists) {
if (needUpdateFaceData) {
/*
* 若IR人脸框有偏移,则需要对IR的人脸数据进行updateFaceData处理,再将处理后的FaceInfo信息传输给活体检测接口
*/
flCode = flEngine.updateFaceData(nv21Data, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21,
new ArrayList<>(Collections.singletonList(faceInfo)));
if (flCode == ErrorInfo.MOK) {
flCode = flEngine.processIr(nv21Data, width, height, format, Arrays.asList(faceInfo), FaceEngine.ASF_IR_LIVENESS); flCode = flEngine.processIr(nv21Data, width, height, format, Arrays.asList(faceInfo), FaceEngine.ASF_IR_LIVENESS);
}
} else {
flCode = flEngine.processIr(nv21Data, width, height, format, Arrays.asList(faceInfo), FaceEngine.ASF_IR_LIVENESS);
}
} else { } else {
onFail(new Exception("ir detectFaces failed fdCode:" + fdCode + ",isFaceExists:" + isFaceExists)); onFail(new Exception("ir detectFaces failed fdCode:" + fdCode + ",isFaceExists:" + isFaceExists));
} }
@@ -1090,9 +1085,9 @@ public class FaceHelper implements FaceListener {
private FaceEngine ftEngine; private FaceEngine ftEngine;
private FaceEngine frEngine; private FaceEngine frEngine;
private FaceEngine flEngine; private FaceEngine flEngine;
private FaceEngine maskEngine;
private Camera.Size previewSize; private Camera.Size previewSize;
private boolean onlyDetectLiveness; private boolean onlyDetectLiveness;
private boolean needUpdateFaceData;
private RecognizeConfiguration recognizeConfiguration; private RecognizeConfiguration recognizeConfiguration;
private RecognizeCallback recognizeCallback; private RecognizeCallback recognizeCallback;
private IDualCameraFaceInfoTransformer dualCameraFaceInfoTransformer; private IDualCameraFaceInfoTransformer dualCameraFaceInfoTransformer;
@@ -1133,6 +1128,11 @@ public class FaceHelper implements FaceListener {
return this; return this;
} }
public Builder maskEngine(FaceEngine val) {
maskEngine = val;
return this;
}
public Builder previewSize(Camera.Size val) { public Builder previewSize(Camera.Size val) {
previewSize = val; previewSize = val;
return this; return this;
@@ -1158,11 +1158,6 @@ public class FaceHelper implements FaceListener {
return this; return this;
} }
public Builder needUpdateFaceData(boolean val) {
needUpdateFaceData = val;
return this;
}
public FaceHelper build() { public FaceHelper build() {
return new FaceHelper(this); return new FaceHelper(this);
} }
@@ -9,7 +9,7 @@ import androidx.room.RoomDatabase;
import com.sw.plate.utils.arcface.facedb.dao.FaceDao; import com.sw.plate.utils.arcface.facedb.dao.FaceDao;
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity; import com.sw.plate.utils.arcface.facedb.entity.FaceEntity;
@Database(entities = {FaceEntity.class}, version = 1, exportSchema = false) @Database(entities = {FaceEntity.class}, version = 2, exportSchema = false)
public abstract class FaceDatabase extends RoomDatabase { public abstract class FaceDatabase extends RoomDatabase {
public abstract FaceDao faceDao(); public abstract FaceDao faceDao();
@@ -22,7 +22,10 @@ public abstract class FaceDatabase extends RoomDatabase {
faceDatabase = Room.databaseBuilder(context, FaceDatabase.class, faceDatabase = Room.databaseBuilder(context, FaceDatabase.class,
context.getDatabasePath("faceDB.db").getPath() context.getDatabasePath("faceDB.db").getPath()
// context.getExternalFilesDir("database") + File.separator + "faceDB.db" // context.getExternalFilesDir("database") + File.separator + "faceDB.db"
).build(); )
// 5.0 特征模型不兼容,升级时直接删除旧表重建
.fallbackToDestructiveMigration()
.build();
} }
} }
} }
@@ -55,6 +55,18 @@ public interface FaceDao {
@Query("DELETE from face WHERE user_name = :userName") @Query("DELETE from face WHERE user_name = :userName")
int deleteFaceById(String userName); int deleteFaceById(String userName);
/**
* 按 userFaceId 删除单条人脸记录
*/
@Query("DELETE from face WHERE user_face_id = :userFaceId")
int deleteFaceByUserFaceId(String userFaceId);
/**
* 按 userFaceId 查询单条人脸记录
*/
@Query("SELECT * FROM face WHERE user_face_id = :userFaceId LIMIT 1")
FaceEntity queryByUserFaceId(String userFaceId);
/** /**
* 删除所有已注册的人脸 * 删除所有已注册的人脸
* *
@@ -92,16 +104,25 @@ public interface FaceDao {
@Query("SELECT * FROM face WHERE user_name = :userName limit 1") @Query("SELECT * FROM face WHERE user_name = :userName limit 1")
FaceEntity queryByUserName(String userName); FaceEntity queryByUserName(String userName);
/**
* 查询指定用户的所有人脸记录(同一用户可能存有多条特征数据)
*
* @param userName 用户ID
* @return 该用户所有人脸记录列表
*/
@Query("SELECT * FROM face WHERE user_name = :userName")
List<FaceEntity> queryAllByUserName(String userName);
/** /**
* @return 删除临时用户人脸 * @return 删除临时用户人脸
*/ */
@Query("DELETE from face WHERE user_type = '2'") @Query("DELETE from face WHERE user_type = :userType")
int deleteTempUserFaceData(); int deleteUserFaceData(int userType);
/** /**
* 查询用户人脸数 * 查询用户人脸数
* @param userType 1-会员,2-临时用户 * @param userType 1-会员,2-临时用户
*/ */
@Query("SELECT COUNT(1) FROM face WHERE user_type = :userType") @Query("SELECT COUNT(1) FROM face WHERE user_type = :userType")
int getFaceCount(String userType); int getFaceCountByUserType(int userType);
} }
@@ -52,12 +52,39 @@ public class FaceEntity implements Parcelable {
/** /**
* 会员编号 * 会员编号
*/ */
@ColumnInfo(name = "cardNo") @ColumnInfo(name = "card_no")
private String cardNo; private String cardNo;
@Ignore @Ignore
private int trackId;//人脸追踪ID private int trackId;//人脸追踪ID
@ColumnInfo(name = "user_id")
private String userId;
@ColumnInfo(name = "user_face_id")
private String userFaceId;
@ColumnInfo(name = "member")
private boolean member;
@ColumnInfo(name = "face_update_timestamp")
private long faceUpdateTimestamp;
public FaceEntity() {
registerTime = System.currentTimeMillis();
}
@Ignore
public FaceEntity(String userName, byte[] featureData, String userType, String cardNo, String userId, String userFaceId, boolean member, long faceUpdateTimestamp) {
this.userName = userName;
this.featureData = featureData;
this.userType = userType;
this.cardNo = cardNo;
this.userId = userId;
this.userFaceId = userFaceId;
this.member = member;
this.faceUpdateTimestamp = faceUpdateTimestamp;
registerTime = System.currentTimeMillis();
}
@Ignore
public FaceEntity(String userName, String imagePath, byte[] featureData) { public FaceEntity(String userName, String imagePath, byte[] featureData) {
this.userName = userName; this.userName = userName;
this.imagePath = imagePath; this.imagePath = imagePath;
@@ -65,6 +92,7 @@ public class FaceEntity implements Parcelable {
registerTime = System.currentTimeMillis(); registerTime = System.currentTimeMillis();
} }
@Ignore
public FaceEntity(FaceEntity faceEntity) { public FaceEntity(FaceEntity faceEntity) {
this.faceId = faceEntity.faceId; this.faceId = faceEntity.faceId;
this.userName = faceEntity.userName; this.userName = faceEntity.userName;
@@ -73,9 +101,14 @@ public class FaceEntity implements Parcelable {
this.registerTime = faceEntity.registerTime; this.registerTime = faceEntity.registerTime;
this.userType = faceEntity.getUserType(); this.userType = faceEntity.getUserType();
this.cardNo = faceEntity.getCardNo(); this.cardNo = faceEntity.getCardNo();
this.userId = faceEntity.getUserId();
this.userFaceId = faceEntity.getUserFaceId();
this.member = faceEntity.isMember();
this.faceUpdateTimestamp = faceEntity.getFaceUpdateTimestamp();
} }
@Ignore
protected FaceEntity(Parcel in) { protected FaceEntity(Parcel in) {
faceId = in.readLong(); faceId = in.readLong();
registerTime = in.readLong(); registerTime = in.readLong();
@@ -84,6 +117,10 @@ public class FaceEntity implements Parcelable {
featureData = in.createByteArray(); featureData = in.createByteArray();
userType = in.readString(); userType = in.readString();
cardNo = in.readString(); cardNo = in.readString();
userId = in.readString();
userFaceId = in.readString();
member = in.readByte() != 0;
faceUpdateTimestamp = in.readLong();
} }
public static final Creator<FaceEntity> CREATOR = new Creator<FaceEntity>() { public static final Creator<FaceEntity> CREATOR = new Creator<FaceEntity>() {
@@ -162,7 +199,37 @@ public class FaceEntity implements Parcelable {
this.cardNo = cardNo; this.cardNo = cardNo;
} }
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getUserFaceId() {
return userFaceId;
}
public void setUserFaceId(String userFaceId) {
this.userFaceId = userFaceId;
}
public boolean isMember() {
return member;
}
public void setMember(boolean member) {
this.member = member;
}
public long getFaceUpdateTimestamp() {
return faceUpdateTimestamp;
}
public void setFaceUpdateTimestamp(long faceUpdateTimestamp) {
this.faceUpdateTimestamp = faceUpdateTimestamp;
}
@Override @Override
public int describeContents() { public int describeContents() {
@@ -178,6 +245,10 @@ public class FaceEntity implements Parcelable {
dest.writeByteArray(featureData); dest.writeByteArray(featureData);
dest.writeString(userType); dest.writeString(userType);
dest.writeString(cardNo); dest.writeString(cardNo);
dest.writeString(userId);
dest.writeString(userFaceId);
dest.writeByte((byte) (member ? 1 : 0));
dest.writeLong(faceUpdateTimestamp);
} }
@Override @Override
@@ -195,12 +266,16 @@ public class FaceEntity implements Parcelable {
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); TextUtils.equals(this.cardNo, that.cardNo) &&
TextUtils.equals(this.userId, that.userId) &&
TextUtils.equals(this.userFaceId, that.userFaceId) &&
this.member == that.member &&
this.faceUpdateTimestamp == that.faceUpdateTimestamp;
} }
@Override @Override
public int hashCode() { public int hashCode() {
int result = Objects.hash(faceId, registerTime, userName, imagePath, userType, cardNo); int result = Objects.hash(faceId, registerTime, userName, imagePath, userType, cardNo, userId, userFaceId, member, faceUpdateTimestamp);
result = 31 * result + Arrays.hashCode(featureData); result = 31 * result + Arrays.hashCode(featureData);
return result; return result;
} }
@@ -12,7 +12,6 @@ import androidx.lifecycle.ViewModel;
import com.arcsoft.face.AgeInfo; import com.arcsoft.face.AgeInfo;
import com.arcsoft.face.ErrorInfo; import com.arcsoft.face.ErrorInfo;
import com.arcsoft.face.FaceAttributeParam;
import com.arcsoft.face.FaceEngine; import com.arcsoft.face.FaceEngine;
import com.arcsoft.face.FaceInfo; import com.arcsoft.face.FaceInfo;
import com.arcsoft.face.GenderInfo; import com.arcsoft.face.GenderInfo;
@@ -132,13 +131,14 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
private MutableLiveData<Integer> ftInitCode = new MutableLiveData<>(); private MutableLiveData<Integer> ftInitCode = new MutableLiveData<>();
private MutableLiveData<Integer> frInitCode = new MutableLiveData<>(); private MutableLiveData<Integer> frInitCode = new MutableLiveData<>();
private MutableLiveData<Integer> flInitCode = new MutableLiveData<>(); private MutableLiveData<Integer> flInitCode = new MutableLiveData<>();
private MutableLiveData<Integer> maskInitCode = new MutableLiveData<>();
/** /**
* 人脸操作辅助类,推帧即可,内部会进行特征提取、识别 * 人脸操作辅助类,推帧即可,内部会进行特征提取、识别
*/ */
private FaceHelper faceHelper; private FaceHelper faceHelper;
/** /**
* VIDEO模式人脸检测引擎,用于预览帧人脸追踪及图像质量检测 * VIDEO模式人脸检测引擎,用于预览帧人脸追踪
*/ */
private FaceEngine ftEngine; private FaceEngine ftEngine;
/** /**
@@ -149,6 +149,10 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
* IMAGE模式活体检测引擎,用于预览帧人脸活体检测 * IMAGE模式活体检测引擎,用于预览帧人脸活体检测
*/ */
private FaceEngine flEngine; private FaceEngine flEngine;
/**
* IMAGE模式口罩检测引擎(5.0 独立引擎)
*/
private FaceEngine maskEngine;
private PreviewConfig previewConfig; private PreviewConfig previewConfig;
@@ -160,10 +164,6 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
private MutableLiveData<CompareResult> recognizeUserId = new MutableLiveData<>(); private MutableLiveData<CompareResult> recognizeUserId = new MutableLiveData<>();
/**
* 检测ir活体前,是否需要更新faceData
*/
private boolean needUpdateFaceData;
/** /**
* 当前活体检测的检测类型 * 当前活体检测的检测类型
*/ */
@@ -297,22 +297,22 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
.similarThreshold(ConfigUtil.getRecognizeThreshold(context)) .similarThreshold(ConfigUtil.getRecognizeThreshold(context))
.imageQualityNoMaskRecognizeThreshold(ConfigUtil.getImageQualityNoMaskRecognizeThreshold(context)) .imageQualityNoMaskRecognizeThreshold(ConfigUtil.getImageQualityNoMaskRecognizeThreshold(context))
.imageQualityMaskRecognizeThreshold(ConfigUtil.getImageQualityMaskRecognizeThreshold(context)) .imageQualityMaskRecognizeThreshold(ConfigUtil.getImageQualityMaskRecognizeThreshold(context))
.livenessParam(new LivenessParam(ConfigUtil.getRgbLivenessThreshold(context), ConfigUtil.getIrLivenessThreshold(context), .livenessParam(new LivenessParam(ConfigUtil.getRgbLivenessThreshold(context), ConfigUtil.getIrLivenessThreshold(context)))
ConfigUtil.getLivenessFqThreshold(context)))
.build(); .build();
int cameraOffsetX = ConfigUtil.getDualCameraHorizontalOffset(context);
int cameraOffsetY = ConfigUtil.getDualCameraVerticalOffset(context);
needUpdateFaceData = (livenessType == LivenessType.IR && (cameraOffsetX != 0 || cameraOffsetY != 0));
// 人脸追踪引擎(VIDEO模式,仅检测)
ftEngine = new FaceEngine(); ftEngine = new FaceEngine();
int ftEngineMask = FaceEngine.ASF_FACE_DETECT | FaceEngine.ASF_MASK_DETECT; int ftEngineMask = FaceEngine.ASF_FACE_DETECT;
ftInitCode.postValue(ftEngine.init(context, DetectMode.ASF_DETECT_MODE_VIDEO, ConfigUtil.getFtOrient(context), ftInitCode.postValue(ftEngine.init(context, DetectMode.ASF_DETECT_MODE_VIDEO, ConfigUtil.getFtOrient(context),
ConfigUtil.getRecognizeMaxDetectFaceNum(context), ftEngineMask)); ConfigUtil.getRecognizeMaxDetectFaceNum(context), ftEngineMask));
FaceAttributeParam attributeParam = new FaceAttributeParam(
ConfigUtil.getRecognizeEyeOpenThreshold(context), ConfigUtil.getRecognizeMouthCloseThreshold(context),
ConfigUtil.getRecognizeWearGlassesThreshold(context));
ftEngine.setFaceAttributeParam(attributeParam);
// 口罩检测引擎(5.0 独立引擎)
maskEngine = new FaceEngine();
maskInitCode.postValue(maskEngine.init(context, DetectMode.ASF_DETECT_MODE_IMAGE,
DetectFaceOrientPriority.ASF_OP_ALL_OUT,
ConfigUtil.getRecognizeMaxDetectFaceNum(context), FaceEngine.ASF_MASK_DETECT));
// 特征提取引擎
frEngine = new FaceEngine(); frEngine = new FaceEngine();
int frEngineMask = FaceEngine.ASF_FACE_RECOGNITION; int frEngineMask = FaceEngine.ASF_FACE_RECOGNITION;
if (enableFaceQualityDetect) { if (enableFaceQualityDetect) {
@@ -322,16 +322,13 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
10, frEngineMask)); 10, frEngineMask));
FaceServer.getInstance().initFaceList(context, frEngine, faceCount -> loadFaceList = true, true); FaceServer.getInstance().initFaceList(context, frEngine, faceCount -> loadFaceList = true, true);
//启用活体检测时,才初始化活体引擎 // 启用活体检测时,才初始化活体引擎
if (enableLive) { if (enableLive) {
flEngine = new FaceEngine(); flEngine = new FaceEngine();
int flEngineMask = (livenessType == LivenessType.RGB ? FaceEngine.ASF_LIVENESS : (FaceEngine.ASF_IR_LIVENESS | FaceEngine.ASF_FACE_DETECT)); int flEngineMask = (livenessType == LivenessType.RGB ? FaceEngine.ASF_LIVENESS : (FaceEngine.ASF_IR_LIVENESS | FaceEngine.ASF_FACE_DETECT));
if (needUpdateFaceData) {
flEngineMask |= FaceEngine.ASF_UPDATE_FACEDATA;
}
flInitCode.postValue(flEngine.init(context, DetectMode.ASF_DETECT_MODE_IMAGE, flInitCode.postValue(flEngine.init(context, DetectMode.ASF_DETECT_MODE_IMAGE,
DetectFaceOrientPriority.ASF_OP_ALL_OUT, 10, flEngineMask)); DetectFaceOrientPriority.ASF_OP_ALL_OUT, 10, flEngineMask));
LivenessParam livenessParam = new LivenessParam(ConfigUtil.getRgbLivenessThreshold(context), ConfigUtil.getIrLivenessThreshold(context), ConfigUtil.getLivenessFqThreshold(context)); LivenessParam livenessParam = new LivenessParam(ConfigUtil.getRgbLivenessThreshold(context), ConfigUtil.getIrLivenessThreshold(context));
flEngine.setLivenessParam(livenessParam); flEngine.setLivenessParam(livenessParam);
} }
@@ -356,19 +353,25 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
if (ftEngine != null) { if (ftEngine != null) {
synchronized (ftEngine) { synchronized (ftEngine) {
int ftUnInitCode = ftEngine.unInit(); int ftUnInitCode = ftEngine.unInit();
Log.i(TAG, "unInitEngine: " + ftUnInitCode); Log.i(TAG, "unInitEngine ft: " + ftUnInitCode);
}
}
if (maskEngine != null) {
synchronized (maskEngine) {
int maskUnInitCode = maskEngine.unInit();
Log.i(TAG, "unInitEngine mask: " + maskUnInitCode);
} }
} }
if (frEngine != null) { if (frEngine != null) {
synchronized (frEngine) { synchronized (frEngine) {
int frUnInitCode = frEngine.unInit(); int frUnInitCode = frEngine.unInit();
Log.i(TAG, "unInitEngine: " + frUnInitCode); Log.i(TAG, "unInitEngine fr: " + frUnInitCode);
} }
} }
if (flEngine != null) { if (flEngine != null) {
synchronized (flEngine) { synchronized (flEngine) {
int flUnInitCode = flEngine.unInit(); int flUnInitCode = flEngine.unInit();
Log.i(TAG, "unInitEngine: " + flUnInitCode); Log.i(TAG, "unInitEngine fl: " + flUnInitCode);
} }
} }
} }
@@ -397,6 +400,20 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
} }
} }
/**
* 重置人脸识别状态:清空上次识别结果及 FaceHelper 内部的 recognizeInfoMap
* 使下一帧进入时能重新触发识别流程。
* 适用场景:短时间内再次识别、点击重试按钮等需要重新开始识别的时机。
*/
public void resetFaceState() {
// 清空粘性 LiveData,防止旧结果被重新投递给 observer
recognizeUserId.postValue(null);
// 清空 FaceHelper 内部所有人脸状态,让 trackId 对应的状态回到 TO_RETRY
if (faceHelper != null) {
faceHelper.clearFacePreviewInfoList();
}
}
/** /**
* 释放操作 * 释放操作
*/ */
@@ -455,7 +472,7 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
.ftEngine(ftEngine) .ftEngine(ftEngine)
.frEngine(frEngine) .frEngine(frEngine)
.flEngine(flEngine) .flEngine(flEngine)
.needUpdateFaceData(needUpdateFaceData) .maskEngine(maskEngine)
.frQueueSize(maxDetectFaceNum) .frQueueSize(maxDetectFaceNum)
.flQueueSize(maxDetectFaceNum) .flQueueSize(maxDetectFaceNum)
.previewSize(previewSize) .previewSize(previewSize)
+1 -1
View File
@@ -25,6 +25,6 @@ dependencyResolutionManagement {
} }
} }
rootProject.name = "SmartPlateCabinet" rootProject.name = "SmartTakePlate"
include(":app") include(":app")
include(":lib_face") include(":lib_face")