Author SHA1 Message Date
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
lvmeng 513f4dbaea 增加查询用户数量 2026-03-04 09:03:32 +08:00
lvmengandClaude Sonnet 4.6 037816c303 fix(activity): 修复协程冗余 start() 调用并收敛方法可见性
- 移除 lifecycleScope.launch 后的 .start(),launch 已自动启动协程
- createBlackNV21Frame 和 emptyFrame 加 private 修饰符,收敛对外可见性

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 09:19:49 +08:00
mazengfei 4ea95092f5 优化 2026-02-28 16:54:57 +08:00
lvmeng cc88dccaa6 识别成功未提示及采集时间长问题优化 2026-02-28 10:03:45 +08:00
mazengfei e979322697 优化 2026-02-05 18:16:38 +08:00
lvmeng 1e6ea1517c 人脸识别+采集分开处理 2026-02-05 15:05:45 +08:00
lvmeng 2ee4d2d320 人脸识别+采集分开处理 2026-02-05 13:32:45 +08:00
lvmeng 67f5d07f25 优化 2026-02-04 18:01:15 +08:00
lvmeng 79e089363d 优化重复注册 2026-02-04 10:20:34 +08:00
lvmeng 16c9e5f61a 优化重复注册 2026-02-04 09:23:19 +08:00
lvmeng a8bbf68b9b 优化 2026-02-03 18:51:13 +08:00
mazengfei 0024ea2775 优化 2026-02-03 16:19:42 +08:00
lvmeng 9fea23303c 打开app不再清除本地数据,不再发送数据到结算终端,改为新增人脸采集接口,全量增量接口优化 2026-02-03 14:59:22 +08:00
mazengfei c5325f6ad6 优化流程,测试不再验证活体 2026-02-02 16:36:24 +08:00
mazengfei e1e754be9f 调整提示音 2026-01-29 14:31:23 +08:00
mazengfei 1d0e6d4bca 隐藏toast 2026-01-26 17:32:50 +08:00
lvmeng 8f788529d2 增加日志 2026-01-26 15:16:35 +08:00
mazengfei 5bc244dde5 增加无餐盘提醒 2026-01-22 15:49:36 +08:00
lvmeng d4f9ace67b 新增识别提示 2026-01-22 15:10:12 +08:00
lvmeng 855a84684c 人脸识别小于阈值时增加重试次数,降低识别错误情况 2026-01-22 10:30:57 +08:00
mazengfei f2bedb7777 优化 2026-01-21 17:58:55 +08:00
mazengfei aad4f299aa 优化 2026-01-21 15:53:53 +08:00
lvmeng 176c555860 采集优化 2026-01-21 14:27:16 +08:00
mazengfei de4e5d57e2 优化 2026-01-21 13:00:58 +08:00
mazengfei 8326ff0a03 优化 2026-01-20 19:04:17 +08:00
mazengfei b7408dc6c4 优化 2026-01-20 14:00:28 +08:00
mazengfei ec12017fd4 优化 2026-01-20 12:23:38 +08:00
lvmeng 662e2c63e1 优化--- 2026-01-20 08:32:14 +08:00
mazengfei f473d1f174 优化 2026-01-19 17:55:16 +08:00
lvmeng 3b51e6f3f2 日志按钮优化 2026-01-19 14:35:00 +08:00
lvmeng 40d41d1afb 增加实时日志查询 2026-01-19 14:32:48 +08:00
mazengfei b23045194a 增加日志 2026-01-19 13:08:28 +08:00
mazengfei 1b4762916a 餐盘柜优化-不绑定用户可多次取盘 2026-01-16 16:45:13 +08:00
mazengfei cf2d2db42f 餐盘柜优化-绑定临时用户 2026-01-16 16:14:38 +08:00
mazengfei 8337b3fabb 餐盘柜优化 2026-01-16 11:53:44 +08:00
mazengfei ab58651733 取盘优化 2026-01-15 11:39:13 +08:00
mazengfei aadba7efd5 增加人脸采集功能;
识别人脸成功后打开有餐盘的第一个柜子;
扫码打开已绑定的柜子,无则打开第一个空柜子并放入餐盘;
设置页面问题优化;
2026-01-14 19:00:20 +08:00
mazengfei 11a0a41e3c Rename .java to .kt 2026-01-14 19:00:20 +08:00
马增飞 582be4e9b7 绑定解绑功能调试;其它优化 2026-01-05 18:38:10 +08:00
马增飞 17d47a876f 增加配置接口;调试人脸识别;验证手机号密码登录和人脸登录功能; 2025-12-29 18:41:48 +08:00
马增飞 6c28260069 Merge remote-tracking branch 'refs/remotes/origin/main_新版_lvmeng_1222' into main_新版_lvmeng_1229 2025-12-29 09:57:05 +08:00
马增飞 7097bbf907 优化 2025-12-29 09:55:49 +08:00
lvmeng c046441d4e 优化 2025-12-26 14:16:01 +08:00
lvmeng 7a6c4012d1 餐盘柜接口改造 2025-12-26 14:05:41 +08:00
mazengfei 5e4dc4baa4 处理人脸数据不同步 2025-11-07 20:37:50 +08:00
mazengfei 4231c68cdc 添加方法 2025-11-07 15:18:15 +08:00
mazengfei 55ba11bf34 修改人脸同步逻辑 2025-11-07 10:55:48 +08:00
马增飞 abe594fb66 设备联调 2025-11-06 19:34:42 +08:00
lvmeng fb074c1aaa 修改开机初始化页面 2025-09-26 16:11:00 +08:00
lvmeng 73456046c5 增加初始化页面 2025-09-17 17:42:38 +08:00
lvmeng 7f56d4d1a1 修改待机也按钮名称 2025-09-17 15:45:00 +08:00
lvmeng 1658b6fe6f logo修改 2025-09-12 13:32:10 +08:00
lvmeng e9a366b52a 增加心跳处理人脸数据增减 2025-09-09 15:59:29 +08:00
lvmeng 3b492cb710 待机页面优化 2025-09-05 17:59:30 +08:00
lvmeng eaafeab1d8 增加余额不足判断 2025-09-05 15:29:43 +08:00
zxj 578a9aaced 添加注释 2025-08-22 16:28:20 +08:00
zxj cd375bfaf8 修复了登录成功后跳转界面错误问题 2025-08-21 15:50:05 +08:00
zxj 7a832ec40d 调整了初始化界面逻辑 2025-08-14 13:36:45 +08:00
zxj b83a442fe3 修改了配色,添加了解除绑定提示 2025-08-08 17:26:09 +08:00
zxj 2c10731b8d 修复了识别追踪信息功能 2025-08-08 10:50:11 +08:00
zxj 47f6e814a6 添加init界面,调整了认证后逻辑,调整了扫描枪扫描处理 2025-08-01 18:23:54 +08:00
zxj d20ee771b0 排序放入协程 2025-07-28 18:25:23 +08:00
zxj 6f18245c4f 密码登录后关闭密码界面 2025-07-28 18:05:06 +08:00
zxj 9a89fff9dd 工具类优化 2025-07-28 17:35:21 +08:00
zxj cc317117bc 修复了排序问题 2025-07-28 17:09:24 +08:00
zxj ca081f659b 初步实现功能 2025-07-28 15:47:55 +08:00
zxj 307fc70ae2 实现了垂直排序 2025-07-28 10:50:15 +08:00
zxj 8780f5d463 倒计时+1 2025-07-25 16:33:50 +08:00
zxj c0a628c51f 优化了键盘操作 2025-07-25 15:58:11 +08:00
zxj c2bae2f3e8 调整了餐盘柜3种情况处理 2025-07-25 13:35:17 +08:00
zxj c89efd2d6f 完善了功能 2025-07-25 10:36:30 +08:00
zxj 5b3773d277 调整了人脸识别界面 2025-07-24 18:02:42 +08:00
zxj 082011a939 调整了餐盘在不同设备提示 2025-07-24 08:40:36 +08:00
zxj 7fd2ccb960 调整了部分问题 2025-07-23 14:22:11 +08:00
zxj 5ad82eb224 添加了界面及逻辑 2025-07-22 15:48:12 +08:00
zxj 9d14b5499b 修改成java17 2025-07-18 17:42:05 +08:00
zxj 8e28e2f43c 填充了一些逻辑 2025-07-18 17:24:34 +08:00
zxj f97ef151c3 添加了界面及逻辑 2025-07-18 17:16:00 +08:00
194 changed files with 12574 additions and 283 deletions
Generated
+1
View File
@@ -0,0 +1 @@
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>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AppInsightsSettings">
<option name="selectedTabId" value="Firebase Crashlytics" />
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>
+9 -1
View File
@@ -2,8 +2,16 @@
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="SmartPlateCabinet.app">
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-03-18T06:09:00.079142300Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="Default" identifier="serial=192.168.1.161:5555;connection=2c537abb" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</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>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="gradleJvm" value="azul-17" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
+2 -3
View File
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="zulu-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
@@ -10,7 +9,7 @@
<component name="VisualizationToolProject">
<option name="state">
<ProjectState>
<option name="scale" value="0.19390498954287422" />
<option name="scale" value="0.24414062499999997" />
</ProjectState>
</option>
</component>
+28 -7
View File
@@ -6,21 +6,30 @@ plugins {
}
android {
signingConfigs {
getByName("debug") {
storeFile = file("./swkey.jks")
storePassword = "123456"
keyAlias = "android"
keyPassword = "123456"
}
}
namespace = "com.sw.platecabinet"
compileSdk = 35
defaultConfig {
applicationId = "com.sw.platecabinet"
applicationId = "com.sw.take.plate"
minSdk = 24
targetSdk = 35
versionCode = 1
versionName = "1.0"
versionName = "1.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ndk {
abiFilters.addAll(listOf("armeabi-v7a"/*, "arm64-v8a"*/))
}
setProperty("archivesBaseName", "zncpg_${versionName}")
}
buildTypes {
@@ -50,7 +59,10 @@ dependencies {
implementation(libs.material)
implementation(libs.androidx.activity)
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)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
@@ -62,11 +74,9 @@ dependencies {
implementation(libs.androidx.camera.view)
implementation(libs.androidx.camera.extensions)
// 权限申请
implementation(libs.accompanist.permissions)
// hilt注入
implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler)
// implementation(libs.hilt.android)
// ksp(libs.hilt.android.compiler)
// retrofit网络请求
implementation(libs.retrofit)
@@ -82,4 +92,15 @@ dependencies {
implementation(libs.kotlinx.coroutines.android)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.fragment.ktx)
// implementation(libs.adapter.coroutines)
implementation("androidx.work:work-runtime-ktx:2.8.1")
implementation(libs.core)
implementation(libs.android.core)
implementation("org.greenrobot:eventbus:3.3.1")
}
Binary file not shown.
+35 -10
View File
@@ -8,25 +8,51 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32"
tools:ignore="ScopedStorage" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:name=".MyApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:icon="@drawable/logo_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:roundIcon="@drawable/logo_launcher"
android:supportsRtl="true"
android:theme="@style/Theme.SmartPlateCabinet"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<!-- 注册BootReceiver,监听开机完成广播 -->
<receiver
android:name=".receiver.BootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<activity
android:name=".activity.LoginByPwdActivity"
android:name=".activity.InitActivity"
android:exported="false"
android:launchMode="singleTask" />
android:launchMode="singleTask">
</activity>
<activity
android:name=".activity.LoginByFaceActivity"
android:exported="true"
android:launchMode="singleTask"/>
<activity
android:name=".activity.DeviceInitActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -36,13 +62,12 @@
</activity>
<activity
android:name=".activity.MainActivity"
android:exported="true">
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.MAIN" />-->
<!-- <category android:name="android.intent.category.LAUNCHER" />-->
<!-- </intent-filter>-->
android:exported="false">
</activity>
<activity
android:name=".activity.PlcSettingActivity"
android:exported="false" />
<!-- <activity android:name="com.sw.platecabinet.activity.FunActivity" />-->
</application>
</manifest>
@@ -0,0 +1,72 @@
package com.sw.platecabinet
object GlobalData {
/**
* 同一设备全局使用的设备编号
*/
// var globalEquipmentCode: String = "202501171634"
var globalEquipmentCode: String = "202507231144"
/**
* app版本号
*/
var appVersion: String = ""
/**
* 横排数量
*/
var arrayCross: Int = 2
/**
* 竖排数量
*/
var arrayVertical: Int = 11
/**
* 排列方式 0 垂直 1 水平
*/
var arrayMode: Int = 0
var appId = "Hkz1rBk6PZXbS8KwKr67K2eZtsz8bRoMHLg64bUdgCZj"
var sdkKey = "AabXs3sHM8UhhE7oCGf4LVMLrdkGb1nJNksbjjTdVt7k"
var activeKey = "85Q1-1216-X3DH-QYTJ"//"085F-118G-Q3J6-35UX" //"085F-118G-Q391-53YL"
/**
* 具体业务baseurl
*/
var appBaseUrl: String = ""
/**
* 具体业务 BaseUrl
*/
const val TEST_BASE_URL: String = "http://192.168.1.201:14801"
const val UAT_BASE_URL: String = "https://dev.yixiong-tech.com:8083"
const val PROD_BASE_URL: String = "https://api.dm.yixiong-tech.com:8443"
/**
* 设备id
*/
var deviceId: String = ""
var restId: String = ""
}
/**
* 全局常量
*/
object Constants {
/**
* 自动关闭时间
*/
const val AUTO_CLOSE_TIME: Long = 3
}
/**
* key
*/
object GlobalKey {
const val KEY_EQUIPMENT_INFO = "equipmentInfo"
const val KEY_TOKEN = "tokenKey"
const val KEY_FIRST_RUN = "firstRun"
const val KEY_USER_INFO = "userInfoKey"
const val PARAM_EQUIPMENT_INFO = "equipmentUserInfo"
const val KEY_BASE_URL = "baseUrlKey"
}
+53 -2
View File
@@ -1,9 +1,60 @@
package com.sw.platecabinet
import android.app.Application
import android.content.Context
import android.annotation.SuppressLint
import android.util.Log
import com.sw.plate.App
import com.sw.plate.utils.AppUtil
import com.sw.platecabinet.utils.CrashHandler
import com.sw.platecabinet.utils.SpTool
import timber.log.Timber
class MyApp : App() {
companion object {
const val DEBUG: Boolean = true
var instance: MyApp? = null
// var plateList: List<EquipmentUserInfo>? = null
}
override fun onCreate() {
super.onCreate()
instance = this
Timber.plant(Timber.DebugTree())
Timber.d("初始化")
initGlobalData()
// 初始化崩溃处理器
CrashHandler.init(this)
}
/**
* 初始化全局数据
*/
@SuppressLint("LogNotTimber")
private fun initGlobalData() {
val deviceId = AppUtil.getUDID(this)
// deviceId = "be154831-3466-3ba2-a2ea-57652c919fed"
Log.d("MyApp", "initialize: 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.TEST_BASE_URL
else
GlobalData.PROD_BASE_URL
}
// TODO: 临时测试----------------
// if ("2987f0c5-5754-33e9-b00a-251db5e2e55f" == deviceId) {
// GlobalData.deviceId = "79814f20-f253-397c-9da7-8430ffa5ef00"
// }
GlobalData.appVersion = AppUtil.getAppVersionCode(this).toString()
GlobalData.globalEquipmentCode = "202501171634"
// GlobalData.appBaseUrl = "http://192.168.1.201:14801"
}
}
@@ -1,65 +1,419 @@
package com.sw.platecabinet.activity
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
import android.view.View
import android.view.WindowInsetsController
import android.view.WindowManager
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import com.sw.inbound.utils.DateTimeUtils
import com.sw.plate.App
import com.sw.plate.utils.Base64
import com.sw.plate.utils.arcface.facedb.FaceDatabase
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import com.sw.platecabinet.R
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import kotlinx.coroutines.CoroutineScope
import com.sw.platecabinet.ext.clickWithCoroutines
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.utils.IntervalExecutor
import com.sw.platecabinet.utils.SpTool
import com.sw.platecabinet.view.CustomLoadingDialog
import com.sw.platecabinet.viewmodel.UserViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
/**
* activity 基类
*/
abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
protected lateinit var binding: VB
private set
private var headerBinding: ItemTitleTimeBinding? = null
protected lateinit var context: Context
private var timeJob: Job? = null
private var mDialogWaiting: CustomLoadingDialog? = null
// private val permissionHelpers = mutableMapOf<Int, PermissionHelper>()
// protected var keyEventHelper: ScanGunKeyEventHelper? = null
// 用户对应的viewModel
protected val userViewModel by viewModels<UserViewModel>()
public val recognizeViewModel by viewModels<RecognizeViewModel>()
private var startTime: Long = 0
override fun onCreate(savedInstanceState: Bundle?) {
startTime = System.currentTimeMillis()
super.onCreate(savedInstanceState)
enableEdgeToEdge()
window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
context = this
disableSystemUICompletely()
// 确保内容延伸到导航栏区域
WindowCompat.setDecorFitsSystemWindows(window, false)
//保持亮屏
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
binding = inflateViewBinding()
headerBinding = inflateTitleBinding()
setContentView(binding.root)
updateTime()
registerDataChange()
initialize()
// registerKeyEvent()
val durationTime = System.currentTimeMillis() - startTime
Timber.d("启动时间:$durationTime")
}
// /**
// * 监听扫描枪扫描事件
// */
// protected fun registerKeyEvent() {
// keyEventHelper =
// ScanGunKeyEventHelper(context, object : ScanGunKeyEventHelper.OnScanSuccessListener {
// override fun onScanSuccess(barcode: String?) {
// Timber.d("onScanSuccess barcode = $barcode")
// if (barcode == null) return
// handleScanKeyInfo(barcode)
// }
// })
// }
/**
* 处理扫描枪数据
*/
protected open fun handleScanKeyInfo(scanInfo: String) {
//// settingViewModel.findByPlateNumber(plateNumber = scanInfo)
// var plateNumber = scanInfo
// if (scanInfo.length > 6) {
// plateNumber = scanInfo.take(6)
// }
// Log.d("TAG", "handleScanKeyInfo: " + Gson().toJson(MyApp.plateList))
// if (MyApp.plateList == null) {
// initPlateList()
// }
// //柜子未关联该餐盘,直接找空位置放入盘子
// val item = MyApp.plateList?.firstOrNull { it.plateNumber.isNullOrBlank() }
// if (item == null) {
// //柜子已满
// MainActivity.start(context = context, pageType = PageType.PLATE_CABINET_FULL)
// return
// }
// //找到位置,放入餐盘,清除已有id和时间
// item.plateNumber = plateNumber
// item.faceId = null
// item.updateTime = null
// PlateUtils.open(this, item.equipmentBoxCode, true)
// SpTool.savePlateData(MyApp.plateList!!)
}
// override fun dispatchKeyEvent(event: KeyEvent): Boolean {
// if (keyEventHelper != null) {
// if (keyEventHelper!!.isScanGunEvent(event)) {
// keyEventHelper!!.analysisKeyEvent(event)
// return true
// }
// }
// return super.dispatchKeyEvent(event)
// }
fun updateTime() {
headerBinding?.let {
val scope = CoroutineScope(Dispatchers.Main)
timeJob = scope.launch {
it.tvLeftDate.clickWithCoroutines(
onDoubleClick = {
onLeftDoubleClick()
}
)
it.tvRightTime.clickWithCoroutines(
onDoubleClick = {
onRightDoubleClick()
}
)
timeJob = lifecycleScope.launch {
DateTimeUtils.realTimeChineseDateFlow()
.collect { (date, time) ->
headerBinding!!.tvLeftDate.text = date
headerBinding!!.tvRightTime.text = time
.collect { date ->
it.tvRightTime.text = date
}
}
}
}
/**
* 左侧日期双击
*/
open fun onLeftDoubleClick() {
}
/**
* 右侧时间双击
*/
open fun onRightDoubleClick() {
//MainActivity.start(context, pageType = PageType.SETTING_LIST)
}
// override fun onRequestPermissionsResult(
// requestCode: Int,
// permissions: Array<out String>,
// grantResults: IntArray
// ) {
// super.onRequestPermissionsResult(requestCode, permissions, grantResults)
//
// val helper = permissionHelpers[requestCode]
// val allGranted = PermissionHelper.handlePermissionResult(
// this,
// requestCode,
// permissions,
// grantResults,
// permissionHelper = helper
// )
//
// if (allGranted) {
// permissionHelpers.remove(requestCode)
// }
// }
//
// /**
// * 注册PermissionHelper以便处理结果
// */
// fun registerPermissionHelper(helper: PermissionHelper) {
// permissionHelpers[helper.requestCode] = helper
// }
override fun onResume() {
super.onResume()
// 关键点2:防止某些场景下系统栏恢复
enforceImmersiveMode()
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
disableSystemUICompletely()
}
}
private fun disableSystemUICompletely() {
Timber.d("disableSystemUICompletely")
// 禁用系统手势(Android 10+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.insetsController?.systemBarsBehavior =
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
// 全屏+隐藏导航栏(所有版本通用)
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
)
// 禁止窗口扩展至系统栏区域(彻底锁定)
window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
}
private fun enforceImmersiveMode() {
Timber.d("enforceImmersiveMode")
// 持续强制隐藏系统栏(防止手势触发)
window.decorView.postDelayed({
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
)
}, 100) // 延迟100ms确保覆盖手势触发
}
/**
* 显示等待提示框
*/
fun showWaitingDialog(tip: String?) {
runOnUiThread {
hideWaitingDialog()
val view = View.inflate(this, R.layout.dialog_waiting, null)
if (!TextUtils.isEmpty(tip)) (view.findViewById<View?>(R.id.tvTip) as TextView).setText(
tip
)
mDialogWaiting = CustomLoadingDialog(this, view, R.style.MyDialog)
mDialogWaiting!!.show()
mDialogWaiting!!.setCancelable(true)
mDialogWaiting!!.setCanceledOnTouchOutside(true)
}
}
/**
* 隐藏等待提示框
*/
fun hideWaitingDialog() {
runOnUiThread {
mDialogWaiting?.dismiss()
mDialogWaiting = null
}
}
protected abstract fun inflateViewBinding(): VB
protected abstract fun inflateTitleBinding(): ItemTitleTimeBinding?
protected abstract fun initialize()
/**
* 注册数据监听
*/
protected open fun registerDataChange() {
Timber.d("registerDataChange")
}
override fun onDestroy() {
timeJob?.cancel()
// keyEventHelper?.onDestroy()
super.onDestroy()
}
private val intervalExecutor by lazy { IntervalExecutor() }
private var faceTaskJob: Job? = null
// private val initialDelay = 5 * 60 * 1000L
// private val dealyMillis = 10 * 60 * 1000L
private val initialDelay = 1 * 1000L
private val dealyMillis = 10 * 1000L
fun startFaceTask(block: (List<String?>) -> Unit) {
faceTaskJob =
intervalExecutor.startIntervalTaskWithInitialDelay(initialDelay, dealyMillis) {
getFaceIncrementList(action = block)
}
}
private var faceTimestamp = 0L
//private var taskPageNo = 1
private fun getFaceIncrementList(pageNo: Int = 1, action: (List<String?>) -> Unit) {
val timestamp = SpTool.lastFaceTimestamp
userViewModel.getFaceIncrementList(
pageNo = pageNo,
timestamp = timestamp
) { list ->
runOnUiThread {
if (pageNo == 1 && list.isEmpty()) {
return@runOnUiThread
}
updateFaceData(list)
val userIdList = list.map { it.userId }
action(userIdList)
if (list.isNotEmpty()) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0
}
SpTool.lastFaceTimestamp = faceTimestamp
}
}
}
private fun updateFaceData(list: List<UserFaceModel>) {
lifecycleScope.launch(Dispatchers.IO) {
val faceList = mutableListOf<FaceEntity>()
val faceDao = FaceDatabase.getInstance(this@BaseActivity).faceDao()
try {
list.forEach { model ->
if (model.faceDeleted == true) {
//删除数据
faceDao.deleteFaceById(model.userId)
} else {
// 获取该用户所有已存特征,判断当前特征是否已存在
val existList = faceDao.queryAllByUserName(model.userId)
val featureMatches =
existList.map { Base64.encode(it.featureData) == model.faceFeatureStr }
Timber.d("userId=${model.userId}, 已存记录数=${existList.size}, 各条特征是否与服务器一致=$featureMatches")
val alreadyExists = featureMatches.any { it }
if (!alreadyExists) {
//当前特征不存在,保存
runCatching { getNewFaceEntity(model) }
.onSuccess { faceList.add(it) }
.onFailure { Timber.e(it, "人脸特征解码失败: userId=${model.userId}") }
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
try {
if (faceList.isNotEmpty()) {
FaceDatabase.getInstance(this@BaseActivity).faceDao().insert(faceList)
}
recognizeViewModel.refreshFaceList()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private fun getNewFaceEntity(model: UserFaceModel): FaceEntity {
return FaceEntity(
model.userId,
null,
Base64.decode(model.faceFeatureStr)
).also {
//1-会员、2-临时用户
it.userType = if (model.member) "1" else "2"
it.cardNo = model.cardNo
}
}
fun clearAllFace(block: () -> Unit) {
lifecycleScope.launch(Dispatchers.IO) {
val faceDao = FaceDatabase.getInstance(App.getContext()).faceDao()
faceDao.deleteAll()
faceDao.resetId()
recognizeViewModel.refreshFaceList()
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)
}
}
@@ -0,0 +1,88 @@
package com.sw.platecabinet.activity
import android.content.Intent
import androidx.lifecycle.lifecycleScope
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.FaceApi
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.databinding.ActivityDeviceInitBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.utils.SpTool
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* 设备初始化界面
*/
class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
override fun inflateViewBinding(): ActivityDeviceInitBinding {
return ActivityDeviceInitBinding.inflate(layoutInflater)
}
override fun inflateTitleBinding(): ItemTitleTimeBinding? {
return null
}
override fun initialize() {
showWaitingDialog("加载中……")
lifecycleScope.launch {
withContext(Dispatchers.Default) {
//val count = FaceApi().queryFaceCount()
//if (count == 0) {
if (SpTool.firstGetFace) {
userViewModel.getUserFaceCache { status, msg ->
SpTool.firstGetFace = false
getDeviceConfig()
}
} else {
getDeviceConfig()
}
}
}
}
private fun getDeviceConfig() {
userViewModel.getDeviceConfig { deviceConfig ->
runOnUiThread {
if (deviceConfig == null) {
hideWaitingDialog()
ToastUtils.showToast("获取设备配置数据失败")
hideWaitingDialog()
goLoginActivity()
return@runOnUiThread
}
GlobalData.appId = deviceConfig.arcsoftAppId ?: ""
GlobalData.sdkKey = deviceConfig.arcsoftSdkKey ?: ""
GlobalData.activeKey = deviceConfig.arcsoftActiveKey ?: ""
binding.root.postDelayed({
hideWaitingDialog()
goLoginActivity()
}, 500)
}
}
}
override fun registerDataChange() {
super.registerDataChange()
// lifecycleScope.launch {
// deviceViewModel.deviceInfoResult.collect { it ->
// if (it == true) {
// goLoginActivity()
// }
// }
// }
}
private fun goLoginActivity() {
val intent = Intent(this, LoginByFaceActivity::class.java)
startActivity(intent)
finish()
}
override fun onLeftDoubleClick() {
}
}
@@ -0,0 +1,152 @@
//package com.sw.platecabinet.activity
//
//import androidx.activity.viewModels
//import androidx.lifecycle.lifecycleScope
//import com.sw.plate.utils.Base64
//import com.sw.plate.utils.ToastUtils
//import com.sw.plate.utils.arcface.facedb.FaceDatabase
//import com.sw.platecabinet.MyApp
//import com.sw.platecabinet.databinding.ActivityFunBinding
//import com.sw.platecabinet.databinding.ItemTitleTimeBinding
//import com.sw.platecabinet.dialog.CustomDialog
//import com.sw.platecabinet.ext.clickWithDebounce
//import com.sw.platecabinet.utils.PlateUtils
//import com.sw.platecabinet.utils.SpTool
//import com.sw.platecabinet.viewmodel.UserViewModel
//import kotlinx.coroutines.Dispatchers
//import kotlinx.coroutines.delay
//import kotlinx.coroutines.launch
//import kotlinx.coroutines.withContext
//import kotlin.getValue
//
//class FunActivity : BaseActivity<ActivityFunBinding>() {
//
// override fun inflateTitleBinding(): ItemTitleTimeBinding? {
// return binding.includeHeader
// }
//
// override fun inflateViewBinding() = ActivityFunBinding.inflate(layoutInflater)
//
// override fun initialize() {
// binding.btnClearFace.clickWithDebounce { clearFace() }
// binding.btnClearAndGetAll.clickWithDebounce { clearFaceAndGetAll() }
// binding.btnOpenAllPlate.clickWithDebounce {
// lifecycleScope.launch {
// openAllPlate()
// }
// }
// binding.btnPutPlate.clickWithDebounce { putPlate() }
//
// queryUserCount()
//
// binding.btnTestFeature.clickWithDebounce {
// testFeature()
// }
// }
// private fun testFeature() {
// userViewModel.getUserFace(1, 1) {list ->
// lifecycleScope.launch {
// withContext(Dispatchers.IO) {
// if (list.isEmpty()) return@withContext
// val userId = list[0].userId
// val base64Feature = list[0].faceFeatureStr
// val faceDao = FaceDatabase.getInstance(this@FunActivity).faceDao()
// val entityList = faceDao.queryAllByUserName(userId)
// val resultList = entityList.map { base64Feature == Base64.encode(it.featureData) }
// val isEqual = resultList.any { true }
// ToastUtils.showToast("比较结果:$isEqual")
// }
// }
// }
// }
//
// private fun clearFace() {
// loadClearDialog("请确认是否清除本地所有数据?") {
// showWaitingDialog("加载中……")
// clearAllFace {
// binding.root.postDelayed(
// {
// hideWaitingDialog()
// ToastUtils.showToast("已清除")
// queryUserCount()
// }, 500
// )
// }
// }
// }
//
// private fun clearFaceAndGetAll() {
// loadClearDialog("请确认是否清除本地所有数据,并重新拉取全量人脸数据?") {
// showWaitingDialog("加载中……")
// clearAllFace {
// userViewModel.getUserFaceCache { status, msg ->
// hideWaitingDialog()
// ToastUtils.showToast(
// if (status) "请求成功,$msg" else "请求失败,$msg"
// )
// queryUserCount()
// }
// }
// }
// }
//
// private suspend fun openAllPlate() {
// MyApp.plateList?.let { plateList ->
// val boxCodeList = plateList.filter { it.equipmentBoxCode.isNullOrBlank().not() }
// .map { it.equipmentBoxCode }
// boxCodeList.forEach { boxCode ->
// withContext(Dispatchers.Default) {
// PlateUtils.openDirect(boxCode)
// delay(500)
// }
// }
// }
// }
//
// private fun putPlate() {
// showWaitingDialog("加载中……")
// MyApp.plateList?.forEachIndexed { index, item ->
// val num = index + 1
// val realNum = if (num < 10) "0${num}" else "${num}"
// item.plateNumber = realNum + realNum + realNum
// item.faceId = null
// item.updateTime = null
// }
// SpTool.savePlateData(MyApp.plateList!!)
// binding.root.postDelayed({
// hideWaitingDialog()
// ToastUtils.showToast("已完成")
// }, 1000)
// }
//
// /**
// * 加载清除数据弹窗
// */
// 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}人"
// }
// }
//
//}
@@ -0,0 +1,48 @@
package com.sw.platecabinet.activity
import android.util.Log
import androidx.lifecycle.lifecycleScope
import com.sw.plate.utils.arcface.FaceApi
import com.sw.plate.utils.arcface.faceserver.FaceServer
import com.sw.platecabinet.databinding.ActivityInitBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
/**
* 待机界面
*/
class InitActivity : BaseActivity<ActivityInitBinding>() {
override fun inflateViewBinding(): ActivityInitBinding {
return ActivityInitBinding.inflate(layoutInflater)
}
override fun inflateTitleBinding(): ItemTitleTimeBinding? {
return binding.includeHeader
}
override fun initialize() {
Log.d("InitActivity", "collectFace,initialize: ")
binding.takeButton.setOnClickListener {
finish()
}
// binding.takeButton.setOnClickListener {
//
// lifecycleScope.launch(Dispatchers.IO) {
// FaceApi().deleteByUserName("288")
// }
//
// finish()
// }
}
override fun onLeftDoubleClick() {
finish()
}
override fun onRightDoubleClick() {
PlcSettingActivity.start(this)
}
}
File diff suppressed because it is too large Load Diff
@@ -1,23 +0,0 @@
package com.sw.platecabinet.activity
import android.content.Intent
import com.sw.platecabinet.databinding.ActivityLoginByPwdBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
class LoginByPwdActivity : BaseActivity<ActivityLoginByPwdBinding>() {
override fun inflateViewBinding(): ActivityLoginByPwdBinding {
return ActivityLoginByPwdBinding.inflate(layoutInflater)
}
override fun inflateTitleBinding(): ItemTitleTimeBinding {
return binding.includeHeader
}
override fun initialize() {
binding.tvFaceRec.setOnClickListener {
val intent = Intent(this, LoginByFaceActivity::class.java)
startActivity(intent)
}
}
}
@@ -1,9 +1,23 @@
package com.sw.platecabinet.activity
import android.content.Context
import android.content.Intent
import com.sw.platecabinet.R
import com.sw.platecabinet.databinding.ActivityMainBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.fragment.PlateCabinetFullFragment
import com.sw.platecabinet.fragment.PlateOpenFragment
import com.sw.platecabinet.utils.FragmentHelper
typealias Callback = (String) -> Unit
/**
* 主界面,内部嵌套framgnet使用
*/
class MainActivity : BaseActivity<ActivityMainBinding>() {
private lateinit var fragmentHelper: FragmentHelper
private var pageType = PageType.SETTING_LIST
private var callback: Callback? = null
override fun inflateViewBinding(): ActivityMainBinding {
return ActivityMainBinding.inflate(layoutInflater)
@@ -13,7 +27,110 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
return binding.includeHeader
}
override fun initialize() {
// 静态启动方法
companion object {
private const val PARAM_PAGE_TYPE = "pageType"
// private const val PARAM_EQUIPMENT_INFO = "equipmentUserInfo"
// private const val PARAM_IS_ADMIN = "isAdmin"
// fun start(
// context: Context,
// pageType: PageType,
// equipmentUserInfo: EquipmentUserInfo? = null,
// isAdmin: Boolean = false,
// block:()->Unit={}
// ) {
// val intent = Intent(context, MainActivity::class.java)
// intent.putExtra(PARAM_PAGE_TYPE, pageType.name)
// intent.putExtra(PARAM_EQUIPMENT_INFO, equipmentUserInfo)
// intent.putExtra(PARAM_IS_ADMIN, isAdmin)
// context.startActivity(intent)
// }
fun start(context: Context, pageType: PageType, isScanCode: Boolean = false) {
val intent = Intent(context, MainActivity::class.java)
intent.putExtra(PARAM_PAGE_TYPE, pageType.name)
intent.putExtra(PlateOpenFragment.IS_SCAN_CODE, isScanCode)
context.startActivity(intent)
}
}
}
override fun initialize() {
val param = intent.getStringExtra(PARAM_PAGE_TYPE)
// val isAdmin = intent.getBooleanExtra(PARAM_IS_ADMIN, false)
val isScanCode = intent.getBooleanExtra(PlateOpenFragment.IS_SCAN_CODE, false)
// val equipmentUserInfo = intent.getParcelableExtra<EquipmentUserInfo>(PARAM_EQUIPMENT_INFO)
pageType = param?.let { PageType.valueOf(param) } ?: PageType.SETTING_LIST
fragmentHelper = FragmentHelper(supportFragmentManager, R.id.fragment_container)
val page = when (pageType) {
// PageType.SETTING_LIST -> SettingListFragment()
// PageType.BIND_PLATE -> BindPlateFragment.newInstance(equipmentUserInfo)
// PageType.PLATE_TIP -> PlateOpenFragment.newInstance(equipmentUserInfo, isAdmin)
PageType.PLATE_TIP -> PlateOpenFragment.newInstance(isScanCode)
// PageType.UNBIND_PLATE -> UnBindPlateFragment.newInstance(equipmentUserInfo)
PageType.PLATE_CABINET_FULL -> PlateCabinetFullFragment()
else -> null
}
page?.let {
fragmentHelper.addFragment(it)
}
}
override fun onLeftDoubleClick() {
if (pageType == PageType.SETTING_LIST) {
finish()
}
}
override fun onRightDoubleClick() {
if (pageType == PageType.SETTING_LIST) {
// startActivity(Intent(this, FunActivity::class.java))
}
}
override fun handleScanKeyInfo(scanInfo: String) {
if (callback != null) {
callback!!.invoke(scanInfo)
}
}
fun registerKeyEventInfo(callback: Callback) {
this.callback = callback
}
override fun registerDataChange() {
super.registerDataChange()
}
}
/**
* 界面类型
*/
enum class PageType {
/**
* 提示
*/
PLATE_TIP,
/**
* 设置列表
*/
SETTING_LIST,
/**
* 绑定餐盘
*/
BIND_PLATE,
/**
* 解绑餐盘
*/
UNBIND_PLATE,
/**
* 餐盘柜已满
*/
PLATE_CABINET_FULL
}
@@ -0,0 +1,362 @@
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.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()
binding.btnSwitchEnv.setOnClickListener {
EnvSwitchDialog(context = this, onEnvChanged = {
showWaitingDialog("重置人脸数据中……")
lifecycleScope.launch {
withContext(Dispatchers.IO) {
FaceServer.getInstance().clearAllFaces()
}
}
userViewModel.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()
}
// ==================== 生命周期 ====================
/**
* 左侧日期双击 → 关闭设置界面
*/
override fun onLeftDoubleClick() {
finish()
}
/**
* 右侧时间双击 → 不做操作,防止循环打开
*/
override fun onRightDoubleClick() {
// 空实现,避免在设置页再次触发跳转
}
// ==================== 工具方法 ====================
/**
* 隐藏软键盘
*/
private fun hideKeyboard() {
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(currentFocus?.windowToken, 0)
}
}
@@ -0,0 +1,92 @@
package com.sw.platecabinet.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.sw.platecabinet.R
/**
* 通用分页适配器(支持ViewBinding
*/
class GenericPageAdapter<T, VB : ViewBinding>(
private var pages: List<List<T>>,
private val pageLayoutId: Int,
private val itemBindingInflater: (LayoutInflater, ViewGroup, Boolean) -> VB,
private val itemBindCallback: VB.(item: T, position: Int) -> Unit
) : RecyclerView.Adapter<GenericPageAdapter<T, VB>.PageViewHolder>() {
fun updatePages(newPages: List<List<T>>) {
this.pages = newPages
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageViewHolder {
val view = LayoutInflater.from(parent.context).inflate(pageLayoutId, parent, false)
val holder = PageViewHolder(view)
// 在创建ViewHolder时设置ItemDecoration,只设置一次
holder.childRecyclerView.addItemDecoration(
GridSpacingItemDecoration(
spanCount = 2,
spacing = view.context.dpToPx(12),
includeEdge = true // 包含边缘间距
)
)
return holder
}
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) {
val childRecyclerView: RecyclerView = itemView.findViewById(R.id.childRecyclerView)
init {
childRecyclerView.layoutManager = GridLayoutManager(itemView.context, 2)
}
fun bind(items: List<T>) {
childRecyclerView.adapter =
GenericItemAdapter(items, itemBindingInflater, itemBindCallback)
}
}
}
/**
* 通用子项适配器(支持ViewBinding
*/
class GenericItemAdapter<T, VB : ViewBinding>(
private var items: List<T>,
private val bindingInflater: (LayoutInflater, ViewGroup, Boolean) -> VB,
private val bindCallback: VB.(item: T, position: Int) -> Unit,
) : RecyclerView.Adapter<GenericItemAdapter<T, VB>.ViewHolder>() {
inner class ViewHolder(private val binding: VB) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: T, position: Int) {
binding.bindCallback(item, position)
}
}
fun updateData(newItem: List<T>) {
this.items = newItem
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = bindingInflater(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position], position)
}
override fun getItemCount(): Int = items.size
}
@@ -0,0 +1,54 @@
package com.sw.platecabinet.adapter
import android.content.Context
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
/**
* 为RecyclerView添加间距的ItemDecoration
*/
class GridSpacingItemDecoration(
private val spanCount: Int, // 列数
private val spacing: Int, // 间距(dp)
private val includeEdge: Boolean // 是否包含边缘
) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val position = parent.getChildAdapterPosition(view) // item position
val column = position % spanCount // item column
if (includeEdge) {
// 左右间距计算
outRect.left = spacing - column * spacing / spanCount
outRect.right = (column + 1) * spacing / spanCount
// 第一行添加顶部间距
if (position < spanCount) {
outRect.top = spacing
}
outRect.bottom = spacing // 每行底部都添加间距
} else {
// 左右间距计算(不包含边缘)
outRect.left = column * spacing / spanCount
outRect.right = spacing - (column + 1) * spacing / spanCount
// 不是第一行时添加顶部间距
if (position >= spanCount) {
outRect.top = spacing
}
}
}
}
/**
* 将dp值转换为px
*/
fun Context.dpToPx(dp: Int): Int {
return (dp * resources.displayMetrics.density).toInt()
}
@@ -0,0 +1,34 @@
package com.sw.platecabinet.adapter
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.sw.platecabinet.databinding.ListItemLogInfoBinding
class LogAdapter(var list: MutableList<String>) : RecyclerView.Adapter<LogAdapter.ViewHolder>() {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.binding.tvLogText.text = list[position]
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding =
ListItemLogInfoBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun getItemCount() = list.size
inner class ViewHolder(var binding: ListItemLogInfoBinding) :
RecyclerView.ViewHolder(binding.root)
fun addData(logInfo: String) {
Handler(Looper.getMainLooper()).post {
// list.add(logInfo)
// notifyDataSetChanged()
}
}
}
@@ -0,0 +1,36 @@
//package com.sw.platecabinet.dialog
//
//import android.app.Dialog
//import android.graphics.drawable.ColorDrawable
//import android.os.Bundle
//import android.view.Window
//import androidx.fragment.app.FragmentActivity
//import com.sw.platecabinet.databinding.DialogBalanceNotEnoughBinding
//import com.sw.platecabinet.ext.dp
//
//open class BalanceNotEnoughDialog(
// private var activity: FragmentActivity
//) : Dialog(activity) {
//
// private lateinit var binding: DialogBalanceNotEnoughBinding
//
// override fun onCreate(savedInstanceState: Bundle?) {
// super.onCreate(savedInstanceState)
// requestWindowFeature(Window.FEATURE_NO_TITLE)
// binding = DialogBalanceNotEnoughBinding.inflate(layoutInflater)
// setContentView(binding.root)
// window?.run {
// attributes = attributes.apply {
// width = 480.dp
// }
// setBackgroundDrawable(ColorDrawable())
// setCancelable(false)
// }
// addListener()
// }
//
// private fun addListener() {
// 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,48 @@
package com.sw.platecabinet.dialog
import android.app.Dialog
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.Window
import androidx.fragment.app.FragmentActivity
import com.sw.platecabinet.databinding.DialogCustomBinding
import com.sw.platecabinet.databinding.DialogUserBindRemindBinding
import com.sw.platecabinet.ext.dp
open class CustomDialog(
var activity: FragmentActivity,
var content: String,
var onCancel: () -> Unit = {},
var onConfirm: () -> Unit = {},
) : Dialog(activity) {
private lateinit var binding: DialogCustomBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
binding = DialogCustomBinding.inflate(layoutInflater)
setContentView(binding.root)
window?.run {
attributes = attributes.apply {
width = 480.dp
}
setBackgroundDrawable(ColorDrawable())
setCancelable(false)
}
binding.tvContent.text = content
addClickListener()
}
private fun addClickListener() {
binding.btnCancel.setOnClickListener {
onCancel()
dismiss()
}
binding.btnConfirm.setOnClickListener {
onConfirm()
dismiss()
}
}
}
@@ -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.TEST_BASE_URL -> binding.rbTest.isChecked = true
GlobalData.UAT_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.TEST_BASE_URL
binding.rbUat.id -> GlobalData.UAT_BASE_URL
binding.rbProd.id -> GlobalData.PROD_BASE_URL
else -> return@setOnClickListener
}
GlobalData.appBaseUrl = newUrl
SpTool.baseUrl = newUrl
onEnvChanged(newUrl)
dismiss()
}
}
}
@@ -0,0 +1,38 @@
//package com.sw.platecabinet.dialog
//
//import android.app.Dialog
//import android.graphics.drawable.ColorDrawable
//import android.os.Bundle
//import android.view.Window
//import androidx.fragment.app.FragmentActivity
//import com.sw.platecabinet.databinding.DialogUserBindRemindBinding
//import com.sw.platecabinet.ext.dp
//
//open class UserBindRemindDialog(
// private var activity: FragmentActivity,
// var content:String
//) : Dialog(activity) {
//
// private lateinit var binding: DialogUserBindRemindBinding
//
// override fun onCreate(savedInstanceState: Bundle?) {
// super.onCreate(savedInstanceState)
// requestWindowFeature(Window.FEATURE_NO_TITLE)
// binding = DialogUserBindRemindBinding.inflate(layoutInflater)
// setContentView(binding.root)
// window?.run {
// attributes = attributes.apply {
// width = 480.dp
// }
// setBackgroundDrawable(ColorDrawable())
// setCancelable(false)
// }
// binding.tvBindContent.text = content
// addListener()
// }
//
// private fun addListener() {
// binding.tvConfirm.setOnClickListener { dismiss() }
// }
//
//}
@@ -1,7 +1,10 @@
package com.sw.platecabinet.ext
import android.content.Context
import android.content.res.Resources
import android.util.TypedValue
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.annotation.Dimension
/**
@@ -46,4 +49,28 @@ val Float.sp: Float
TypedValue.COMPLEX_UNIT_SP,
this,
Resources.getSystem().displayMetrics
)
)
fun formatNumber(num: String): String {
return num.padStart(2, '0')
}
fun View.visible() {
visibility = View.VISIBLE
}
fun View.invisible() {
visibility = View.INVISIBLE
}
fun View.gone() {
visibility = View.GONE
}
/**
* 隐藏软键盘
*/
fun View.hideKeyboard() {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(windowToken, 0)
}
@@ -0,0 +1,37 @@
package com.sw.platecabinet.ext
/**
* 姓名脱敏处理
* 2个字:张*
* 多于2个字:张*某
*/
fun String?.maskName(): String {
if (this == null || this.isEmpty()) return ""
return when {
length == 2 -> "${this[0]}*"
length > 2 -> "${this[0]}*${this[length - 1]}"
else -> this // 1个字的情况原样返回
}
}
/**
* 手机号脱敏处理
* 将第4-7位替换为*
* 例如:138****1234
*/
fun String?.maskPhone(): String {
if (this == null || this.isEmpty()) return ""
if (this.length < 11) {
return this
}
val start = 3 // 第4位(索引从0开始)
val end = 6 // 第7位
val sb = StringBuilder(this)
for (i in start..end) {
sb.setCharAt(i, '*')
}
return sb.toString()
}
@@ -0,0 +1,104 @@
package com.sw.platecabinet.ext
import android.os.Handler
import android.os.Looper
import android.view.View
import android.widget.TextView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/**
* 设置 TextView 的双击和单击事件监听器
* @param doubleClickInterval 双击间隔时间(毫秒),默认 300ms
* @param onDoubleClick 双击回调
* @param onSingleClick 单击回调
*/
fun TextView.setClickListeners(
doubleClickInterval: Long = 300,
onDoubleClick: (View) -> Unit = {},
onSingleClick: (View) -> Unit = {}
) {
var clickCount = 0
val handler = Handler(Looper.getMainLooper())
setOnClickListener { view ->
clickCount++
handler.postDelayed({
if (clickCount == 1) {
onSingleClick(view)
} else if (clickCount == 2) {
onDoubleClick(view)
}
clickCount = 0 // 重置计数
}, doubleClickInterval)
}
}
fun View.clickWithCoroutines(
doubleClickInterval: Long = 300,
onDoubleClick: (View) -> Unit = {},
onSingleClick: (View) -> Unit = {}
) {
var pendingItem: View? = null
var lastClickTime: Long = 0
var clickJob: Job? = null
setOnClickListener { view ->
val currentTime = System.currentTimeMillis()
// 如果是同一控件且在双击间隔内,则判定为双击
if (pendingItem === view && currentTime - lastClickTime < doubleClickInterval) {
// 取消之前的单击任务
clickJob?.cancel()
onDoubleClick(view)
pendingItem = null
} else {
// 取消之前的单击任务
clickJob?.cancel()
// 启动新的单击任务
clickJob = CoroutineScope(Dispatchers.Main).launch {
delay(doubleClickInterval)
onSingleClick(view)
}
pendingItem = view
lastClickTime = currentTime
}
}
}
//fun View.clickWithDebounce(delay: Long = 500, action: () -> Unit) {
// var job: Job? = null
// setOnClickListener {
// job?.cancel()
// job = CoroutineScope(Dispatchers.Main).launch {
// delay(delay)
// action()
// }
// }
//}
/**
* 极简版防重复点击扩展函数
* @param delay 防抖时间(默认300ms
* @param action 点击执行逻辑
*/
fun View.clickWithDebounce(delay: Long = 500, action: () -> Unit) {
setOnClickListener {
// 用View的tag存储是否可点击的状态(默认可点击)
if (tag as? Boolean ?: true) {
tag = false // 标记为不可点击
action() // 立即执行点击逻辑
// 启动协程,延迟后恢复可点击状态
CoroutineScope(Dispatchers.Main).launch {
delay(delay)
tag = true // 恢复可点击
}
}
}
}
@@ -0,0 +1,67 @@
package com.sw.platecabinet.fragment
import android.app.Dialog
import android.os.Bundle
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
import com.sw.platecabinet.R
import com.sw.platecabinet.view.CustomLoadingDialog
abstract class BaseFragment<VB : ViewBinding>(
private val bindingInflater: (inflater: LayoutInflater, parent: ViewGroup?, attachToParent: Boolean) -> VB
) : Fragment() {
protected var _binding: VB? = null
protected val binding get() = _binding!!
private var mDialogWaiting: CustomLoadingDialog? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = bindingInflater(inflater, container, false)
initialize()
registerDataChange()
return binding.root
}
/**
* 注册数据变化监听
*/
open fun registerDataChange() {
}
abstract fun initialize()
/**
* 显示等待提示框
*/
fun showWaitingDialog(tip: String?): Dialog? {
hideWaitingDialog()
val view = View.inflate(requireContext(), R.layout.dialog_waiting, null)
if (!TextUtils.isEmpty(tip)) (view.findViewById<View?>(R.id.tvTip) as TextView).text = tip
mDialogWaiting = CustomLoadingDialog(requireContext(), view, R.style.MyDialog)
mDialogWaiting!!.show()
mDialogWaiting!!.setCancelable(true)
return mDialogWaiting
}
/**
* 隐藏等待提示框
*/
fun hideWaitingDialog() {
mDialogWaiting?.dismiss()
mDialogWaiting = null
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
@@ -0,0 +1,317 @@
//package com.sw.platecabinet.fragment
//
//import android.annotation.SuppressLint
//import android.os.Bundle
//import android.text.Editable
//import android.text.TextUtils
//import android.text.TextWatcher
//import android.view.inputmethod.EditorInfo
//import androidx.fragment.app.viewModels
//import androidx.lifecycle.lifecycleScope
//import androidx.recyclerview.widget.GridLayoutManager
//import androidx.recyclerview.widget.LinearLayoutManager
//import androidx.recyclerview.widget.RecyclerView
//import com.sw.plate.utils.ToastUtils
//import com.sw.plate.utils.comn.SerialApi
//import com.sw.plate.utils.comn.SerialPortManager
//import com.sw.platecabinet.R
//import com.sw.platecabinet.activity.MainActivity
//import com.sw.platecabinet.activity.PageType
//import com.sw.platecabinet.adapter.GenericItemAdapter
//import com.sw.platecabinet.adapter.GridSpacingItemDecoration
//import com.sw.platecabinet.adapter.dpToPx
//import com.sw.platecabinet.databinding.FragmentBindPlateBinding
//import com.sw.platecabinet.databinding.ItemSearchUserInfoBinding
//import com.sw.platecabinet.dialog.UserBindRemindDialog
//import com.sw.platecabinet.ext.maskName
//import com.sw.platecabinet.ext.maskPhone
//import com.sw.platecabinet.model.ErrorInfo
//import com.sw.platecabinet.model.request.BindParam
//import com.sw.platecabinet.model.response.EquipmentUserInfo
//import com.sw.platecabinet.model.response.SearchResult
//import com.sw.platecabinet.utils.Debouncer
//import com.sw.platecabinet.utils.KeyboardUtils
//import com.sw.platecabinet.viewmodel.SettingViewModel
//import com.sw.platecabinet.viewmodel.UserViewModel
//import kotlinx.coroutines.launch
//import timber.log.Timber
//
///**
// * 餐盘柜绑定
// */
//class BindPlateFragment : BaseFragment<FragmentBindPlateBinding>(
// FragmentBindPlateBinding::inflate
//) {
// private lateinit var adapter: GenericItemAdapter<SearchResult.Member, ItemSearchUserInfoBinding>
// internal val KEY_ACTION_TYPE = "actionType"
// private var info: EquipmentUserInfo? = null
// private var checkedItem: SearchResult.Member? = null
// private var lastText = ""
// private val debouncer = Debouncer(2000)
//
// private val userViewModel by viewModels<UserViewModel>()
//
// companion object {
// @JvmStatic
// fun newInstance(userInfo: EquipmentUserInfo?) =
// BindPlateFragment().apply {
// arguments = Bundle().apply {
// putParcelable(KEY_ACTION_TYPE, userInfo)
// }
// }
// }
//
// @SuppressLint("SetTextI18n")
// override fun initialize() {
// arguments?.let { args ->
// info = args.getParcelable(KEY_ACTION_TYPE)
// info?.let {
// binding.tvNum.text = "${it.equipmentName}-${it.equipmentBoxCode}"
// if (!TextUtils.isEmpty(it.plateNumber)) {
//// binding.etCode.text = Editable.Factory.getInstance().newEditable(it.plateNumber)
// binding.etCode.apply {
// setText(it.plateNumber)
// setSelection(length())
// }
// }
// }
// }
// initView()
// initListener()
// }
//
// private fun initView() {
// adapter = createAdapter()
// binding.recyclerview.layoutManager = GridLayoutManager(context, 2)
// // 添加间距装饰(12dp)
// binding.recyclerview.addItemDecoration(
// GridSpacingItemDecoration(
// spanCount = 2,
// spacing = requireContext().dpToPx(6),
// includeEdge = false // 包含边缘间距
// )
// )
// binding.recyclerview.adapter = adapter
// binding.recyclerview.addOnScrollListener(object : RecyclerView.OnScrollListener() {
// override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
// super.onScrolled(recyclerView, dx, dy)
// val layoutManager = recyclerView.layoutManager as LinearLayoutManager
// val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
// val totalItems = layoutManager.itemCount
//
// if (!viewModel.isLoading && viewModel.canLoadMore
// && lastVisibleItem >= totalItems - 3
// ) {
// viewModel.getSearchMemberList(
// param = lastText,
// pageNum = viewModel.currentPage
// )
// }
// }
// })
// (activity as MainActivity).registerKeyEventInfo {
//// binding.etCode.text.clear()
//// binding.etCode.postDelayed({
//// binding.etCode.text.clear()
//// binding.etCode.text = Editable.Factory.getInstance().newEditable(it)
// activity?.runOnUiThread {
// if (binding.etUserInfo.isFocused) {
// return@runOnUiThread
// }
// binding.etCode.apply {
// if (it.length > 6) {
// setText(it.substring(0, 6))
// } else {
// setText(it)
// }
// if (length() > 0) {
// setSelection(length())
// }
// }
// binding.etUserInfo.requestFocus()
// }
//// }, 50)
// }
// }
//
// private fun initListener() {
// binding.etCode.addTextChangedListener(object : TextWatcher {
// override fun beforeTextChanged(
// s: CharSequence?,
// start: Int,
// count: Int,
// after: Int
// ) {
// Timber.d("beforeTextChanged s = ${s.toString()}, start = $start, count = $count, after = $after")
// }
//
// override fun onTextChanged(
// s: CharSequence?,
// start: Int,
// before: Int,
// count: Int
// ) {
// Timber.d("onTextChanged s = ${s.toString()}, start = $start, count = $count, before = $before")
// }
//
// override fun afterTextChanged(s: Editable?) {
// Timber.d("afterTextChanged s = ${s.toString()}")
// }
// })
// binding.etUserInfo.setOnEditorActionListener { _, actionId, keyEvent ->
// if (actionId == EditorInfo.IME_ACTION_SEARCH) {
// val currentText = binding.etUserInfo.text.toString()
// val containsNewlines = currentText.contains('\n') || currentText.contains('\r')
// if (containsNewlines) {
// Timber.d("非用户主动搜索")
// true
// }
// debouncer.debounce {
// // 执行搜索操作
// viewModel.getSearchMemberList(param = currentText, pageNum = 1)
// // 隐藏键盘
// KeyboardUtils.hideKeyboard(requireActivity())
// }
// true // 表示已处理该事件
// } else {
// false // 未处理其他动作
// }
// }
// binding.ivSearch.setOnClickListener {
// debouncer.debounce {
// // 执行搜索操作
// viewModel.getSearchMemberList(
// param = binding.etUserInfo.text.toString(),
// pageNum = 1
// )
// // 隐藏键盘
// KeyboardUtils.hideKeyboard(requireActivity())
// }
// }
// binding.llBind.setOnClickListener {
// if (equipmentBoxCode.isNullOrBlank().not()) {
// showBindDialog()
// return@setOnClickListener
// }
// if (info == null || info!!.equipmentCode?.isEmpty() == true || info!!.equipmentBoxCode?.isEmpty() == true) {
// ToastUtils.showToast("传入的设备信息异常")
// return@setOnClickListener
// }
// val plateNumber = binding.etCode.text.toString()
// if (plateNumber.isEmpty()) {
// ToastUtils.showToast("请输入餐盘号")
// return@setOnClickListener
// }
// if (checkedItem == null) {
// ToastUtils.showToast("请选择要绑定的会员")
// return@setOnClickListener
// }
// if (checkedItem!!.id == null) {
// ToastUtils.showToast("选中的会员信息异常")
// return@setOnClickListener
// }
// val bindParam = BindParam(
// equipmentId = info?.equipmentId,
// equipmentCode = info?.equipmentCode,
// equipmentBoxCode = info?.equipmentBoxCode,
// faceId = checkedItem?.faceId,
// plateNumber = plateNumber
// )
// viewModel.bindPlate(bindParam) { pair ->
// updateBindState(pair)
// }
// KeyboardUtils.hideKeyboard(requireActivity())
// }
// binding.tvBack.setOnClickListener { activity?.finish() }
// }
//
// /**
// * 监听数据变化
// */
// override fun registerDataChange() {
// super.registerDataChange()
// lifecycleScope.launch {
// viewModel.searchMemberList.collect {
// adapter.updateData(it)
// }
// }
//// lifecycleScope.launch {
//// viewModel.bindStateChange.collect {
//// updateBindState(it)
//// }
//// }
// }
//
// private fun createAdapter(): GenericItemAdapter<SearchResult.Member, ItemSearchUserInfoBinding> {
// return GenericItemAdapter(
// items = emptyList(),
// bindingInflater = ItemSearchUserInfoBinding::inflate,
// bindCallback = { item, position ->
// this.tvName.text = item.name.maskName()
// this.tvPhone.text = item.phone.maskPhone()
// if (item.id != checkedItem?.id) {
// this.llRoot.setBackgroundResource(R.drawable.grid_item_bind)
// } else {
// this.llRoot.setBackgroundResource(R.drawable.grid_item_unbind)
// }
//
// this.llRoot.setOnClickListener {
// Timber.d("itemClick ${item.name}, position = $position")
// checkedItem = item
// adapter.notifyDataSetChanged()
// //TODO 调用接口查询用户是否已绑盘
// this@BindPlateFragment.equipmentBoxCode = null
// (activity as? MainActivity)?.showWaitingDialog("查询中,请稍后……")
// userViewModel.getUserInfoById(memberId = item.faceId) {
// (activity as? MainActivity)?.hideWaitingDialog()
// this@BindPlateFragment.equipmentBoxCode = it?.equipmentBoxCode
// }
// }
// }
// )
// }
//
// private var equipmentBoxCode:String? = null
//
// private fun showBindDialog() {
// UserBindRemindDialog(
// activity = requireActivity(),
// content = "您已绑定编号为${equipmentBoxCode}的柜子,请解绑后重试"
// )
// .show()
// }
//
// private fun updateBindState(pair: Pair<Boolean?, ErrorInfo>){
// if (pair.first == null) return // 解绑状态过滤
// val errorInfo = pair.second
// if (errorInfo.isSuccess()) {
// if (errorInfo.equipmentUserInfo == null) {
// ToastUtils.showToast("数据异常")
// return
// }
//// if (errorInfo.equipmentUserInfo.isOtherEquipment()) {
//// MainActivity.start(
//// requireContext(),
//// pageType = PageType.PLATE_TIP,
//// equipmentUserInfo = errorInfo.equipmentUserInfo,
//// isAdmin = true
//// )
//// } else {
// SerialApi.openPlate(
// errorInfo.equipmentUserInfo.equipmentBoxCode!!.toInt(),
// object : SerialPortManager.SendCallback {
// override fun onSuccess() {
// ToastUtils.showToast("绑定成功")
// activity?.finish()
// }
//
// override fun onFail(e: Exception?) {
// ToastUtils.showToast("绑定失败")
// }
// })
//// }
// } else {
// ToastUtils.showToast(errorInfo.msg)
// }
// }
//}
@@ -0,0 +1,40 @@
package com.sw.platecabinet.fragment
import android.os.CountDownTimer
import com.sw.platecabinet.Constants
import com.sw.platecabinet.databinding.FragmentPlateCabinetFullBinding
import timber.log.Timber
/**
* 餐盘柜已满
*/
class PlateCabinetFullFragment :
BaseFragment<FragmentPlateCabinetFullBinding>(FragmentPlateCabinetFullBinding::inflate) {
private var totalTimeInMillis: Long = Constants.AUTO_CLOSE_TIME * 1000
private var countDownTimer: CountDownTimer? = null
override fun initialize() {
Timber.d("initialize")
initCountTime()
}
fun initCountTime() {
countDownTimer = object : CountDownTimer(totalTimeInMillis, 1000) {
override fun onTick(millisUntilFinished: Long) {
_binding?.tvAutoClose?.text = "${(millisUntilFinished / 1000).toInt() + 1}秒后返回主屏"
}
override fun onFinish() {
activity?.finish()
}
}
countDownTimer?.start()
}
override fun onDestroyView() {
countDownTimer?.cancel()
countDownTimer = null
super.onDestroyView()
}
}
@@ -0,0 +1,98 @@
package com.sw.platecabinet.fragment
import android.content.Intent
import android.os.Bundle
import android.os.CountDownTimer
import com.sw.platecabinet.Constants
import com.sw.platecabinet.R
import com.sw.platecabinet.activity.InitActivity
import com.sw.platecabinet.databinding.FragmentPlateOpenBinding
/**
* 餐盘柜提示界面
*/
class PlateOpenFragment :
BaseFragment<FragmentPlateOpenBinding>(FragmentPlateOpenBinding::inflate) {
private var totalTimeInMillis: Long = Constants.AUTO_CLOSE_TIME * 1000
// internal val KEY_ACTION_TYPE = "actionType"
// internal val PARAM_IS_ADMIN = "isAdmin"
// private var info: EquipmentUserInfo? = null
// private var isAdmin: Boolean = false
private var isScanCode = false
private var countDownTimer: CountDownTimer? = null
companion object {
const val IS_SCAN_CODE = "isScanCode"
// @JvmStatic
// fun newInstance(userInfo: EquipmentUserInfo?, isAdmin: Boolean) =
// PlateOpenFragment().apply {
// arguments = Bundle().apply {
//// putParcelable(KEY_ACTION_TYPE, userInfo)
//// putBoolean(PARAM_IS_ADMIN, isAdmin)
// }
// }
@JvmStatic
fun newInstance(isScanCode: Boolean) =
PlateOpenFragment().apply {
arguments = Bundle().apply {
putBoolean(IS_SCAN_CODE, isScanCode)
}
}
}
override fun initialize() {
arguments?.let {
// info = it.getParcelable(KEY_ACTION_TYPE)
// isAdmin = it.getBoolean(PARAM_IS_ADMIN)
// info?.let {
// if (!it.hasEquipmentBox()) {
// binding.ivType.setImageResource(R.drawable.ic_tip_warn)
// binding.tvTitle.text = "暂无餐盘信息"
// binding.tvSubTitle.text = "请联系管理员"
// binding.tvTitle.setTextColor(requireContext().getColor(R.color.tip_title_fail))
// }
//// else if (it.isOtherEquipment()) {
//// binding.ivType.setImageResource(R.drawable.ic_tip_warn)
//// binding.tvTitle.text = "暂无餐盘信息"
//// binding.tvSubTitle.text = "您的餐盘绑定在${it.equipmentName ?: " "}号柜上"
//// binding.tvTitle.setTextColor(requireContext().getColor(R.color.tip_title_fail))
//// }
// else {
// binding.ivType.setImageResource(R.drawable.ic_tip_success)
// binding.tvTitle.text = "柜门开启"
// binding.tvSubTitle.text = "餐盘取出后请关闭柜门"
// binding.tvTitle.setTextColor(requireContext().getColor(R.color.tip_title_success))
// }
// }
isScanCode = it.getBoolean(IS_SCAN_CODE, false)
binding.ivType.setImageResource(R.drawable.ic_tip_success)
binding.tvTitle.text = "柜门开启"
binding.tvSubTitle.text = if (isScanCode) "餐盘放入后请关闭柜门" else "餐盘取出后请关闭柜门"
binding.tvTitle.setTextColor(requireContext().getColor(R.color.tip_title_success))
}
initCountTime()
}
fun initCountTime() {
countDownTimer = object : CountDownTimer(totalTimeInMillis, 1000) {
override fun onTick(millisUntilFinished: Long) {
_binding?.tvAutoClose?.text = ((millisUntilFinished / 1000).toInt() + 1).toString()
}
override fun onFinish() {
val ctx = context ?: return
activity?.finish()
ctx.startActivity(Intent(ctx, InitActivity::class.java))
}
}
countDownTimer?.start()
}
override fun onDestroyView() {
countDownTimer?.cancel()
countDownTimer = null
super.onDestroyView()
}
}
@@ -0,0 +1,241 @@
//package com.sw.platecabinet.fragment
//
//import android.view.View
//import androidx.core.view.isVisible
//import androidx.lifecycle.lifecycleScope
//import androidx.recyclerview.widget.GridLayoutManager
//import androidx.recyclerview.widget.LinearLayoutManager
//import androidx.recyclerview.widget.RecyclerView
//import com.sw.inbound.utils.DateTimeUtils
//import com.sw.plate.utils.ToastUtils
//import com.sw.plate.utils.comn.SerialApi
//import com.sw.plate.utils.comn.SerialPortManager
//import com.sw.platecabinet.GlobalData
//import com.sw.platecabinet.MyApp
//import com.sw.platecabinet.R
//import com.sw.platecabinet.activity.BaseActivity
//import com.sw.platecabinet.activity.MainActivity
//import com.sw.platecabinet.activity.PageType
//import com.sw.platecabinet.adapter.GenericItemAdapter
//import com.sw.platecabinet.adapter.GenericPageAdapter
//import com.sw.platecabinet.adapter.GridSpacingItemDecoration
//import com.sw.platecabinet.adapter.dpToPx
//import com.sw.platecabinet.databinding.FragmentSettingListBinding
//import com.sw.platecabinet.databinding.ItemBindViewBinding
//import com.sw.platecabinet.ext.formatNumber
//import com.sw.platecabinet.ext.maskName
//import com.sw.platecabinet.model.response.EquipmentUserInfo
//import com.sw.platecabinet.utils.ListArrangementUtil
//import com.sw.platecabinet.utils.PlateUtils
//import com.sw.platecabinet.utils.SpTool
//import kotlinx.coroutines.Dispatchers
//import kotlinx.coroutines.launch
//import kotlinx.coroutines.withContext
//import timber.log.Timber
//
///**
// * 餐盘柜绑定的用户列表界面
// */
//class SettingListFragment :
// BaseFragment<FragmentSettingListBinding>(FragmentSettingListBinding::inflate) {
//
// /**
// * 需要横向滚动的adapter
// */
// private var pageAdapter: GenericPageAdapter<EquipmentUserInfo, ItemBindViewBinding>? = null
//
// /**
// * 垂直滚动的adapter
// */
// private var itemAdapter: GenericItemAdapter<EquipmentUserInfo, ItemBindViewBinding>? = null
//
// override fun registerDataChange() {
// super.registerDataChange()
//// lifecycleScope.launch {
//// viewModel.equipmentList.collect { it ->
//// Timber.d("registerDateChange updateAdapter it = $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> {
// 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
// )
// }
// }
//
// fun updateAdapter(items: List<EquipmentUserInfo>) {
// val itemsPerPage = 2 * GlobalData.arrayVertical // 每行2个,每列11个,共22个
// if (GlobalData.arrayCross > 2) {
// lifecycleScope.launch(Dispatchers.Default) {
// // 多recyclerview
// 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()
// if (index == pageList.size - 1) {
// // 补全剩余item
// itemList = (itemList + List(itemsPerPage - itemList.size) {
// EquipmentUserInfo()
// }) as MutableList<EquipmentUserInfo>
// }
// ListArrangementUtil.convertToColumnFirst(
// itemList,
// GlobalData.arrayVertical
// )
// }
// } else {
// val newItems =
// ListArrangementUtil.horizontalSortPageItem(items, GlobalData.arrayCross)
// newItems.chunked(itemsPerPage)
// }
// } else {
// listOf(
// items
// )
// }
// withContext(Dispatchers.Main) {
// pageAdapter?.updatePages(pages)
// }
// }
// } else {
// // 单 recyclerview
// lifecycleScope.launch(Dispatchers.Default) {
// var pages = items
// if (GlobalData.arrayMode == 0) {
// pages = ListArrangementUtil.verticalSortItem(pages)
// }
// withContext(Dispatchers.Main) {
// itemAdapter?.updateData(pages)
// }
// }
// }
// binding.mainRecyclerView.scrollToPosition(0)
// }
//}
@@ -0,0 +1,95 @@
//package com.sw.platecabinet.fragment
//
//import android.app.Activity
//import android.content.Intent
//import android.os.Bundle
//import androidx.activity.result.contract.ActivityResultContracts
//import androidx.lifecycle.lifecycleScope
//import com.sw.inbound.utils.DateTimeUtils
//import com.sw.plate.utils.ToastUtils
//import com.sw.platecabinet.GlobalKey
//import com.sw.platecabinet.activity.UnBindDialogActivity
//import com.sw.platecabinet.databinding.FragmentUnbindPlateBinding
//import com.sw.platecabinet.ext.maskName
//import com.sw.platecabinet.ext.maskPhone
//import com.sw.platecabinet.model.ErrorInfo
//import com.sw.platecabinet.model.response.EquipmentUserInfo
//import kotlinx.coroutines.launch
//
///**
// * 餐盘柜绑定
// */
//class UnBindPlateFragment private constructor() : BaseFragment<FragmentUnbindPlateBinding>(
// FragmentUnbindPlateBinding::inflate
//) {
// internal val ARG_PARAM1 = "param1"
// private var info: EquipmentUserInfo? = null
//
// companion object {
// @JvmStatic
// fun newInstance(param1: EquipmentUserInfo?) =
// UnBindPlateFragment().apply {
// arguments = Bundle().apply {
// putParcelable(ARG_PARAM1, param1)
// }
// }
// }
//
// private val dialogLauncher = registerForActivityResult(
// ActivityResultContracts.StartActivityForResult()
// ) { result ->
// if (result.resultCode == Activity.RESULT_OK) {
// viewModel.unbindPlate(info?.id) { pair ->
// updateBindState(pair)
// }
// }
// }
//
// override fun initialize() {
// arguments?.let { args ->
// info = args.getParcelable(ARG_PARAM1)
// info?.let {
// binding.tvNum.text = "${it.equipmentName}-${it.equipmentBoxCode}"
// binding.tvPlateNumberValue.text = it.plateNumber
// binding.tvNameValue.text = it.name.maskName()
// binding.tvPhoneValue.text = it.phone.maskPhone()
// val timeInfo = DateTimeUtils.parseDateTime(it.updateTime)
// val timeAgo = DateTimeUtils.getTimeAgo(timeInfo)
// binding.tvUpdateTimeValue.text = timeAgo
// }
// }
// binding.llUnbind.setOnClickListener {
// if (info == null) return@setOnClickListener
// val intent = Intent(requireContext(), UnBindDialogActivity::class.java).apply {
// putExtra(GlobalKey.PARAM_EQUIPMENT_INFO, info)
// }
// dialogLauncher.launch(intent)
// requireActivity().overridePendingTransition(0, 0)
// }
// binding.tvBack.setOnClickListener {
// activity?.finish()
// }
// }
//
// override fun registerDataChange() {
// super.registerDataChange()
//// lifecycleScope.launch {
//// viewModel.bindStateChange.collect {
//// updateBindState(it)
//// }
//// }
// }
// private fun updateBindState(pair: Pair<Boolean?, ErrorInfo>){
// if (pair.first == null) return // 绑定状态过滤
// val errorInfo = pair.second
// if (errorInfo.isSuccess()) {
// ToastUtils.showToast("解绑成功")
//// activity?.finish()
// // 关闭宿主 Activity
// requireActivity().finish()
// requireActivity().overridePendingTransition(0, 0)
// } else {
// ToastUtils.showToast(errorInfo.msg)
// }
// }
//}
@@ -0,0 +1,25 @@
package com.sw.platecabinet.model
/**
* 错误信息 code == 200 成功
*/
//data class ErrorInfo(
// val code: String = "00000",
// val msg: String = "",
// val equipmentUserInfo: EquipmentUserInfo? = null
//) {
// fun isSuccess(): Boolean {
// return code == "00000"
// }
//}
data class DeviceConfig(
var arcsoftAppId: String? = null,
var arcsoftSdkKey: String? = null,
var arcsoftActiveKey: String? = null
)
data class CodeMsg(
val code: String? = "",
val msg: String? = ""
)
@@ -0,0 +1,36 @@
package com.sw.platecabinet.model.request
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
/**
*
* 设备与用户餐盘信息绑定解绑请求参数
*/
@Parcelize
data class BindParam(
/**
* 设备Id
*/
val equipmentId: String? = null,
/**
* 设备编号
*/
var equipmentCode: String? = "",
/**
*设备盒子编号
*/
val equipmentBoxCode: String? = null,
/**
*
* 会员Id
*/
val memberId: String? = null,
val faceId: String? = null,
/**
*餐盘编号
*/
val plateNumber: String? = null
) : Parcelable
@@ -0,0 +1,21 @@
package com.sw.platecabinet.model.request
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
/**
* 根据设备ID查询当前用户的餐盘信息,用户信息,设备盒子信息请求参数
*/
@Parcelize
data class EquipmentParam(
/**
* app版本号
*/
var appVersion: String = "",
/**
* 设备ID
*/
@SerializedName("equipmentCode")
var equipmentCode: String = "",
) : Parcelable
@@ -0,0 +1,30 @@
package com.sw.platecabinet.model.request
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
/**
* 登录参数
*/
@Parcelize
data class LoginParam(
/**
* 设备Id
*/
val equipmentId: Int? = null,
/**
* 会员信息
*/
// val memberId: String? = null,
val faceId: String? = null,
/**
* 密码
*/
val password: String? = null,
/**
* 手机号
*/
val phone: String? = null
) : Parcelable
@@ -0,0 +1,36 @@
package com.sw.platecabinet.model.request
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
/**
* (手机号/会员信息/会员卡号)模糊搜索
*/
@Parcelize
data class SearchParam(
// /**
// * app版本号
// */
// var appVersion: String = "",
// /**
// * 设备ID
// */
// @SerializedName("equipmentCode")
// var equipmentCode: String = "",
// @SerializedName("consumptionTime")
// val consumptionTime: List<String?>? = listOf(),
// @SerializedName("createTime")
// val createTime: List<String?>? = listOf(),
// @SerializedName("memberFrom")
// val memberFrom: Int? = 0,
// @SerializedName("pageNum")
var pageNum: Int? = 0,
// @SerializedName("pageSize")
var pageSize: Int? = 0,
// @SerializedName("param")
// val `param`: String? = "",
var name:String?=null,
var phone:String?=null
) : Parcelable
@@ -0,0 +1,12 @@
package com.sw.platecabinet.model.response
data class ApiResponse<T>(
val code: String,
// val success: Boolean? = false,
val msg: String? = "",
val data: T? = null,
// val result: T? = null,
) {
// fun isSuccess(): Boolean = code == 200
fun isSuccess(): Boolean = code == "00000"
}
@@ -0,0 +1,72 @@
//package com.sw.platecabinet.model.response
//
//
//import android.os.Parcelable
//import com.google.gson.annotations.SerializedName
//import kotlinx.parcelize.Parcelize
//
//@Parcelize
//data class EquipmentInfo(
// @SerializedName("appPackageLocalUrl")
// val appPackageLocalUrl: String? = "",
// @SerializedName("appPackageUrl")
// val appPackageUrl: String? = "",
// @SerializedName("arcsoftActiveKey")
// val arcsoftActiveKey: String? = "",
// @SerializedName("arcsoftAppId")
// val arcsoftAppId: String? = "",
// @SerializedName("arcsoftSdkKey")
// val arcsoftSdkKey: String? = "",
// @SerializedName("arrayCross")
// val arrayCross: Int? = 0,
// @SerializedName("arrayMode")
// val arrayMode: String? = "",
// @SerializedName("arrayVertical")
// val arrayVertical: Int? = 0,
// @SerializedName("canteenId")
// val canteenId: String? = "",
// @SerializedName("canteenName")
// val canteenName: String? = "",
// @SerializedName("clientServerIp")
// val clientServerIp: String? = "",
// @SerializedName("createBy")
// val createBy: String? = "",
// @SerializedName("createTime")
// val createTime: String? = "",
// @SerializedName("customerName")
// val customerName: String? = "",
// @SerializedName("equipmentCode")
// val equipmentCode: String? = "",
// @SerializedName("equipmentName")
// val equipmentName: String? = "",
// @SerializedName("equipmentName_dictText")
// val equipmentNameDictText: String? = "",
// @SerializedName("id")
// val id: String? = "",
// @SerializedName("isSync")
// val isSync: Int? = 0,
// @SerializedName("mqName")
// val mqName: String? = "",
// @SerializedName("mqPassword")
// val mqPassword: String? = "",
// @SerializedName("orgCode")
// val orgCode: String? = "",
// @SerializedName("owningTrack")
// val owningTrack: Int? = 0,
// @SerializedName("owningTrackOrder")
// val owningTrackOrder: Int? = 0,
// @SerializedName("screenHtml")
// val screenHtml: String? = "",
// @SerializedName("serviceRequestAddress")
// val serviceRequestAddress: String? = "",
// @SerializedName("status")
// val status: Int? = 0,
// @SerializedName("sysOrgCode")
// val sysOrgCode: String? = "",
// @SerializedName("updateBy")
// val updateBy: String? = "",
// @SerializedName("updateTime")
// val updateTime: String? = "",
// @SerializedName("zhstServerIp")
// val zhstServerIp: String? = ""
//) : Parcelable
@@ -0,0 +1,90 @@
//package com.sw.platecabinet.model.response
//
//import android.os.Parcelable
//import android.text.TextUtils
////import com.google.gson.annotations.SerializedName
//import com.sw.platecabinet.GlobalData
//import kotlinx.parcelize.Parcelize
//
///**
// * 设备绑定用户信息
// */
//@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? = "",
// /**
// *
// * 更新时间
// */
// 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(),
//
//// /**
//// * 用于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()
//// }
//}
@@ -0,0 +1,85 @@
//package com.sw.platecabinet.model.response
//
//
//import android.os.Parcelable
//import com.google.gson.annotations.SerializedName
//import kotlinx.parcelize.Parcelize
//
///**
// * 搜索会员信息结果
// */
//@Parcelize
//data class SearchResult(
// @SerializedName("endRow")
// val endRow: Int? = 0,
// @SerializedName("hasNextPage")
// val hasNextPage: Boolean? = false,
// @SerializedName("hasPreviousPage")
// val hasPreviousPage: Boolean? = false,
// @SerializedName("isFirstPage")
// val isFirstPage: Boolean? = false,
// @SerializedName("isLastPage")
// val isLastPage: Boolean? = false,
// @SerializedName("list")
// val list: List<Member>? = listOf(),
// @SerializedName("navigateFirstPage")
// val navigateFirstPage: Int? = 0,
// @SerializedName("navigateLastPage")
// val navigateLastPage: Int? = 0,
// @SerializedName("navigatePages")
// val navigatePages: Int? = 0,
// @SerializedName("navigatepageNums")
// val navigatepageNums: List<Int?>? = listOf(),
// @SerializedName("nextPage")
// val nextPage: Int? = 0,
// @SerializedName("pageNum")
// val pageNum: Int? = 0,
// @SerializedName("pageSize")
// val pageSize: Int? = 0,
// @SerializedName("pages")
// val pages: Int? = 0,
// @SerializedName("prePage")
// val prePage: Int? = 0,
// @SerializedName("size")
// val size: Int? = 0,
// @SerializedName("startRow")
// val startRow: Int? = 0,
// @SerializedName("total")
// val total: Int? = 0
//) : Parcelable {
// @Parcelize
// data class Member(
//// @SerializedName("cardBalance")
//// val cardBalance: String? = "",
//// @SerializedName("cardCode")
//// val cardCode: String? = "",
//// @SerializedName("consumptionCount")
//// val consumptionCount: Int? = 0,
//// @SerializedName("consumptionTotal")
//// val consumptionTotal: String? = "",
//// @SerializedName("createTime")
//// val createTime: String? = "",
//// @SerializedName("firstTopUpTime")
//// val firstTopUpTime: String? = "",
//// @SerializedName("id")
//// val id: Int? = 0,
//// @SerializedName("integralBalance")
//// val integralBalance: Int? = 0,
//// @SerializedName("lastConsumptionTime")
//// val lastConsumptionTime: String? = "",
//// @SerializedName("memberFrom")
//// val memberFrom: Int? = 0,
//// @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 phone: String? = "",
// ) : Parcelable
//}
@@ -0,0 +1,41 @@
package com.sw.platecabinet.model.response
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* 用户人脸信息
*/
@Parcelize
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 faceFeatureStr: String? = "",
val faceUpdateTimestamp: Long? = null,
/**
* 人脸删除标识
*/
val faceDeleted: Boolean? = false,
/**
* 会员编号
*/
val cardNo: String,
/**
* 是否会员
*/
val member: Boolean
) : Parcelable
@@ -0,0 +1,40 @@
package com.sw.platecabinet.network
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.MyApp
import com.sw.platecabinet.network.api.ApiService
import com.sw.platecabinet.network.interceptor.RequestInterceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import timber.log.Timber
import java.util.concurrent.TimeUnit
object ApiClient {
private const val TIME_OUT = 60L // 超时时间(秒)
private val okHttpClient = OkHttpClient.Builder()
.connectTimeout(TIME_OUT, TimeUnit.SECONDS)
.readTimeout(TIME_OUT, TimeUnit.SECONDS)
.writeTimeout(TIME_OUT, TimeUnit.SECONDS)
.addNetworkInterceptor(HttpLoggingInterceptor { Timber.d("okhttp ==>${it}") }.apply {
level = if (MyApp.DEBUG) HttpLoggingInterceptor.Level.BODY
else HttpLoggingInterceptor.Level.NONE
})
.addInterceptor(RequestInterceptor())
.build()
private val retrofit by lazy {
Retrofit.Builder()
.baseUrl(GlobalData.appBaseUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
val apiService: ApiService by lazy {
retrofit.create(ApiService::class.java)
}
}
@@ -0,0 +1,65 @@
package com.sw.platecabinet.network.api
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.model.DeviceConfig
import com.sw.platecabinet.model.response.ApiResponse
import com.sw.platecabinet.model.response.UserFaceModel
import okhttp3.MultipartBody
import retrofit2.http.Body
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.Url
interface ApiService {
/**
* 获取人脸缓存数据
*/
@POST
suspend fun getUserFaceCache(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/faceFeature/list",
@Body param: Map<String, Int>
): ApiResponse<List<UserFaceModel>>
/**
* 获取人脸增量数据
*/
@POST
suspend fun getFaceIncrementList(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/faceFeature/increment/list",
@Body param: Map<String, Long>
): ApiResponse<List<UserFaceModel>?>
/**
* 获取设备配置数据
*/
@GET
suspend fun getDeviceConfig(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/getYxEquipmentByEquipmentCode"
): ApiResponse<DeviceConfig?>
/**
* 添加人脸数据
*/
@POST
@FormUrlEncoded
suspend fun addUserFace(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/addUserByFace",
@Field("featureChar") faceData: String,
@Field("url") imageUr: String
): ApiResponse<UserFaceModel?>
/**
* 上传图片
*/
@Multipart
@POST
suspend fun uploadImage(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/upload",
@Part file: MultipartBody.Part,
): ApiResponse<String?>
}
@@ -0,0 +1,36 @@
package com.sw.platecabinet.network.interceptor
import com.sw.inbound.utils.SPUtil
import com.sw.plate.App
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.GlobalKey
import okhttp3.Interceptor
import okhttp3.Response
/**
* 请求拦截器
*/
class RequestInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val requestBuilder = originalRequest.newBuilder()
.header("Content-Type", "application/json")
.header("Accept", "application/json")
// .header("Authorization", "Bearer ${getToken()}")
// .header("Authorization", getToken())
.header("X-Access-Token", "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjYW50ZWVuSWQiOiJiZTE1NDgzMS0zNDY2LTNiYTItYTJlYS01NzY1MmM5MTlmZWQiLCJ0eXBlIjoiNCIsInVzZXJJZCI6IjAwMDAwMDAwMDAwMDAwMDAwMDEifQ.sN40cOC-O5WQFrF4IDUs8fFlkNdUKLbJt_rHyTsgYYM")
.header("X-DEVICE-CODE", GlobalData.deviceId)
.header("authorization", "57ee87183f2a4fa59683ec9ef41c8f5d")
val newRequest = requestBuilder.build()
return chain.proceed(newRequest)
}
private fun getToken(): String {
// 从本地获取token的逻辑
val spUtil = SPUtil.getInstance(context = App.getContext())
return spUtil.get(GlobalKey.KEY_TOKEN, "") as String
// return "eyJhbGciOiJIUzUxMiJ9.eyJpZCI6MTQ2LCJ1c2VyTmFtZSI6IjEzNjgxNDQ4ODU2IiwibmFtZSI6IuW-kOejiiIsInBhc3N3b3JkIjoiOTllOTQ1ZmVjZmZjNWIzNDI4MmUwNDRlODYyMzdjM2UxZjU5OWY5OCIsInNhbHQiOiI0NmEzMzUzYWU4OTA0MDYxYjMzODU5ZWNlYTBlMGE2NyIsInBob25lIjoiMTM2ODE0NDg4NTYiLCJzdGF0dXMiOjEsInVzZXJUeXBlIjoyLCJjcmVhdGVVc2VyTm8iOiIxNDEiLCJjcmVhdGVUaW1lIjoxNjI2OTQzNDE2MDAwLCJ1cGRhdGVVc2VyTm8iOiIxNDEiLCJ1cGRhdGVUaW1lIjoxNjI2OTQzNDE2MDAwLCJpc0RlbCI6ZmFsc2UsImVhSWQiOjk5LCJlYUlkTGlzdCI6Ijk5IiwiaXNTaG9wTWFuYWdlciI6dHJ1ZSwidXNlck5vIjoiMWY5Nzk5ZWMtODlkYi00MWYyLTk1YTEtY2UzNTA3Y2QyMTU2In0.f7wImPgBOYMV0AqRchnXGPkUWZN9dFJ9gLPsaB8uNldd21IfXLjJl8y-FiWVuVUvlwUvGpgqGDFR1JKj5H7amw"
}
}
@@ -0,0 +1,104 @@
//package com.sw.platecabinet.network.task
//
//import android.content.Context
//import android.util.Log
//import androidx.work.CoroutineWorker
//import androidx.work.WorkerParameters
//import com.arcsoft.face.FaceEngine
//import com.sw.inbound.utils.GsonUtils
//import com.sw.plate.App
//import com.sw.plate.utils.Base64
//import com.sw.plate.utils.PrefUtils
//import com.sw.plate.utils.arcface.FaceApi
//import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
//import com.sw.plate.utils.arcface.faceserver.FaceServer
//import com.sw.platecabinet.network.ApiClient
//import com.sw.platecabinet.repository.RemoteRepository
//import kotlinx.coroutines.Dispatchers
//import kotlinx.coroutines.withContext
//import org.greenrobot.eventbus.EventBus
//import timber.log.Timber
//
//class HeartBeatTask(appContext: Context, workerParams: WorkerParameters) :
// CoroutineWorker(appContext, workerParams) {
//
// companion object {
// private const val TAG = "HeartBeatTask"
// }
//
// override suspend fun doWork(): Result {
// return try {
// // 执行后台任务逻辑
// performSync()
//
// TaskManager.startTask()
//
// Result.success()
// } catch (e: Exception) {
// e.printStackTrace()
// Result.retry()
// }
// }
//
// private val repository: RemoteRepository by lazy {
// RemoteRepository(ApiClient.apiService)
// }
//
// private val faceApi: FaceApi = FaceApi()
//
// private suspend fun performSync() {
//// withContext(Dispatchers.IO) {
//// val faceUpdateTimeStamp = PrefUtils.getLong(App.getContext(), "faceUpdateTimeStamp", 0)
//// if (faceUpdateTimeStamp == 0L) {
//// val timestamp = System.currentTimeMillis()
//// PrefUtils.setLong(App.getContext(), "faceUpdateTimeStamp", timestamp)
//// }
//// val resp = repository.getHeartbeatInterface(
//// PrefUtils.getLong(
//// App.getContext(),
//// "faceUpdateTimeStamp",
//// 0
//// )
//// )
//// Timber.tag(TAG).d("performSync: ${GsonUtils.toJson(resp)}")
//// val list = resp.data
//// if (list.isNullOrEmpty()) {
//// return@withContext
//// }
//// val faceServer = FaceServer.getInstance()
//// val faceEngine: FaceEngine? = faceServer.faceEngine
////
//// list.forEachIndexed { index, it ->
//// try {
//// if (index == list.size - 1) {
//// PrefUtils.setLong(
//// App.getContext(),
//// "faceUpdateTimeStamp",
//// it.updateTimeStamp
//// )
//// }
//// if (it.faceType == "1") {
//// if (it.userId.isNullOrBlank().not()) {
//// //userId非空,根据userId删除人脸数据
//// faceApi.deleteByUserName(it.userId)
//// faceEngine?.removeFaceFeature(it.userId.toInt())
//// }
//// } else if (it.faceType == "0") {
//// //新增人脸数据
//// val featureData = Base64.decode(it.faceFeature)
//// val insertEntity = FaceEntity(it.userId, null, featureData)
//// val faceId = faceApi.insert(insertEntity)
//// insertEntity.faceId = faceId
////// faceServer.registerFaceFeatureInfoFromDb(insertEntity, faceEngine)
////
//// EventBus.getDefault().post(insertEntity)
////// callback(insertEntity)
//// }
//// } catch (e: Exception) {
//// e.printStackTrace()
//// }
//// }
//// }
// }
//
//}
@@ -0,0 +1,40 @@
//package com.sw.platecabinet.network.task
//
//import androidx.lifecycle.LifecycleOwner
//import androidx.work.ExistingWorkPolicy
//import androidx.work.OneTimeWorkRequestBuilder
//import androidx.work.WorkInfo
//import androidx.work.WorkManager
//import com.sw.plate.App
//import java.util.concurrent.TimeUnit
//
//object TaskManager {
// private const val TASK_NAME = "SmartPlateCabinetTask"
//// private var owner: LifecycleOwner?=null
//// fun setOwner(owner: LifecycleOwner) {
//// this.owner = owner
//// }
// fun startTask() {
// val nextRequest = OneTimeWorkRequestBuilder<HeartBeatTask>()
// .setInitialDelay(1, TimeUnit.MINUTES)
//// .setInputData(inputData)
// .build()
// WorkManager.getInstance(App.getContext())
//// .apply{
//// getWorkInfoByIdLiveData(nextRequest.id)
//// .observe(owner) { workInfo ->
//// if (workInfo?.state == WorkInfo.State.SUCCEEDED) {
//// val result = workInfo.outputData.getString("RESULT_KEY")
//// // 使用返回的结果
//// }
//// }
//// }
// .enqueueUniqueWork(TASK_NAME, ExistingWorkPolicy.REPLACE, nextRequest)
// }
//
// fun cancelTask() {
// WorkManager.getInstance(App.getContext())
// .cancelUniqueWork(TASK_NAME)
// }
//
//}
@@ -0,0 +1,21 @@
package com.sw.platecabinet.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.sw.platecabinet.activity.DeviceInitActivity
import com.sw.platecabinet.activity.LoginByFaceActivity
import timber.log.Timber
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_BOOT_COMPLETED == intent.action) {
Timber.d("设备启动完成,开始执行自启动逻辑")
// val intent = Intent(context, LoginByFaceActivity::class.java)
val intent = Intent(context, DeviceInitActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
}
}
@@ -0,0 +1,58 @@
package com.sw.platecabinet.repository
import com.google.gson.JsonParseException
import com.sw.inbound.utils.GsonUtils
import com.sw.platecabinet.model.CodeMsg
import com.sw.platecabinet.model.response.ApiResponse
import retrofit2.HttpException
import timber.log.Timber
import java.io.IOException
import java.net.ConnectException
import java.net.SocketTimeoutException
import javax.net.ssl.SSLHandshakeException
abstract class BaseRepository {
suspend fun <T> safeApiCall(apiCall: suspend () -> ApiResponse<T>): ApiResponse<T> {
return try {
apiCall()
} catch (e: Exception) {
Timber.e("safeApiCall Exception: ${e.stackTraceToString()}")
when (e) {
is HttpException -> {
val jsonData = e.response()?.errorBody()?.string()
val codeMsg = GsonUtils.fromJson(jsonData, CodeMsg::class.java)
if (codeMsg?.msg.isNullOrBlank()) {
ApiResponse(code = "" + e.code(), msg = e.message())
} else {
ApiResponse(code = codeMsg?.code?:"", msg = codeMsg?.msg?:"")
}
}
is SocketTimeoutException -> {
ApiResponse(code = "-2", msg = "请求超时: ${e.message}")
}
is ConnectException -> {
ApiResponse(code = "-3", msg = "连接失败: ${e.message}")
}
is SSLHandshakeException -> {
ApiResponse(code = "-4", msg = "SSL握手失败: ${e.message}")
}
is JsonParseException -> {
ApiResponse(code = "-5", msg = "JSON解析错误: ${e.message}")
}
is IOException -> {
ApiResponse(code = "-6", msg = "网络IO错误: ${e.message}")
}
else -> {
ApiResponse(code = "-1", msg = "未知错误: ${e.message ?: "无错误信息"}")
}
}
}
}
}
@@ -0,0 +1,69 @@
package com.sw.platecabinet.repository
import com.sw.platecabinet.model.DeviceConfig
import com.sw.platecabinet.model.response.ApiResponse
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.network.api.ApiService
import com.sw.platecabinet.utils.FileUtils
import java.io.File
/**
* 远程数据处理
*/
class RemoteRepository constructor(
private val apiService: ApiService
) : BaseRepository() {
/**
* 获取人脸数据
*/
suspend fun getUserFaceCache(
pageNum: Int,
pageSize: Int = 100,
): ApiResponse<List<UserFaceModel>> {
return safeApiCall {
apiService.getUserFaceCache(
param = mapOf(
"pageNum" to pageNum,
"pageSize" to pageSize
)
)
}
}
suspend fun getFaceIncrementList(
pageNum: Long,
pageSize: Long = 100L,
timestamp: Long
): ApiResponse<List<UserFaceModel>?> {
return safeApiCall {
apiService.getFaceIncrementList(
param = mapOf(
"pageNum" to pageNum,
"pageSize" to pageSize,
"timestamp" to timestamp
)
)
}
}
suspend fun getDeviceConfig(): ApiResponse<DeviceConfig?> {
return safeApiCall { apiService.getDeviceConfig() }
}
suspend fun addUserFace(faceData: String, imageUr: String): ApiResponse<UserFaceModel?> {
return safeApiCall { apiService.addUserFace(faceData = faceData, imageUr = imageUr) }
}
suspend fun uploadImage(file: File): ApiResponse<String?> {
return safeApiCall {
val part = FileUtils.genRequestPart(file)
if (part == null) {
ApiResponse(code = "-1", msg = "解析图片失败")
} else {
apiService.uploadImage(file = part)
}
}
}
}
@@ -0,0 +1,300 @@
package com.sw.platecabinet.socket;
import android.annotation.SuppressLint;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 局域网多客户端通信管理器:支持并发、心跳、认证、广播、点对点发送
* 可用于 Android 和 JVM 程序。
*/
public class LanCommunicationManager {
// ============ 监听配置 ============
private final int port;
private final long HEARTBEAT_TIMEOUT_MS;
private final int MAX_CLIENT_THREADS;
// ============ 状态 ============
private volatile boolean running = false;
private ServerSocket serverSocket;
// ============ 线程池 ============
private final ExecutorService acceptExecutor = Executors.newSingleThreadExecutor();
private final ExecutorService clientExecutor;
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
// ============ 客户端会话 ============
private final ConcurrentHashMap<String, ClientSession> clients = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Socket, ClientSession> unAuthSessions = new ConcurrentHashMap<>();
// ============ 回调接口 ============
public interface Listener {
// 新客户端完成 AUTH / 认证
void onClientConnected(String clientId);
// 客户端断开
void onClientDisconnected(String clientId);
// 收到业务消息(type != heartbeat/auth)
void onMessageReceived(String clientId, JSONObject message);
}
private Listener listener;
// ============ 构造 ============
public LanCommunicationManager(int port, int maxClientThreads, long heartbeatTimeoutMs) {
this.port = port;
this.MAX_CLIENT_THREADS = maxClientThreads;
this.HEARTBEAT_TIMEOUT_MS = heartbeatTimeoutMs;
this.clientExecutor = Executors.newFixedThreadPool(Math.max(2, maxClientThreads));
}
public void setListener(Listener listener) {
this.listener = listener;
}
// ============ 启动服务端 ============
@SuppressLint("DiscouragedApi")
public void start() throws IOException {
if (running) return;
running = true;
serverSocket = new ServerSocket(port);
serverSocket.setSoTimeout(2000);
acceptExecutor.execute(this::acceptLoop);
scheduler.scheduleAtFixedRate(this::heartbeatCheck,
HEARTBEAT_TIMEOUT_MS,
HEARTBEAT_TIMEOUT_MS,
TimeUnit.MILLISECONDS);
System.out.println("LanCommunicationManager started on port " + port);
}
private void acceptLoop() {
while (running) {
try {
Socket socket = serverSocket.accept();
socket.setSoTimeout((int) HEARTBEAT_TIMEOUT_MS * 2);
ClientSession session = new ClientSession(socket);
unAuthSessions.put(socket, session);
clientExecutor.execute(() -> clientReadLoop(session));
} catch (SocketTimeoutException ignore) {
} catch (Exception e) {
if (running) e.printStackTrace();
}
}
}
// ============ 处理客户端数据读取 ============
private void clientReadLoop(ClientSession session) {
Socket socket = session.socket;
try (DataInputStream in = new DataInputStream(socket.getInputStream())) {
while (running && !socket.isClosed()) {
int len;
try {
len = in.readInt();
} catch (SocketTimeoutException ste) {
continue;
}
if (len <= 0 || len > 10 * 1024 * 1024) break;
byte[] buf = new byte[len];
in.readFully(buf);
session.updateLastSeen();
JSONObject msg = new JSONObject(new String(buf));
handleMessage(session, msg);
}
} catch (Exception ignored) {
} finally {
closeSession(session);
}
}
private void handleMessage(ClientSession session, JSONObject msg) {
String type = msg.optString("type", "");
switch (type) {
case "auth":
handleAuth(session, msg);
break;
case "heartbeat":
session.updateLastSeen();
break;
default:
if (listener != null && session.clientId != null) {
listener.onMessageReceived(session.clientId, msg);
}
break;
}
}
private void handleAuth(ClientSession session, JSONObject msg) {
String clientId = msg.optString("clientId", null);
if (clientId == null) return;
session.clientId = clientId;
// 移动到已认证 map
unAuthSessions.remove(session.socket);
clients.put(clientId, session);
if (listener != null) listener.onClientConnected(clientId);
sendToSession(session, ack("auth_ok"));
}
// ============ 心跳超时 ============
private void heartbeatCheck() {
long now = System.currentTimeMillis();
for (Map.Entry<String, ClientSession> e : clients.entrySet()) {
ClientSession s = e.getValue();
if (now - s.lastSeen > HEARTBEAT_TIMEOUT_MS) {
closeSession(s);
}
}
for (ClientSession s : unAuthSessions.values()) {
if (now - s.lastSeen > HEARTBEAT_TIMEOUT_MS * 2) {
closeSession(s);
}
}
}
// ============ 发送 ============
public boolean sendToClient(String clientId, JSONObject json) {
ClientSession s = clients.get(clientId);
return s != null && sendToSession(s, json);
}
public void broadcast(JSONObject json) {
for (ClientSession s : clients.values()) {
sendToSession(s, json);
}
}
private boolean sendToSession(ClientSession s, JSONObject json) {
try {
DataOutputStream out = s.out;
synchronized (out) {
byte[] data = json.toString().getBytes();
out.writeInt(data.length);
out.write(data);
out.flush();
}
return true;
} catch (Exception e) {
closeSession(s);
return false;
}
}
// ============ ACK ============
private JSONObject ack(String type) {
JSONObject j = new JSONObject();
try {
j.put("type", "ack");
j.put("ack", type);
} catch (JSONException e) {
throw new RuntimeException(e);
}
return j;
}
// ============ 停止 ============
public void stop() {
running = false;
try {
serverSocket.close();
} catch (Exception ignored) {
}
for (ClientSession s : clients.values()) closeSession(s);
for (ClientSession s : unAuthSessions.values()) closeSession(s);
acceptExecutor.shutdownNow();
clientExecutor.shutdownNow();
scheduler.shutdownNow();
System.out.println("LanCommunicationManager stopped");
}
// ============ 会话类 ============
public static class ClientSession {
public final Socket socket;
public final DataOutputStream out;
public volatile long lastSeen = System.currentTimeMillis();
public volatile String clientId;
public ClientSession(Socket socket) throws IOException {
this.socket = socket;
this.out = new DataOutputStream(socket.getOutputStream());
}
public void updateLastSeen() {
lastSeen = System.currentTimeMillis();
}
}
public void closeSession(ClientSession session) {
if (session == null) return;
try {
Socket socket = session.socket;
// 1. 从已认证表移除
if (session.clientId != null) {
ClientSession removed = clients.remove(session.clientId);
if (removed != null && listener != null) {
listener.onClientDisconnected(session.clientId);
}
}
// 2. 从未认证表移除
unAuthSessions.remove(socket);
// 3. 关闭输出流
try {
session.out.close();
} catch (Exception ignored) {
}
// 4. 关闭 socket
try {
if (!socket.isClosed()) socket.close();
} catch (Exception ignored) {
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
@@ -0,0 +1,35 @@
package com.sw.platecabinet.socket;
public class LanServer {
public static final int TYPE_ADD_FACE_DATA = 1;
public static final int TYPE_CLEAR_FACE_DATA = 2;
private static volatile LanCommunicationManager instance;
public static LanCommunicationManager getInstance() {
if (instance == null) {
synchronized (LanServer.class) {
if (instance == null) {
instance = new LanCommunicationManager(
5000, // 监听端口
20, // 最大客户端数
10_000 // 心跳超时时间 10 秒
);
}
}
}
return instance;
}
void a() {
//HashMap<String, Object> data = new HashMap<>();
//data.put("type", LanServer.TYPE_ADD_FACE_DATA);
//data.put("faceId", tempUserId);//收到采集信息,每次生成新的ID
//data.put("faceFeature", faceFeatureString);
//lanServer.broadcast(new JSONObject(data));
//
//HashMap<String, Object> data = new HashMap<>();
//data.put("type", LanServer.TYPE_CLEAR_FACE_DATA);
//lanServer.broadcast(new JSONObject(data));
}
}
@@ -0,0 +1,21 @@
package com.sw.platecabinet.socket
import android.util.Log
import org.json.JSONObject
open class LanServerListenerImpl: LanCommunicationManager.Listener {
companion object {
private const val TAG = "LanServerListenerImpl"
}
override fun onClientConnected(clientId: String?) {
Log.d(TAG, "addSocketListener,onClientConnected: clientId = $clientId")
}
override fun onClientDisconnected(clientId: String?) {
Log.d(TAG, "addSocketListener,onClientDisconnected: clientId = $clientId")
}
override fun onMessageReceived(clientId: String?, message: JSONObject?) {
Log.d(TAG, "addSocketListener,onMessageReceived: clientId = $clientIdmessage = $message")
}
}
@@ -0,0 +1,330 @@
package com.sw.platecabinet.socket;
import android.util.Log;
import org.json.JSONObject;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* TcpClient - Android ready
* <p>
* Features:
* - length-prefix protocol (int length + bytes)
* - separate read thread and write queue & writer thread
* - auto-reconnect with exponential backoff
* - heartbeat scheduler
* - send queue with optional callback for send result
* - auto send "auth" JSON after connection
*/
public class TcpClient {
private static final String TAG = "TcpClient";
// configuration
private final String serverIp;
private final int serverPort;
private final String clientId; // will be sent in auth message
private final int connectTimeoutMs;
private final long heartbeatIntervalMs;
private final long heartbeatTimeoutMs;
// socket + streams
private Socket socket;
private DataOutputStream out;
private DataInputStream in;
// threads & executors
private final ExecutorService writerExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "TcpClient-Writer"));
private final ExecutorService readerExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "TcpClient-Reader"));
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "TcpClient-Scheduler"));
private final ExecutorService connectExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "TcpClient-Connect"));
// send queue
private final BlockingQueue<JSONObject> sendQueue = new LinkedBlockingQueue<>();
// state
private final AtomicBoolean running = new AtomicBoolean(false);
private final AtomicBoolean connected = new AtomicBoolean(false);
private final AtomicBoolean authSent = new AtomicBoolean(false);
// reconnection/backoff
private final long baseReconnectDelayMs = 1000; // 1s
private final long maxReconnectDelayMs = 30_000; // 30s
private final AtomicInteger reconnectAttempt = new AtomicInteger(0);
// heartbeat task future
private ScheduledFuture<?> heartbeatFuture;
// listener
public interface Listener {
void onConnected();
void onDisconnected(Exception e);
void onMessage(JSONObject json);
void onSendSuccess(JSONObject json);
void onSendFailed(JSONObject json, Exception e);
}
private Listener listener;
public void setListener(Listener l) {
this.listener = l;
}
// ctor
public TcpClient(String serverIp, int serverPort, String clientId,
int connectTimeoutMs, long heartbeatIntervalMs, long heartbeatTimeoutMs) {
this.serverIp = serverIp;
this.serverPort = serverPort;
this.clientId = clientId;
this.connectTimeoutMs = connectTimeoutMs;
this.heartbeatIntervalMs = heartbeatIntervalMs;
this.heartbeatTimeoutMs = heartbeatTimeoutMs;
}
// start client (will attempt connect)
public void start() {
if (running.getAndSet(true)) return;
scheduleConnect(0);
// writer thread drains sendQueue
writerExecutor.execute(this::writerLoop);
}
// stop client and cleanup
public void stop() {
running.set(false);
cancelHeartbeat();
closeSocketQuiet();
writerExecutor.shutdownNow();
readerExecutor.shutdownNow();
scheduler.shutdownNow();
connectExecutor.shutdownNow();
sendQueue.clear();
}
// send JSON (queued). Non-blocking.
public void send(JSONObject json) {
if (!running.get()) return;
sendQueue.offer(json);
}
// AUTH shortcut (immediately send auth JSON)
private void sendAuth() {
try {
JSONObject auth = new JSONObject();
auth.put("type", "auth");
auth.put("clientId", clientId);
sendQueue.offer(auth);
authSent.set(true);
} catch (Exception ignored) {
}
}
// writer thread loop (serializes sends)
private void writerLoop() {
while (running.get()) {
try {
JSONObject json = sendQueue.take(); // blocks
if (connected.get() && out != null) {
try {
byte[] data = json.toString().getBytes();
synchronized (out) {
out.writeInt(data.length);
out.write(data);
out.flush();
}
if (listener != null) listener.onSendSuccess(json);
} catch (Exception e) {
if (listener != null) listener.onSendFailed(json, e);
// on write failure, attempt reconnect
safeCloseAndScheduleReconnect(e);
}
} else {
// not connected: requeue it and wait for connection
sendQueue.offer(json);
Thread.sleep(500); // avoid busy loop
}
} catch (InterruptedException ignored) {
break;
}
}
}
// reader loop (runs in readerExecutor)
private void startReaderLoop() {
readerExecutor.execute(() -> {
try {
while (running.get() && connected.get() && in != null) {
int length;
try {
length = in.readInt(); // will throw SocketTimeoutException if set
} catch (SocketTimeoutException ste) {
// used to detect socket liveness; continue loop
continue;
}
if (length <= 0 || length > 10 * 1024 * 1024) {
// invalid length, break
throw new RuntimeException("Invalid message length: " + length);
}
byte[] buf = new byte[length];
in.readFully(buf);
String s = new String(buf);
try {
JSONObject json = new JSONObject(s);
// update last seen time via heartbeat ack if needed
if ("heartbeat".equals(json.optString("type"))) {
// optionally respond or update time
} else if ("auth_ok".equals(json.optString("type")) || "ack".equals(json.optString("type"))) {
// ignore or process ack
} else {
if (listener != null) listener.onMessage(json);
}
} catch (Exception je) {
Log.w(TAG, "Invalid JSON from server: " + s, je);
}
}
} catch (Exception e) {
if (running.get()) {
safeCloseAndScheduleReconnect(e);
}
}
});
}
// schedule connect attempt with delay (ms)
private void scheduleConnect(long delayMs) {
connectExecutor.execute(() -> {
try {
if (delayMs > 0) Thread.sleep(delayMs);
} catch (InterruptedException ignored) {
}
if (!running.get()) return;
tryConnect();
});
}
// connect logic
private void tryConnect() {
if (!running.get()) return;
closeSocketQuiet(); // ensure closed
try {
Socket s = new Socket();
s.connect(new InetSocketAddress(serverIp, serverPort), connectTimeoutMs);
s.setSoTimeout((int) Math.max(heartbeatTimeoutMs, 5_000));
socket = s;
out = new DataOutputStream(socket.getOutputStream());
in = new DataInputStream(socket.getInputStream());
connected.set(true);
reconnectAttempt.set(0);
authSent.set(false);
// start reader
startReaderLoop();
// send auth immediately
sendAuth();
// start heartbeat
startHeartbeat();
if (listener != null) listener.onConnected();
Log.i(TAG, "Connected to " + serverIp + ":" + serverPort);
} catch (Exception e) {
Log.w(TAG, "Connect failed: " + e.getMessage());
scheduleReconnectWithBackoff();
}
}
// start heartbeat scheduler
private void startHeartbeat() {
cancelHeartbeat();
heartbeatFuture = scheduler.scheduleAtFixedRate(() -> {
if (!running.get() || !connected.get()) return;
try {
JSONObject hb = new JSONObject();
hb.put("type", "heartbeat");
hb.put("time", System.currentTimeMillis());
sendQueue.offer(hb);
} catch (Exception ignored) {
}
}, 0, heartbeatIntervalMs, TimeUnit.MILLISECONDS);
}
private void cancelHeartbeat() {
if (heartbeatFuture != null && !heartbeatFuture.isCancelled()) {
heartbeatFuture.cancel(true);
heartbeatFuture = null;
}
}
// close socket quietly and notify listener
private void safeCloseAndScheduleReconnect(Exception cause) {
closeSocketQuiet();
if (listener != null) listener.onDisconnected(cause);
scheduleReconnectWithBackoff();
}
private void scheduleReconnectWithBackoff() {
int attempt = reconnectAttempt.incrementAndGet();
long delay = Math.min(maxReconnectDelayMsFromAttempt(attempt), maxReconnectDelayMs);
Log.i(TAG, "Scheduling reconnect attempt " + attempt + " after " + delay + "ms");
scheduleConnect(delay);
}
// compute exponential backoff
private long maxReconnectDelayMsFromAttempt(int attempt) {
long d = baseReconnectDelayMs * (1L << Math.min(attempt, 30));
if (d < 0) d = maxReconnectDelayMs;
return Math.min(d, maxReconnectDelayMs);
}
// close socket and streams
private void closeSocketQuiet() {
connected.set(false);
cancelHeartbeat();
try {
if (out != null) {
out.close();
}
} catch (Exception ignored) {
}
try {
if (in != null) {
in.close();
}
} catch (Exception ignored) {
}
try {
if (socket != null && !socket.isClosed()) {
socket.close();
}
} catch (Exception ignored) {
}
out = null;
in = null;
socket = null;
}
// helper: when manually call reconnect (immediately)
public void reconnectNow() {
scheduleConnect(0);
}
// helper to set immediate send of a JSON and wait (blocking) until it is queued (not until delivered)
public boolean sendBlocking(JSONObject json, long timeoutMs) throws InterruptedException {
return sendQueue.offer(json, timeoutMs, TimeUnit.MILLISECONDS);
}
}
@@ -0,0 +1,33 @@
package com.sw.platecabinet.socket
import android.util.Log
import com.sw.platecabinet.socket.TcpClient.*
import org.json.JSONObject
import java.lang.Exception
open class TcpClientListenerImpl : Listener {
companion object {
private const val TAG = "TcpClientListenerImpl"
}
override fun onConnected() {
Log.d(TAG, "addSocketListener,onConnected: ")
}
override fun onDisconnected(e: Exception?) {
Log.d(TAG, "addSocketListener,onDisconnected: ${e?.toString()}")
}
override fun onMessage(json: JSONObject?) {
Log.d(TAG, "addSocketListener,onMessage: $json")
}
override fun onSendSuccess(json: JSONObject?) {
Log.d(TAG, "addSocketListener,onSendSuccess: $json")
}
override fun onSendFailed(json: JSONObject?, e: Exception?) {
Log.d(TAG, "addSocketListener,onSendFailed: $json,e:${e?.toString()}")
}
}
@@ -0,0 +1,47 @@
package com.sw.platecabinet.socket;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
/**
* Simple UDP discovery client:
* Sends "DISCOVER_SERVER" broadcast and waits for first reply "SERVER_FOUND:serverName:ip"
*/
public class UdpDiscoveryClient {
public interface Listener {
void onFound(String ip, String serverName);
void onError(Exception e);
}
public void discover(int discoveryPort, int timeoutMs, Listener listener) {
new Thread(() -> {
try (DatagramSocket socket = new DatagramSocket()) {
socket.setBroadcast(true);
byte[] data = "DISCOVER_SERVER".getBytes();
DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getByName("255.255.255.255"), discoveryPort);
socket.send(packet);
socket.setSoTimeout(timeoutMs);
byte[] buf = new byte[512];
DatagramPacket resp = new DatagramPacket(buf, buf.length);
socket.receive(resp);
String msg = new String(resp.getData(), 0, resp.getLength());
if (msg.startsWith("SERVER_FOUND")) {
// format: SERVER_FOUND:serverName:ip
String[] parts = msg.split(":", 3);
if (parts.length >= 3) {
listener.onFound(parts[2], parts[1]);
return;
}
}
listener.onError(new Exception("Invalid response"));
} catch (Exception e) {
listener.onError(e);
}
}).start();
}
}
@@ -0,0 +1,113 @@
package com.sw.platecabinet.socket;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
public class test {
LanCommunicationManager manager = new LanCommunicationManager(
9999, // 端口
20, // 最大客户端线程
30_000 // 心跳超时 30 秒
);
public void test() {
manager.setListener(new LanCommunicationManager.Listener() {
@Override
public void onClientConnected(String clientId) {
System.out.println("新的客户端上线:" + clientId);
}
@Override
public void onClientDisconnected(String clientId) {
System.out.println("客户端离线:" + clientId);
}
@Override
public void onMessageReceived(String clientId, JSONObject msg) {
System.out.println("收到 " + clientId + " 的消息:" + msg);
}
});
// 启动
try {
manager.start();
} catch (IOException e) {
throw new RuntimeException(e);
}
// 给某个客户端发送
JSONObject j = new JSONObject();
try {
j.put("type", "cmd");
j.put("content", "hello");
} catch (JSONException e) {
throw new RuntimeException(e);
}
manager.sendToClient("device123", j);
// 广播
manager.broadcast(j);
}
public void clientTets() {
// 1) discover server (optional)
UdpDiscoveryClient disc = new UdpDiscoveryClient();
disc.discover(9876, 3000, new UdpDiscoveryClient.Listener() {
@Override
public void onFound(String ip, String serverName) {
startClient(ip);
}
@Override
public void onError(Exception e) { /* fallback to manual IP */ }
});
}
// 2) start client
private TcpClient client;
private void startClient(String serverIp) {
client = new TcpClient(
serverIp,
9999,
"device123", // clientId
5000, // connectTimeoutMs
10_000, // heartbeatIntervalMs
30_000 // heartbeatTimeoutMs
);
client.setListener(new TcpClient.Listener() {
@Override
public void onConnected() {
Log.i("APP", "connected");
}
@Override
public void onDisconnected(Exception e) {
Log.i("APP", "disconnected", e);
}
@Override
public void onMessage(JSONObject json) {
Log.i("APP", "msg:" + json);
}
@Override
public void onSendSuccess(JSONObject json) {
}
@Override
public void onSendFailed(JSONObject json, Exception e) {
}
});
client.start();
}
}
@@ -0,0 +1,59 @@
package com.sw.platecabinet.utils
import android.content.Context
import android.graphics.Bitmap
import android.os.Environment
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import kotlin.io.use
object BitmapSaver {
// 保存到公共目录(需WRITE_EXTERNAL_STORAGE权限)
fun saveToPublicDirectory(
bitmap: Bitmap,
folderName: String = Environment.DIRECTORY_PICTURES,
fileName: String,
format: Bitmap.CompressFormat = Bitmap.CompressFormat.PNG,
quality: Int = 100
): File? {
val dir = Environment.getExternalStoragePublicDirectory(folderName)
if (!dir.exists()) dir.mkdirs()
return saveBitmap(bitmap, File(dir, fileName), format, quality)
}
// 保存到应用私有目录(无需权限)
fun saveToAppFilesDir(
bitmap: Bitmap,
context: Context,
fileName: String,
format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
quality: Int = 100
): File? {
//val dir = context.getExternalFilesDir(null)
val dir = context.cacheDir
val cropFile = File(dir, "crop")
if (cropFile.exists().not()) {
cropFile.mkdirs()
}
return saveBitmap(bitmap, File(cropFile, fileName), format, quality)
}
private fun saveBitmap(
bitmap: Bitmap,
outputFile: File,
format: Bitmap.CompressFormat,
quality: Int
): File? {
return try {
FileOutputStream(outputFile).use { fos ->
bitmap.compress(format, quality, fos)
fos.flush()
}
outputFile
} catch (e: IOException) {
e.printStackTrace()
null
}
}
}
@@ -0,0 +1,79 @@
package com.sw.platecabinet.utils
import android.util.Log
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import java.util.concurrent.atomic.AtomicBoolean
/**
* 基于协程的倒计时工具类(实例化使用,各调用方持有独立实例,互不干扰)
* 修复:onCompletion 区分正常结束与取消,手动取消时不触发 onFinish
*/
class CountDownUtil {
private val isCounting = AtomicBoolean(false)
private var currentJob: Job? = null
/**
* 启动倒计时(若已在运行则忽略,需重启请先调用 cancelCountDown
* @param total 总秒数
* @param lifecycleScope Activity/Fragment 的 lifecycleScope
* @param onStart 启动回调(主线程)
* @param onTick 每秒回调剩余秒数(主线程)
* @param onFinish 正常结束回调(主线程,手动取消时不触发)
*/
fun startCountDown(
total: Int = 15,
lifecycleScope: CoroutineScope,
onStart: () -> Unit = {},
onTick: (Int) -> Unit = {},
onFinish: () -> Unit
) {
if (isCounting.get()) return
isCounting.set(true)
currentJob = flow {
for (i in total downTo 1) {
emit(i)
delay(1000L)
}
}
.flowOn(Dispatchers.IO)
.onStart {
withContext(Dispatchers.Main) { onStart() }
}
.onEach { remaining ->
withContext(Dispatchers.Main) { onTick(remaining) }
}
.onCompletion { cause ->
withContext(Dispatchers.Main) {
isCounting.set(false)
currentJob = null
// cause == null 为正常结束,有异常(含 CancellationException)时不触发 onFinish
if (cause == null) {
Log.d("CountDownUtil", "倒计时正常结束")
onFinish()
} else {
Log.d("CountDownUtil", "倒计时被取消:${cause.message}")
}
}
}
.catch { e ->
Log.e("CountDownUtil", "倒计时异常:${e.message}", e)
isCounting.set(false)
currentJob = null
}
.launchIn(lifecycleScope)
}
/**
* 取消倒计时(不触发 onFinish)
*/
fun cancelCountDown() {
currentJob?.cancel()
currentJob = null
isCounting.set(false)
Log.d("CountDownUtil", "倒计时已取消")
}
}
@@ -0,0 +1,241 @@
package com.sw.platecabinet.utils
import android.app.ActivityManager
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.Process
import com.sw.platecabinet.activity.InitActivity
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.PrintWriter
import java.io.StringWriter
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.system.exitProcess
/**
* 崩溃处理
*/
class CrashHandler private constructor(private val context: Context) :
Thread.UncaughtExceptionHandler {
companion object {
private const val TAG = "CrashHandler"
private const val CRASH_REPORTS_DIR = "crash_reports"
private const val LOG_LINES = 500 // 收集最近500行日志
@Volatile
private var instance: CrashHandler? = null
fun init(context: Context) {
if (instance == null) {
synchronized(CrashHandler::class.java) {
if (instance == null) {
instance = CrashHandler(context.applicationContext)
}
}
}
}
fun getCrashReportFiles(context: Context): Array<File> {
val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
return if (crashDir.exists() && crashDir.isDirectory) {
crashDir.listFiles { _, name -> name.endsWith(".log") } ?: emptyArray()
} else {
emptyArray()
}
}
fun clearCrashReports(context: Context) {
getCrashReportFiles(context).forEach { it.delete() }
}
}
private val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
init {
Thread.setDefaultUncaughtExceptionHandler(this)
}
override fun uncaughtException(thread: Thread, ex: Throwable) {
handleException(thread, ex)
// 如果系统提供了默认的异常处理器,则交给系统去结束程序
// 否则自己结束程序
defaultHandler?.uncaughtException(thread, ex) ?: run {
Process.killProcess(Process.myPid())
exitProcess(1)
}
}
/**
* 自动重启app
*/
private fun restartApp() {
// 延迟1秒后重启应用
Handler(Looper.getMainLooper()).postDelayed({
val intent = Intent(context, InitActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
val pendingIntent = PendingIntent.getActivity(
context, 0, intent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
)
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + 100, pendingIntent)
Process.killProcess(Process.myPid())
exitProcess(1)
}, 1000)
}
private fun handleException(thread: Thread, ex: Throwable) {
// 收集设备信息和异常信息
val crashInfo = collectCrashInfo(thread, ex)
// 保存日志文件
saveCrashInfoToFile(crashInfo)
// 这里可以添加其他处理逻辑,比如上传到服务器等
}
private fun collectCrashInfo(thread: Thread, ex: Throwable): String {
return buildString {
// 收集设备信息
collectDeviceInfo(this)
// 收集应用日志
try {
append("\n\n").append(collectLogs())
} catch (e: OutOfMemoryError) {
append("collectLogs发生OutOfMemoryError:${e.message}")
}
// 收集线程和异常信息
append("\n\n========== Thread & Exception Info ==========\n")
append("Thread: ${thread.name}\n")
append("Stack Trace:\n")
val sw = StringWriter()
val pw = PrintWriter(sw)
ex.printStackTrace(pw)
var cause: Throwable? = ex.cause
while (cause != null) {
cause.printStackTrace(pw)
cause = cause.cause
}
pw.close()
append(sw.toString())
}
}
private fun collectDeviceInfo(sb: StringBuilder) {
sb.append("========== Device Info ==========\n")
try {
// 应用信息
val pm = context.packageManager
val pi = pm.getPackageInfo(context.packageName, 0)
sb.append("App Version: ${pi.versionName}_${pi.versionCode}\n")
// Android 版本信息
sb.append("OS Version: ${Build.VERSION.RELEASE}_${Build.VERSION.SDK_INT}\n")
// 设备信息
sb.append("Vendor: ${Build.MANUFACTURER}\n")
sb.append("Model: ${Build.MODEL}\n")
sb.append("CPU ABI: ${Build.SUPPORTED_ABIS.firstOrNull() ?: "unknown"}\n")
// 其他信息
sb.append("Locale: ${Locale.getDefault()}\n")
sb.append(
"Current Time: ${
SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss",
Locale.getDefault()
).format(Date())
}\n"
)
// 内存信息
val memoryInfo = ActivityManager.MemoryInfo()
val activityManager =
context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
activityManager.getMemoryInfo(memoryInfo)
sb.append("Available Memory: ${memoryInfo.availMem / (1024 * 1024)}MB\n")
sb.append("Total Memory: ${memoryInfo.totalMem / (1024 * 1024)}MB\n")
sb.append("Low Memory: ${memoryInfo.lowMemory}\n")
} catch (e: Exception) {
Timber.e(e, "Error while collecting device info")
sb.append("Error while collecting device info: ${e.message}\n")
}
}
private fun collectLogs(): String {
return buildString {
append("========== Application Logs ==========\n")
try {
val process = Runtime.getRuntime().exec("logcat -d -v threadtime")
val reader = process.inputStream.bufferedReader()
val logLines = reader.readLines()
val start = kotlin.comparisons.maxOf(0, logLines.size - LOG_LINES)
logLines.subList(start, logLines.size).forEach {
append(it).append("\n")
}
} catch (e: IOException) {
Timber.e(e, "Error collecting logs")
append("Error collecting logs: ${e.message}\n")
}
}
}
private fun saveCrashInfoToFile(crashInfo: String) {
try {
val time = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.getDefault()).format(Date())
val fileName = "crash_$time.log"
val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
if (!crashDir.exists() && !crashDir.mkdirs()) {
Timber.tag(TAG).e("Failed to create crash report directory")
return
}
val crashFile = File(crashDir, fileName)
FileOutputStream(crashFile).use { it.write(crashInfo.toByteArray()) }
Timber.tag(TAG).d("Crash info saved to: ${crashFile.absolutePath}")
} catch (e: Exception) {
Timber.tag(TAG).e(e, "Error saving crash info to file")
}
}
/**
* 清理旧的崩溃日志
*/
fun cleanupOldCrashReports(maxAgeDays: Int = 7) {
val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
if (!crashDir.exists() || !crashDir.isDirectory) return
val now = System.currentTimeMillis()
val maxAgeMillis = maxAgeDays * 24 * 60 * 60 * 1000L
crashDir.listFiles()?.forEach { file ->
if (file.lastModified() < now - maxAgeMillis) {
file.delete()
}
}
}
}
@@ -2,9 +2,11 @@ package com.sw.inbound.utils
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.math.abs
/**
* 时间格式化工具类
@@ -19,10 +21,17 @@ object DateTimeUtils {
}
/**
* 获取带时间的完整中文格式(示例:2025年6月11日 星期三 14:30
* 获取标准时间格式
*/
fun getDateTimeString(date: Date = Date()): String {
return SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA).format(date)
}
/**
* 获取带时间的完整中文格式(示例:2025年06月11日 星期三 14:30
*/
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 的线程安全问题)
@@ -43,8 +52,69 @@ object DateTimeUtils {
*/
fun realTimeChineseDateFlow(intervalMillis: Long = 1000) = flow {
while (true) {
emit(getChineseDateTimePair())
emit(getChineseDateTimeString())
delay(intervalMillis)
}
}
/**
* 解析日期时间字符串
* @param timeString 格式为 "yyyy-MM-dd HH:mm:ss" 的字符串
* @return Date 对象,解析失败返回 null
*/
fun parseDateTime(timeString: String?): Date? {
return try {
if (timeString == null) return null
val format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
format.parse(timeString)
} catch (e: Exception) {
// e.printStackTrace()
Timber.e(e.message)
null
}
}
/**
* 判断给定时间是否距离当前时间超过72小时
* @param timeInMillis 时间戳(毫秒)
* @return true 表示超过72小时,false 表示未超过
*/
fun isMoreThanHoursFromNow(timeInMillis: Long): Boolean {
val currentTime = System.currentTimeMillis()
val timeDifference = currentTime - timeInMillis
val hoursDifference = timeDifference / (1000 * 60 * 60) // 毫秒转小时
return hoursDifference >= 72
}
/**
* 获取时间间隔描述
*/
fun getTimeAgo(date: Date?): String {
if (date == null) return "未知时间"
val now = Date()
val diffMillis = now.time - date.time
// 如果是未来时间
if (diffMillis < 0) {
val futureHours = abs(diffMillis) / (1000 * 60 * 60)
return if (futureHours < 24) {
"未来 $futureHours 小时"
} else {
val days = futureHours / 24
"未来 $days"
}
}
// 过去时间
val hours = diffMillis / (1000 * 60 * 60)
return when {
hours < 1 -> "刚刚"
hours < 24 -> "${hours}小时前"
else -> {
val days = hours / 24
"${days}天前"
}
}
}
}
@@ -0,0 +1,32 @@
package com.sw.platecabinet.utils
import timber.log.Timber
class Debouncer(private val delayMillis: Long) {
companion object {
const val TAG = "Debouncer"
}
private var lastActionTime = 0L
/**
* 执行防抖操作
* @param action 要执行的操作
* @return Boolean 是否执行了操作 (true=已执行, false=被防抖)
*/
fun debounce(tag:String = TAG, action: () -> Unit): Boolean {
val currentTime = System.currentTimeMillis()
if (currentTime - lastActionTime >= delayMillis) {
Timber.tag(tag).d("collectFace,recognizeUserId防止重复操作:执行action回调")
lastActionTime = currentTime
action()
return true
}
Timber.tag(tag).d("collectFace,recognizeUserId防止重复操作-----------------------")
return false
}
// 重置防抖计时
fun reset() {
lastActionTime = 0L
}
}
@@ -0,0 +1,163 @@
package com.sw.platecabinet.utils
import android.content.ContentUris
import android.content.Context
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import androidx.annotation.RequiresApi
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import timber.log.Timber
import java.io.File
import kotlin.io.copyTo
import kotlin.io.outputStream
import kotlin.io.use
import kotlin.text.equals
object FileUtils {
/**
* 从Uri获取File
* example: file:///data/user/0/com.sw.inbound/cache/IMG_17515262353556856678814444882273.jpg
*/
private fun getFileFromUri(context: Context, uri: Uri): File? {
Timber.d("getFileFromUri uri = ${uri.scheme}")
return when (uri.scheme) {
"file" -> File(uri.path ?: return null)
"content" -> {
try {
val inputStream = context.contentResolver.openInputStream(uri) ?: return null
val cacheDir = context.cacheDir
val file = File.createTempFile(
"upload_${System.currentTimeMillis()}",
".jpg",
cacheDir
)
file.outputStream().use { output ->
inputStream.copyTo(output)
}
file
} catch (e: Exception) {
Timber.e(e)
null
}
}
else -> null
}
}
/**
* 通过uri生成http请求体
*/
fun genRequestPart(context: Context, imageUri: Uri): MultipartBody.Part? {
Timber.d("genRequestPart imageUri = $imageUri")
// 1. 从Uri获取文件
val file = getFileFromUri(context, imageUri)
return genRequestPart(file)
}
fun genRequestPart(file: File?): MultipartBody.Part? {
if (file == null) {
Timber.e("getFileFromUri file is null")
return null
}
// 2. 创建请求体
val requestFile = file
.asRequestBody("application/octet-stream".toMediaTypeOrNull())
val imagePart = MultipartBody.Part.createFormData(
"file",
file.name,
requestFile
)
return imagePart
}
/**
* 通过Uri删除文件
* @param context 上下文
* @param uri 文件Uri
* @return Boolean 是否删除成功
*/
fun deleteFileWithUri(context: Context, uri: Uri): Boolean {
Timber.d("deleteFileWithUri uri = ${uri.scheme}")
return when {
// 1. 处理 content:// 类型的Uri (MediaStore)
uri.scheme.equals("content", ignoreCase = true) -> {
deleteContentUriFile(context, uri)
}
// 2. 处理 file:// 类型的Uri
uri.scheme.equals("file", ignoreCase = true) -> {
deleteFileUriFile(uri)
}
// 3. 其他情况尝试直接解析路径
else -> {
deleteFileFromPath(uri.path ?: return false)
}
}
}
// 删除Content Uri文件
private fun deleteContentUriFile(context: Context, uri: Uri): Boolean {
Timber.d("deleteContentUriFile uri = ${uri.scheme}")
return try {
context.contentResolver.delete(uri, null, null) > 0
} catch (e: SecurityException) {
// Android 10+需要特殊处理
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
deleteMediaStoreFile(context, uri)
} else {
false
}
} catch (e: Exception) {
Timber.e(e)
false
}
}
// Android 10+删除MediaStore文件
@RequiresApi(Build.VERSION_CODES.Q)
private fun deleteMediaStoreFile(context: Context, uri: Uri): Boolean {
Timber.d("deleteMediaStoreFile uri = ${uri.scheme}")
val contentResolver = context.contentResolver
val projection = arrayOf(MediaStore.MediaColumns._ID)
return try {
contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val id =
cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
val contentUri = ContentUris.withAppendedId(uri, id)
contentResolver.delete(contentUri, null, null) > 0
} else {
false
}
} ?: false
} catch (e: Exception) {
Timber.e(e)
false
}
}
// 删除File Uri文件
private fun deleteFileUriFile(uri: Uri): Boolean {
Timber.d("deleteFileUriFile uri = $uri")
return try {
File(uri.path ?: return false).delete()
} catch (e: Exception) {
Timber.e(e)
false
}
}
// 直接通过路径删除文件
private fun deleteFileFromPath(path: String): Boolean {
Timber.d("deleteFileFromPath path = $path")
return try {
File(path).delete()
} catch (e: Exception) {
Timber.e(e)
false
}
}
}
@@ -0,0 +1,33 @@
package com.sw.platecabinet.utils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
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 kotlin.ranges.downTo
// 倒计时Flow扩展函数
fun countDownByFlow(
total: Int,
scope: CoroutineScope,
onTick: (Int) -> Unit,
onStart: (() -> Unit)? = null,
onFinish: (() -> Unit)? = null
): Job {
return flow {
for (i in total downTo 0) {
emit(i)
if (i != 0) delay(1000)
}
}.flowOn(Dispatchers.Main)
.onStart { onStart?.invoke() }
.onCompletion { onFinish?.invoke() }
.onEach { onTick.invoke(it) }
.launchIn(scope)
}
@@ -0,0 +1,141 @@
package com.sw.platecabinet.utils
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
class FragmentHelper(
private val fragmentManager: FragmentManager,
private val containerId: Int
) {
/**
* 添加Fragment到容器中
* @param fragment 要添加的Fragment
* @param addToBackStack 是否添加到返回栈
* @param tag Fragment的标签
*/
fun addFragment(fragment: Fragment, addToBackStack: Boolean = false, tag: String? = null) {
fragmentManager.beginTransaction().apply {
add(containerId, fragment, tag)
if (addToBackStack) {
addToBackStack(tag)
}
commit()
}
}
/**
* 替换容器中的Fragment
* @param fragment 要替换的Fragment
* @param addToBackStack 是否添加到返回栈
* @param tag Fragment的标签
* @param animEnter 进入动画
* @param animExit 退出动画
* @param animPopEnter 弹出进入动画
* @param animPopExit 弹出退出动画
*/
fun replaceFragment(
fragment: Fragment,
addToBackStack: Boolean = true,
tag: String? = null,
animEnter: Int = 0,
animExit: Int = 0,
animPopEnter: Int = 0,
animPopExit: Int = 0
) {
fragmentManager.beginTransaction().apply {
if (animEnter != 0 || animExit != 0 || animPopEnter != 0 || animPopExit != 0) {
setCustomAnimations(animEnter, animExit, animPopEnter, animPopExit)
}
replace(containerId, fragment, tag)
if (addToBackStack) {
addToBackStack(tag)
}
commit()
}
}
/**
* 显示Fragment
* @param fragment 要显示的Fragment
*/
fun showFragment(fragment: Fragment) {
fragmentManager.beginTransaction()
.show(fragment)
.commit()
}
/**
* 隐藏Fragment
* @param fragment 要隐藏的Fragment
*/
fun hideFragment(fragment: Fragment) {
fragmentManager.beginTransaction()
.hide(fragment)
.commit()
}
/**
* 移除Fragment
* @param fragment 要移除的Fragment
*/
fun removeFragment(fragment: Fragment) {
fragmentManager.beginTransaction()
.remove(fragment)
.commit()
}
/**
* 返回上一个Fragment
* @return 是否成功返回
*/
fun popBackStack(): Boolean {
return fragmentManager.popBackStackImmediate()
}
/**
* 返回到指定的Fragment
* @param tag Fragment的标签
* @param inclusive 是否包含指定的Fragment
*/
fun popBackStackTo(tag: String, inclusive: Boolean = false): Boolean {
return fragmentManager.popBackStackImmediate(
tag,
if (inclusive) FragmentManager.POP_BACK_STACK_INCLUSIVE else 0
)
}
/**
* 获取当前显示的Fragment
*/
fun getCurrentFragment(): Fragment? {
return fragmentManager.findFragmentById(containerId)
}
/**
* 通过tag查找Fragment
*/
fun findFragmentByTag(tag: String): Fragment? {
return fragmentManager.findFragmentByTag(tag)
}
/**
* 清除所有Fragment
*/
fun clearAllFragments() {
// 清除所有回退栈中的Fragment
fragmentManager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
// 清除所有已添加的Fragment
fragmentManager.fragments.forEach { fragment ->
removeFragment(fragment)
}
}
/**
* 检查返回栈是否为空
*/
fun isBackStackEmpty(): Boolean {
return fragmentManager.backStackEntryCount == 0
}
}
@@ -0,0 +1,68 @@
package com.sw.platecabinet.utils
import kotlinx.coroutines.*
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
class IntervalExecutor {
/**
* 启动定时任务
* @param delayMillis 延迟时间(毫秒)
* @param action 要执行的方法
* @return Job 可用于取消任务
*/
fun startIntervalTask(delayMillis: Long, action: suspend () -> Unit): Job {
return CoroutineScope(Dispatchers.Default).launch {
while (isActive) {
ensureActive()
action()
delay(delayMillis)
}
}
}
/**
* 启动定时任务(带初始延迟)
* @param initialDelay 初始延迟时间(毫秒)
* @param delayMillis 后续执行间隔(毫秒)
* @param action 要执行的方法
* @return Job 可用于取消任务
*/
fun startIntervalTaskWithInitialDelay(
initialDelay: Long,
delayMillis: Long,
action: suspend () -> Unit
): Job {
return CoroutineScope(Dispatchers.Default).launch {
delay(initialDelay)
while (isActive) {
ensureActive()
action()
delay(delayMillis)
}
}
}
}
// 使用示例
//fun main() = runBlocking {
// val executor = IntervalExecutor()
//
// // 示例1:每隔10秒执行一次
// val job1 = executor.startIntervalTask(10000) {
// println("定时任务执行: ${System.currentTimeMillis()}")
// // 这里可以执行你的业务逻辑
// }
//
// // 示例2:先延迟5秒,然后每隔3秒执行一次
// val job2 = executor.startIntervalTaskWithInitialDelay(5000, 3000) {
// println("带初始延迟的定时任务: ${System.currentTimeMillis()}")
// }
//
// // 运行30秒后取消任务
// delay(30000)
// job1.cancel()
// job2.cancel()
// println("所有定时任务已取消")
//}
@@ -0,0 +1,29 @@
package com.sw.platecabinet.utils
import android.app.Activity
import android.content.Context
import android.view.View
import android.view.inputmethod.InputMethodManager
/**
* 键盘工具类
*/
object KeyboardUtils {
/**
* 显示键盘
*/
fun showKeyboard(view: View) {
val imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT)
}
/**
* 隐藏键盘
*/
fun hideKeyboard(activity: Activity) {
val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
val view = activity.currentFocus ?: View(activity)
imm.hideSoftInputFromWindow(view.windowToken, 0)
}
}
@@ -0,0 +1,57 @@
package com.sw.platecabinet.utils;
/**
* Log统一管理类
*/
public class L {
private L() {
/* cannot be instantiated */
throw new UnsupportedOperationException("cannot be instantiated");
}
public static boolean isDebug = true;// 是否需要打印bug,可以在application的onCreate函数里面初始化
private static final String TAG = "mzf";
// 下面四个是默认tag的函数
public static void i(String msg) {
if (isDebug)
android.util.Log.i(TAG, msg);
}
public static void d(String msg) {
if (isDebug)
android.util.Log.d(TAG, msg);
}
public static void e(String msg) {
if (isDebug)
android.util.Log.e(TAG, msg);
}
public static void v(String msg) {
if (isDebug)
android.util.Log.v(TAG, msg);
}
// 下面是传入自定义tag的函数
public static void i(String tag, String msg) {
if (isDebug)
android.util.Log.i(tag, msg);
}
public static void d(String tag, String msg) {
if (isDebug)
android.util.Log.d(tag, msg);
}
public static void e(String tag, String msg) {
if (isDebug)
android.util.Log.e(tag, msg);
}
public static void v(String tag, String msg) {
if (isDebug)
android.util.Log.v(tag, msg);
}
}
@@ -0,0 +1,88 @@
//package com.sw.platecabinet.utils
//
//import com.sw.platecabinet.model.response.EquipmentUserInfo
//import timber.log.Timber
//import kotlin.math.ceil
//
///**
// * 列表排序工具类
// */
//object ListArrangementUtil {
//
// /**
// * page item水平排序
// */
// fun horizontalSortPageItem(
// list: List<EquipmentUserInfo>,
// columns: Int,
// defaultValue: EquipmentUserInfo = EquipmentUserInfo()
// ): List<EquipmentUserInfo> {
// Timber.d("horizontalSortPageItem")
// require(columns > 0) { "Columns must be positive" }
//
// val filledList = list.padToMultiple(columns, defaultValue)
// val chunks = filledList.chunked(columns)
//
// return (0 until columns)
// .windowed(2, 2, partialWindows = true)
// .flatMap { group ->
// when (group.size) {
// 2 -> chunks.flatMap { listOf(it[group[0]], it[group[1]]) }
// 1 -> chunks.flatMap {
// listOf(it[group[0]], EquipmentUserInfo(equipmentBoxCode = null))
// }.dropLast(1)
// else -> error("Unexpected group size")
// }
// }
// }
//
// /**
// * item 垂直排序
// */
// fun verticalSortItem(list: List<EquipmentUserInfo>): List<EquipmentUserInfo> {
// Timber.d("verticalSortItem")
// val users = list.padToEvenSize()
// val half = users.size / 2
//
// return users.take(half)
// .zip(users.drop(half)) { a, b -> listOf(a, b) }
// .flatten()
// }
//
// /**
// * page item垂直排序
// */
// fun convertToColumnFirst(
// original: List<EquipmentUserInfo>,
// cols: Int
// ): List<EquipmentUserInfo> {
// Timber.d("convertToColumnFirst")
// require(cols > 0) { "Columns must be positive" }
//
// val rows = ceil(original.size.toDouble() / cols).toInt()
// return List(original.size) { pos ->
// original.getOrNull(pos % rows * cols + pos / rows) ?: EquipmentUserInfo()
// }
// }
//
// // 提取的扩展函数
// private fun List<EquipmentUserInfo>.padToMultiple(
// multiple: Int,
// defaultValue: EquipmentUserInfo
// ): List<EquipmentUserInfo> {
// return if (size % multiple != 0) {
// this + List(multiple - (size % multiple)) { defaultValue }
// } else {
// this
// }
// }
//
// private fun List<EquipmentUserInfo>.padToEvenSize(): List<EquipmentUserInfo> {
// return if (size % 2 != 0) {
// Timber.d("列表长度必须是偶数")
// this + EquipmentUserInfo()
// } else {
// this
// }
// }
//}
@@ -0,0 +1,698 @@
package com.sw.platecabinet.utils;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import com.sw.plate.App;
import com.sw.plate.utils.AppUtil;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringReader;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class LogFileUtil {
private static boolean isSaveLog = true;
// 使用内部存储,不会被系统缓存清理策略影响
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_KB = 2;//获取文件大小单位为KB的double值
public static final int SIZETYPE_MB = 3;//获取文件大小单位为MB的double值
public static final int SIZETYPE_GB = 4;//获取文件大小单位为GB的double值
private static String urlNull = "原文件路径不存在";
private static String isFile = "原文件不是文件";
private static String canRead = "原文件不能读";
private static String copyFalse = "备份失败!";
private static String cFromFile = "创建原文件出错:";
private static String ctoFile = "创建备份文件出错:";
/**
* 写入文件
* FileUtil.byteWriteFile(getExternalFilesDir("123"),"test.jpg",nv21);
*
* @param filePath
* @param fileName
* @param bytes
*/
public static void byteWriteFile(File filePath, String fileName, byte[] bytes) {
BufferedOutputStream bout = null;
try {
File file = new File(filePath, fileName);
if (file.exists() == false) {
if (file.getParentFile().exists() == false) {
file.getParentFile().mkdirs();
}
file.createNewFile();
}
bout = new BufferedOutputStream(new FileOutputStream(file, false));
bout.write(bytes);
bout.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (bout != null) {
try {
bout.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 获取文件名
*
* @param path 路径
* @return 文件名
*/
public static String getFileName(String path) {
int index = path.lastIndexOf("/");
return path.substring(index + 1);
}
/**
* 根据文件路径获取文件
*
* @param path 路径
* @return
*/
public static File getFileByPath(String path) {
File file = new File(path);
if (file.exists()) {
return new File(path);
} else {
return null;
}
}
/**
* 重命名文件
*
* @param filePath 文件路径
* @param newName 新名称
* @return {@code true}: 重命名成功<br>{@code false}: 重命名失败
*/
public static boolean rename(String filePath, String newName) {
return rename(getFileByPath(filePath), newName);
}
/**
* 重命名文件
*
* @param file 文件
* @param newName 新名称
* @return {@code true}: 重命名成功<br>{@code false}: 重命名失败
*/
public static boolean rename(File file, String newName) {
// 文件为空返回false
if (file == null) return false;
// 文件不存在返回false
if (!file.exists()) return false;
// 新的文件名为空返回false
if (AppUtil.isEmpty(newName)) return false;
// 如果文件名没有改变返回true
if (newName.equals(file.getName())) return true;
File newFile = new File(file.getParent() + File.separator + newName);
// 如果重命名的文件已存在返回false
return !newFile.exists()
&& file.renameTo(newFile);
}
/**
* 判断文件是否存在
*
* @param path 文件的路径,含文件后缀和文件名
* @return 是否存在
*/
public static boolean isFileExists(String path) {
File file = new File(path);
if (file.exists()) {
return true;
} else {
return false;
}
}
/**
* 判断文件是否存在
*
* @return 是否存在
*/
public static boolean isFileExists(File file) {
if (file.exists()) {
return true;
} else {
return false;
}
}
/**
* 删除文件夹及文件夹下所有内容
*
* @param path 文件夹路径
* @return 返回是否删除成功
*/
public static boolean deleteFiles(String path) {
if (getFileByPath(path) != null) {
return deleteFiles(getFileByPath(path));
} else {
return false;
}
}
/**
* 删除文件夹及文件夹下所有内容
*
* @param file 文件
* @return 返回是否删除成功
*/
public static boolean deleteFiles(File file) {
try {
if (file.exists()) { // 判断文件是否存在
if (file.isFile()) { // 判断是否是文件
file.delete(); // delete()方法
} else if (file.isDirectory()) { // 否则如果它是一个目录
File files[] = file.listFiles(); // 声明目录下所有的文件 files[];
for (int i = 0; i < files.length; i++) { // 遍历目录下所有的文件
deleteFiles(files[i].getPath()); // 把每个文件 用这个方法进行迭代
}
file.delete();//删除目录
}
//file.delete();
return true;
} else {
return false;
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 获取文件的Uri
*
* @param path 文件的路径
* @return 文件的Uri
*/
public static Uri getUriFromFile(String path) {
File file = new File(path);
return Uri.fromFile(file);
}
/**
* 获取文件指定文件的指定单位的大小
*
* @param filePath 文件路径
* @param sizeType 获取大小的类型1为B、2为KB、3为MB、4为GB
* @return double值的大小
*/
public static double getFileOrFilesSize(String filePath, int sizeType) {
File file = new File(filePath);
long blockSize = 0;
try {
if (file.isDirectory()) {
blockSize = getFileSizes(file);
} else {
blockSize = getFileSize(file);
}
} catch (Exception e) {
e.printStackTrace();
L.i("获取文件大小", "获取失败!");
}
return FormetFileSize(blockSize, sizeType);
}
/**
* 调用此方法自动计算指定文件或指定文件夹的大小
*
* @param filePath 文件路径
* @return 计算好的带B、KB、MB、GB的字符串
*/
public static String getAutoFileOrFilesSize(String filePath) {
File file = new File(filePath);
long blockSize = 0;
try {
if (file.isDirectory()) {
blockSize = getFileSizes(file);
} else {
blockSize = getFileSize(file);
}
} catch (Exception e) {
e.printStackTrace();
L.i("获取文件大小", "获取失败!");
}
return FormetFileSize(blockSize);
}
/**
* 获取指定文件大小
*
* @param file
* @return
* @throws Exception
*/
private static long getFileSize(File file) throws Exception {
long size = 0;
if (file.exists()) {
FileInputStream fis = null;
fis = new FileInputStream(file);
size = fis.available();
} else {
// file.createNewFile();
L.i("获取文件大小", "文件不存在!");
}
return size;
}
/**
* 获取目录下指定文件名的文件包括子目录
* <p>大小写忽略</p>
*
* @param dirPath 目录路径
* @param fileName 文件名
* @return 文件链表
*/
public static List<File> searchFileInDir(String dirPath, String fileName) {
return searchFileInDir(getFileByPath(dirPath), fileName);
}
/**
* 获取目录下指定文件名的文件包括子目录
* <p>大小写忽略</p>
*
* @param dir 目录
* @param fileName 文件名
* @return 文件链表
*/
public static List<File> searchFileInDir(File dir, String fileName) {
if (dir == null || !dir.isDirectory()) return null;
List<File> list = new ArrayList<>();
File[] files = dir.listFiles();
if (files != null && files.length != 0) {
for (File file : files) {
if (file.getName().toUpperCase().equals(fileName.toUpperCase())) {
list.add(file);
}
if (file.isDirectory()) {
list.addAll(searchFileInDir(file, fileName));
}
}
}
return list;
}
/**
* 获取指定文件夹
*
* @param f
* @return
* @throws Exception
*/
private static long getFileSizes(File f) throws Exception {
long size = 0;
File flist[] = f.listFiles();
for (int i = 0; i < flist.length; i++) {
if (flist[i].isDirectory()) {
size = size + getFileSizes(flist[i]);
} else {
size = size + getFileSize(flist[i]);
}
}
return size;
}
/**
* 转换文件大小
*
* @param fileS
* @return
*/
private static String FormetFileSize(long fileS) {
DecimalFormat df = new DecimalFormat("#.00");
String fileSizeString = "";
String wrongSize = "0B";
if (fileS == 0) {
return wrongSize;
}
if (fileS < 1024) {
fileSizeString = df.format((double) fileS) + "B";
} else if (fileS < 1048576) {
fileSizeString = df.format((double) fileS / 1024) + "KB";
} else if (fileS < 1073741824) {
fileSizeString = df.format((double) fileS / 1048576) + "MB";
} else {
fileSizeString = df.format((double) fileS / 1073741824) + "GB";
}
return fileSizeString;
}
/**
* 转换文件大小,指定转换的类型
*
* @param fileS
* @param sizeType
* @return
*/
private static double FormetFileSize(long fileS, int sizeType) {
DecimalFormat df = new DecimalFormat("#.00");
double fileSizeLong = 0;
switch (sizeType) {
case SIZETYPE_B:
fileSizeLong = Double.valueOf(df.format((double) fileS));
break;
case SIZETYPE_KB:
fileSizeLong = Double.valueOf(df.format((double) fileS / 1024));
break;
case SIZETYPE_MB:
fileSizeLong = Double.valueOf(df.format((double) fileS / 1048576));
break;
case SIZETYPE_GB:
fileSizeLong = Double.valueOf(df.format((double) fileS / 1073741824));
break;
default:
break;
}
return fileSizeLong;
}
/**
* 复制文件
*
* @param fromFilePath 旧文件地址和名称
* @param toFilePath 新文件地址和名称
* @return 返回备份文件的信息,ok是成功,其它就是错误
*/
public static File copyFile(String fromFilePath, String toFilePath) {
File fromFile = null;
File toFile = null;
try {
fromFile = new File(fromFilePath);
} catch (Exception e) {
L.i(cFromFile + e.getMessage());
return null;
}
try {
toFile = new File(toFilePath);
if (toFile.isDirectory()) {
toFile = new File(toFilePath.endsWith("/") ? toFilePath + fromFile.getName() : toFilePath + "/" + fromFile.getName());
}
} catch (Exception e) {
L.i(ctoFile + e.getMessage());
return null;
}
if (!fromFile.exists()) {
L.i(urlNull);
return null;
}
if (!fromFile.isFile()) {
L.i(isFile);
return null;
}
if (!fromFile.canRead()) {
L.i(canRead);
return null;
}
// 复制到的路径如果不存在就创建
if (!toFile.getParentFile().exists()) {
toFile.getParentFile().mkdirs();
}
if (toFile.exists()) {
toFile.delete();
}
if (!toFile.canWrite()) {
//return notWrite;
}
try {
FileInputStream fosfrom = new FileInputStream(
fromFile);
FileOutputStream fosto = new FileOutputStream(toFile);
byte bt[] = new byte[1024];
int c;
while ((c = fosfrom.read(bt)) > 0) {
fosto.write(bt, 0, c); // 将内容写到新文件当中
}
//关闭数据流
fosfrom.close();
fosto.close();
} catch (Exception e) {
e.printStackTrace();
L.i(copyFalse + e.getMessage());
return null;
}
return toFile;
}
/**
* 创建文件
*
* @param path 文件路径
* @return 创建的文件
*/
public static synchronized File createNewFile(String path) {
File file = new File(path);
String parentPath = path.substring(0, path.lastIndexOf("/") + 1);
File parentFile = new File(parentPath);
if (!parentFile.exists()) {
parentFile.mkdirs();
}
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
return file;
}
/***
* 根据文件后缀回去MIME类型
****/
private static String getMIMEType(File file) {
String type = "*/*";
String fName = file.getName();
//获取后缀名前的分隔符"."在fName中的位置。
int dotIndex = fName.lastIndexOf(".");
if (dotIndex < 0) {
return type;
}
/* 获取文件的后缀名*/
String end = fName.substring(dotIndex, fName.length()).toLowerCase();
if (end == "") return type;
//在MIME和文件类型的匹配表中找到对应的MIME类型。
for (int i = 0; i < MIME_MapTable.length; i++) { //MIME_MapTable??在这里你一定有疑问,这个MIME_MapTable是什么?
if (end.equals(MIME_MapTable[i][0]))
type = MIME_MapTable[i][1];
}
return type;
}
/**
* 获取全路径中的文件拓展名
*
* @param file 文件
* @return 文件拓展名
*/
public static String getFileExtension(File file) {
if (file == null) return null;
return getFileExtension(file.getPath());
}
/**
* 获取全路径中的文件拓展名
*
* @param filePath 文件路径
* @return 文件拓展名
*/
public static String getFileExtension(String filePath) {
if (AppUtil.isEmpty(filePath)) return filePath;
int lastPoi = filePath.lastIndexOf('.');
int lastSep = filePath.lastIndexOf(File.separator);
if (lastPoi == -1 || lastSep >= lastPoi) return "";
return filePath.substring(lastPoi + 1);
}
/**
* 调用系统应用打开文件
*
* @param context 上下文
* @param file 文件
*/
public static void openFile(Context context, File file) {
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//设置intent的Action属性
intent.setAction(Intent.ACTION_VIEW);
//获取文件file的MIME类型
String type = getMIMEType(file);
//设置intent的data和Type属性。
intent.setDataAndType(Uri.fromFile(file), type);
//跳转
try {
context.startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
L.i("找不到打开此文件的应用!");
}
}
/**
* 保存文本文件
* FileUtil.saveStrFile("测试", "log.txt", FILE_STR_PATH, true);
*
* @param res 写入内容
* @param fileName 文件名
* @param filePath 路径
* @param append true新增 false替换
* @return
*/
public static boolean saveStrFile(String res, String fileName, String filePath, boolean append) {
boolean flag = true;
BufferedReader bufferedReader = null;
BufferedWriter bufferedWriter = null;
try {
File file = new File(filePath, fileName);
if (!file.exists()) {
file.getParentFile().mkdirs();
file.createNewFile();
}
bufferedReader = new BufferedReader(new StringReader(res));
bufferedWriter = new BufferedWriter(new FileWriter(file, append));
char buf[] = new char[1024]; //字符缓冲区
int len;
while ((len = bufferedReader.read(buf)) != -1) {
bufferedWriter.write(buf, 0, len);
}
bufferedWriter.flush();
bufferedReader.close();
bufferedWriter.close();
} catch (IOException e) {
e.printStackTrace();
flag = false;
return flag;
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return flag;
}
public static void saveLog(String... logs) {
if (!isSaveLog) {
return;
}
// 提交到单线程队列,串行执行,避免并发写入竞争
LOG_EXECUTOR.execute(() -> {
StringBuilder sb = new StringBuilder();
for (String log : logs) {
sb.append("===")
.append((AppUtil.formatDateGetCurrentTime()))
.append("===")
.append("\n")
.append(log)
.append("\n");
}
String fileName = "log" + AppUtil.formatDateGetDay(System.currentTimeMillis()) + ".txt";
LogFileUtil.saveStrFile(sb.toString(), fileName, FILE_STR_PATH, true);
});
}
private static final String[][] MIME_MapTable = {
// {后缀名,MIME类型}
{".3gp", "video/3gpp"},
{".apk", "application/vnd.android.package-archive"},
{".asf", "video/x-ms-asf"},
{".avi", "video/x-msvideo"},
{".bin", "application/octet-stream"},
{".bmp", "image/bmp"},
{".c", "text/plain"},
{".class", "application/octet-stream"},
{".conf", "text/plain"},
{".cpp", "text/plain"},
{".doc", "application/msword"},
{".docx",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"},
{".xls", "application/vnd.ms-excel"},
{".xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
{".exe", "application/octet-stream"},
{".gif", "image/gif"},
{".gtar", "application/x-gtar"},
{".gz", "application/x-gzip"},
{".h", "text/plain"},
{".htm", "text/html"},
{".html", "text/html"},
{".jar", "application/java-archive"},
{".java", "text/plain"},
{".jpeg", "image/jpeg"},
{".jpg", "image/jpeg"},
{".js", "application/x-javascript"},
{".log", "text/plain"},
{".m3u", "audio/x-mpegurl"},
{".m4a", "audio/mp4a-latm"},
{".m4b", "audio/mp4a-latm"},
{".m4p", "audio/mp4a-latm"},
{".m4u", "video/vnd.mpegurl"},
{".m4v", "video/x-m4v"},
{".mov", "video/quicktime"},
{".mp2", "audio/x-mpeg"},
{".mp3", "audio/x-mpeg"},
{".mp4", "video/mp4"},
{".mpc", "application/vnd.mpohun.certificate"},
{".mpe", "video/mpeg"},
{".mpeg", "video/mpeg"},
{".mpg", "video/mpeg"},
{".mpg4", "video/mp4"},
{".mpga", "audio/mpeg"},
{".msg", "application/vnd.ms-outlook"},
{".ogg", "audio/ogg"},
{".pdf", "application/pdf"},
{".png", "image/png"},
{".pps", "application/vnd.ms-powerpoint"},
{".ppt", "application/vnd.ms-powerpoint"},
{".pptx",
"application/vnd.openxmlformats-officedocument.presentationml.presentation"},
{".prop", "text/plain"}, {".rc", "text/plain"},
{".rmvb", "audio/x-pn-realaudio"}, {".rtf", "application/rtf"},
{".sh", "text/plain"}, {".tar", "application/x-tar"},
{".tgz", "application/x-compressed"}, {".txt", "text/plain"},
{".wav", "audio/x-wav"}, {".wma", "audio/x-ms-wma"},
{".wmv", "audio/x-ms-wmv"},
{".wps", "application/vnd.ms-works"}, {".xml", "text/plain"},
{".z", "application/x-compress"},
{".zip", "application/x-zip-compressed"}, {"", "*/*"}};
}
@@ -0,0 +1,151 @@
package com.sw.platecabinet.utils
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.provider.Settings
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
class PermissionHelper private constructor(
private val context: Context,
private val permissions: Array<String>,
internal val requestCode: Int,
private val rationale: String? = null,
private val onGranted: (() -> Unit)?
) {
companion object {
/**
* 创建权限请求构建器
*/
fun with(context: Context, permissions: Array<String>, requestCode: Int): Builder {
return Builder(context, permissions, requestCode)
}
/**
* 检查是否已授予所有权限
*/
fun checkPermissions(context: Context, permissions: Array<String>): Boolean {
return permissions.all { permission ->
ContextCompat.checkSelfPermission(
context,
permission
) == PackageManager.PERMISSION_GRANTED
}
}
/**
* 处理权限请求结果(供BaseActivity/BaseFragment调用)
* @return 是否所有权限都已授予
*/
fun handlePermissionResult(
context: Context,
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray,
permissionHelper: PermissionHelper? = null
): Boolean {
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
permissionHelper?.onGranted?.invoke()
return true
}
val permanentlyDeniedPermissions = mutableListOf<String>()
permissions.forEachIndexed { index, permission ->
if (grantResults[index] != PackageManager.PERMISSION_GRANTED) {
if (!ActivityCompat.shouldShowRequestPermissionRationale(
context as Activity,
permission
)
) {
permanentlyDeniedPermissions.add(permission)
}
}
}
return false
}
/**
* 跳转到应用设置页面
*/
fun openAppSettings(context: Context) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
context.startActivity(intent)
}
}
class Builder(
private val context: Context,
private val permissions: Array<String>,
private val requestCode: Int
) {
private var rationale: String? = null
private var onGranted: (() -> Unit)? = null
/**
* 设置权限说明
*/
fun setRationale(rationale: String): Builder {
this.rationale = rationale
return this
}
/**
* 设置权限授予回调
*/
fun onGranted(callback: () -> Unit): Builder {
this.onGranted = callback
return this
}
/**
* 构建PermissionHelper实例
*/
fun build(): PermissionHelper {
return PermissionHelper(context, permissions, requestCode, rationale, onGranted)
}
}
/**
* 检查并请求权限
*/
fun checkAndRequest() {
if (checkPermissions(context, permissions)) {
onGranted?.invoke()
} else {
requestPermissions()
}
}
private fun requestPermissions() {
val activity = context as? Activity ?: return
val shouldShowRationale = permissions.any { permission ->
ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
}
if (shouldShowRationale && rationale != null) {
AlertDialog.Builder(activity)
.setTitle("权限说明")
.setMessage(rationale)
.setPositiveButton("确定") { _, _ ->
doRequestPermissions(activity)
}
.setNegativeButton("取消", null)
.show()
} else {
doRequestPermissions(activity)
}
}
private fun doRequestPermissions(activity: Activity) {
ActivityCompat.requestPermissions(activity, permissions, requestCode)
}
}
@@ -0,0 +1,66 @@
package com.sw.platecabinet.utils
import androidx.fragment.app.FragmentActivity
import com.sw.platecabinet.activity.BaseActivity
object PlateUtils {
fun open(
fragmentActivity: FragmentActivity,
code: String?,
isScanCode: Boolean,
isSetting: Boolean = false,
callback: () -> Unit = {}
) {
val activity = fragmentActivity as BaseActivity<*>
activity.window.decorView.postDelayed({
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()
// }
// })
}
// fun openDirect(boxCode: String?) {
// val boxNumber = boxCode?.toIntOrNull() ?: -1
// if (boxNumber < 0) return
// SerialApi.openPlate(boxNumber, object : SerialPortManager.SendCallback {
// override fun onSuccess() {
//
// }
//
// override fun onFail(e: Exception?) {
//
// }
// })
// }
}
@@ -0,0 +1,103 @@
package com.sw.platecabinet.utils
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.WriterException
import com.google.zxing.common.BitMatrix
import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import kotlin.apply
import kotlin.ranges.until
import kotlin.text.isEmpty
/**
* 二维码生成工具类
*/
object QRCodeUtil {
/**
* 生成二维码(默认大小)
* @param content 二维码内容
* @return 生成的二维码Bitmap
*/
@JvmOverloads
fun generateQRCode(content: String, size: Int = 500): Bitmap? {
return generateQRCode(content, size, Color.BLACK, Color.WHITE)
}
/**
* 生成二维码(自定义颜色)
* @param content 二维码内容
* @param size 二维码边长(像素)
* @param colorCode 二维码颜色
* @param backgroundColor 背景颜色
* @return 生成的二维码Bitmap
*/
fun generateQRCode(
content: String,
size: Int,
colorCode: Int,
backgroundColor: Int
): Bitmap? {
if (content.isEmpty()) {
return null
}
return try {
val hints = mutableMapOf<EncodeHintType, Any>().apply {
put(EncodeHintType.CHARACTER_SET, "UTF-8")
put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H) // 纠错级别
put(EncodeHintType.MARGIN, 1) // 边距
}
val bitMatrix = QRCodeWriter().encode(
content,
BarcodeFormat.QR_CODE,
size,
size,
hints
)
val pixels = IntArray(size * size).apply {
for (y in 0 until size) {
for (x in 0 until size) {
this[y * size + x] = if (bitMatrix.get(x, y)) colorCode else backgroundColor
}
}
}
Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888).apply {
setPixels(pixels, 0, size, 0, 0, size, size)
}
} catch (e: WriterException) {
e.printStackTrace()
null
}
}
/**
* 生成带Logo的二维码
* @param content 二维码内容
* @param size 二维码边长(像素)
* @param logo Logo Bitmap
* @return 带Logo的二维码Bitmap
*/
fun generateQRCodeWithLogo(content: String, size: Int, logo: Bitmap?): Bitmap? {
val qrCode = generateQRCode(content, size) ?: return null
logo ?: return qrCode
val logoSize = size / 5 // Logo大小约为二维码的1/5
val scaledLogo = Bitmap.createScaledBitmap(logo, logoSize, logoSize, false)
val offset = (size - logoSize) / 2
return Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888).apply {
val canvas = Canvas(this)
canvas.drawBitmap(qrCode, 0f, 0f, null)
canvas.drawBitmap(scaledLogo, offset.toFloat(), offset.toFloat(), null)
}
}
}
@@ -0,0 +1,15 @@
package com.sw.platecabinet.utils
class RegisterCallbackHandler {
private var isCallbackCalled = false
private val lock = Any()
fun setCallback(callback: () -> Unit) {
synchronized(lock) {
if (!isCallbackCalled) {
isCallbackCalled = true
callback()
}
}
}
}
@@ -0,0 +1,427 @@
package com.sw.platecabinet.utils;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Build;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class SoundPoolUtil {
private static final String TAG = SoundPoolUtil.class.getSimpleName();
private static SoundPoolUtil mSound;
private SoundPool mSoundPool;
private boolean isLoadC = false;
private Map<String, Integer> idCache;
private List<Integer> sidCache;
public SoundPoolUtil() {
idCache = new HashMap<>();
sidCache = new ArrayList<>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
AudioAttributes aab = new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build();
mSoundPool = new SoundPool.Builder()
.setMaxStreams(10)
.setAudioAttributes(aab)
.build();
} else {
mSoundPool = new SoundPool(10, AudioManager.STREAM_MUSIC, 8);
}
// mSoundPool = new SoundPool(60, AudioManager.USE_DEFAULT_STREAM_TYPE, 7);
mSoundPool.setOnLoadCompleteListener(new MyOnLoadCompleteListener());
}
public static SoundPoolUtil getInstance() {
synchronized (SoundPoolUtil.class) {
if (mSound == null) {
mSound = new SoundPoolUtil();
}
}
return mSound;
}
private int loadCompleteSize = 0;
private LoadCompletion completionListener;
public void setCompletionListener(LoadCompletion listener) {
this.completionListener = listener;
}
private class MyOnLoadCompleteListener implements SoundPool.OnLoadCompleteListener {
@Override
public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {
loadCompleteSize++;
L.e("loadSize" + loadSize + "===" + loadCompleteSize);
if (loadSize == loadCompleteSize) {
isLoadC = true;
if (completionListener != null) {
completionListener.onCompletion();
}
}
}
}
/**
* 加载指定资源
*
* @param name
* @param path
*/
public void loadR(String name, String path) {
if (checkSoundPool()) {
if (!idCache.containsKey(name)) {
idCache.put(name, mSoundPool.load(path, 1));
}
}
}
private int loadSize = 0;
/**
* 加载指定路径列表的资源
*
* @param map
*/
public void loadR(Map<String, String> map) {
loadSize = map.size();
Set<Map.Entry<String, String>> entries = map.entrySet();
for (Map.Entry<String, String> entry : entries) {
String key = entry.getKey();
if (checkSoundPool()) {
if (!idCache.containsKey(key)) {
idCache.put(key, mSoundPool.load(entry.getValue(), 1));
}
}
}
}
/**
* 加载指定AssetFileDescriptor的资源
*
* @param name
* @param afd
*/
public void loadRF(String name, AssetFileDescriptor afd) {
if (checkSoundPool()) {
if (!idCache.containsKey(name)) {
idCache.put(name, mSoundPool.load(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength(), 1));
}
}
}
/**
* 加载指定AssetFileDescriptor列表的资源
*
* @param map
*/
public void loadRF(Map<String, AssetFileDescriptor> map) {
Set<Map.Entry<String, AssetFileDescriptor>> entries = map.entrySet();
for (Map.Entry<String, AssetFileDescriptor> entry : entries) {
String key = entry.getKey();
if (checkSoundPool()) {
if (!idCache.containsKey(key)) {
idCache.put(key, mSoundPool.load(entry.getValue().getFileDescriptor(), entry.getValue().getStartOffset(), entry.getValue().getLength(), 1));
}
}
}
}
/**
* 加载指定列表资源
*
* @param context
* @param map
*/
public void loadR(Context context, Map<String, Integer> map) {
loadSize = map.size();
Set<Map.Entry<String, Integer>> entries = map.entrySet();
for (Map.Entry<String, Integer> entry : entries) {
String key = entry.getKey();
if (checkSoundPool()) {
if (!idCache.containsKey(key)) {
idCache.put(key, mSoundPool.load(context, entry.getValue(), 1));
}
}
}
}
/**
* 加载单个音频
*
* @param context
* @param name
* @param res
*/
public void loadR(Context context, String name, int res) {
if (checkSoundPool()) {
if (!idCache.containsKey(name)) {
idCache.put(name, mSoundPool.load(context, res, 1));
}
}
}
/**
* 播放指定音频,并返用于停止、暂停、恢复的StreamId
*
* @param name
* @param times
* @return
*/
public int play(String name, int times) {
L.e(String.format("play %s times=%s", name, times));
return this.play(name, 1, 1, 1, times, 1);
}
/**
* 播放指定音频,并指定播放次数和频率
*
* @param name
* @param times
* @param rate
* @return
*/
public int play(String name, int times, int rate) {
return this.play(name, 1, 1, 1, times, rate);
}
/**
* 播放指定音频,并指定优先级和播放频率
*
* @param name
* @param property
* @param times
* @param rate
* @return
*/
public int play(String name, int property, int times, int rate) {
return this.play(name, 1, 1, property, times, rate);
}
/**
* 播放指定音频,并指定左右声道、优先级、播放次数、播放频率
*
* @param name
* @param leftVolume
* @param rightVolume
* @param property
* @param times
* @param rate
* @return
*/
public int play(String name, float leftVolume, float rightVolume, int property, int times, int rate) {
int streamId = -1;
if (checkSoundPool()) {
L.d(TAG, "play: " + name);
if (idCache.containsKey(name) && isLoadC) {
L.d(TAG, "name:" + idCache.get(name));
streamId = mSoundPool.play(idCache.get(name), leftVolume, rightVolume, property, times, rate);
L.d(TAG, "streadmId:" + streamId);
sidCache.add(streamId);
}
}
return streamId;
}
/**
* 播放指定列表的音频,并返回并返用于停止、暂停、恢复的StreamId列表
*
* @param names
* @param times
* @return
*/
public List<Integer> play(List<String> names, int times) {
return this.play(names, 1, 1, 1, times, 1);
}
/**
* 播放指定列表的音频,并返回并返用于停止、暂停、恢复的StreamId列表,指定次数和频率
*
* @param names
* @param times
* @param rate
* @return
*/
public List<Integer> play(List<String> names, int times, int rate) {
return this.play(names, 1, 1, 1, times, rate);
}
/**
* 播放指定列表的音频,并返回并返用于停止、暂停、恢复的StreamId列表,指定所有参数
*
* @param names
* @param leftVolume
* @param rightVolume
* @param property
* @param times
* @param rate
* @return
*/
public List<Integer> play(List<String> names, int leftVolume, int rightVolume, int property, int times, int rate) {
List<Integer> streamIds = new ArrayList<>();
if (checkSoundPool()) {
for (String name : names) {
if (idCache.containsKey(name) && isLoadC) {
int a = mSoundPool.play(idCache.get(name), leftVolume, rightVolume, property, times, rate);
streamIds.add(a);
sidCache.add(a);
}
}
}
return streamIds;
}
/**
* 停止指定id音频
*/
public void stop(int r) {
if (checkSoundPool()) {
mSoundPool.stop(r);
}
}
/**
* 停止指定列表音频
*/
public void stopAll() {
if (checkSoundPool()) {
for (int r : sidCache) {
mSoundPool.stop(r);
}
}
}
/**
* 暂停指定音效
*
* @param r
*/
public void pause(int r) {
if (checkSoundPool()) {
mSoundPool.pause(r);
}
}
/**
* 暂停指定列表音频
*
* @param list
*/
public void pause(List<Integer> list) {
if (checkSoundPool()) {
for (int r : list) {
mSoundPool.pause(r);
}
}
}
/**
* 暂停所有音效
*/
public void pauseAll() {
mSoundPool.autoPause();
}
/**
* 恢复指定音频播放
*
* @param r
*/
public void resume(int r) {
if (checkSoundPool()) {
mSoundPool.resume(r);
}
}
/**
* 恢复指定列表的音频
*
* @param list
*/
public void resume(List<Integer> list) {
if (checkSoundPool()) {
for (int r : list) {
mSoundPool.resume(r);
}
}
}
/**
* 恢复所有暂停的音频
*/
public void resumeAll() {
if (checkSoundPool()) {
mSoundPool.autoResume();
}
}
/**
* 卸载指定音频
*
* @param name
*/
public void unLoad(String name) {
if (checkSoundPool()) {
if (idCache.containsKey(name)) {
mSoundPool.unload(idCache.get(name));
idCache.remove(name);
}
}
}
/**
* 卸载指定列表的音频
*
* @param names
*/
public void unLoad(List<String> names) {
if (checkSoundPool()) {
for (String name : names) {
if (idCache.containsKey(name)) {
mSoundPool.unload(idCache.get(name));
idCache.remove(name);
}
}
}
}
/**
* 释放所有资源,如果想继续播放,需要重新加载资源
*/
public void release() {
if (checkSoundPool()) {
mSound = null;
mSoundPool.release();
idCache.clear();
}
}
private boolean checkSoundPool() {
if (mSoundPool != null) {
return true;
}
return false;
}
public interface LoadCompletion {
void onCompletion();
}
}
@@ -0,0 +1,55 @@
package com.sw.platecabinet.utils
import com.sw.inbound.utils.SPUtil
import com.sw.inbound.utils.SPUtil.Companion.getInstance
import com.sw.platecabinet.GlobalKey
import com.sw.platecabinet.MyApp
object SpTool {
const val LAST_FACE_TIMESTAMP: String = "faceTimestamp"
const val PLATE_BOX_DATA: String = "plateBoxBind"
const val IS_FIRST_GET_FACE = "isFirstGetFace"
private var spUtil: SPUtil? = null
init {
spUtil = getInstance(MyApp.instance!!, "default_sp")
}
fun putString(key:String, value:String) {
spUtil?.put(key, value)
}
var lastFaceTimestamp: Long
get() = spUtil?.get<Long?>(SpTool.LAST_FACE_TIMESTAMP, 0L) ?: 0
set(timestamp) {
spUtil?.put(LAST_FACE_TIMESTAMP, timestamp)
}
// fun savePlateData(items: List<EquipmentUserInfo>) {
// val jsonData = Gson().toJson(items)
// 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)
}
var baseUrl: String
get() = spUtil?.get(GlobalKey.KEY_BASE_URL, "") ?: ""
set(value) {
spUtil?.put(GlobalKey.KEY_BASE_URL, value)
}
}
@@ -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,152 @@
package com.sw.platecabinet.utils.mego
import android.content.Context
import android.os.Handler
import android.os.Looper
import com.megoai.plc_aar.SDK
/**
* PLC 操作工具类(单例)
* 封装 SDK 所有接口,统一回调到主线程
*/
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 }
}
}
}
// ==================== 连接与模式 ====================
/**
* 打开出盘串口(应用启动时调用)
*/
fun openSerialPort(callback: PlcCallback<String>) {
sdk.openPlcSerialPort { result ->
mainHandler.post { callback.onSuccess(result.toString()) }
}
}
/**
* 打开自动模式(应用启动时调用一次)
*/
fun openAutoMode(callback: PlcCallback<String>) {
sdk.actionOpenAutoMode { result ->
mainHandler.post { callback.onSuccess(result.toString()) }
}
}
/**
* 故障复位(出现 alarmCode 或补盘完成后调用)
*/
fun alarmReset(callback: PlcCallback<String>) {
sdk.actionAlarmReset { result ->
mainHandler.post { callback.onSuccess(result.toString()) }
}
}
// ==================== 出盘操作 ====================
/**
* 出盘
*/
fun outPlate(callback: PlcCallback<String>) {
sdk.actionOutPlate { result ->
mainHandler.post { callback.onSuccess(result.toString()) }
}
}
// ==================== 平台控制 ====================
/**
* 平台下降(开始补盘)
*/
fun platformDown(callback: PlcCallback<String>) {
sdk.actionPlatformDown { result ->
mainHandler.post { callback.onSuccess(result.toString()) }
}
}
/**
* 平台上升(补盘完成)
*/
fun platformUp(callback: PlcCallback<String>) {
sdk.actionPlatformDownStop { result ->
mainHandler.post { callback.onSuccess(result.toString()) }
}
}
/**
* 补盘完成返回营业(自动模式 + 故障复位)
*/
fun backToBusiness(callback: PlcCallback<String>) {
sdk.actionOpenAutoModeAndActionAlarmReset { result ->
mainHandler.post { callback.onSuccess(result.toString()) }
}
}
// ==================== 状态查询 ====================
/**
* 获取 PLC 状态(解析为 PlcStatus 对象)
*/
fun getStatus(callback: PlcCallback<PlcStatus>) {
sdk.getPlcStatus { json ->
mainHandler.post {
if (json != null) {
callback.onSuccess(PlcStatus.Companion.fromJson(json.toString()))
} else {
callback.onError("获取状态失败")
}
}
}
}
/**
* 获取 PLC 状态(原始 JSON 字符串)
*/
fun getStatusJson(callback: PlcCallback<String>) {
sdk.getPlcStatus { json ->
mainHandler.post {
if (json != null) callback.onSuccess(json.toString())
else callback.onError("获取状态失败")
}
}
}
// ==================== 参数设置 ====================
/**
* 设置 PLC 参数
* @param paramType 参数类型(如 "18FD" 表示主电机运行最长时间)
* @param paramValue 参数值(int,单位 0.1s
*/
fun setParams(paramType: String, paramValue: Int, callback: PlcCallback<String>) {
sdk.setPLCParams(
{ result -> mainHandler.post { callback.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()
}
}
}
}
@@ -0,0 +1,91 @@
package com.sw.platecabinet.view;
import android.app.Dialog;
import android.content.Context;
import android.content.res.Resources;
import android.os.Build;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
/**
* 加载进度弹窗
*/
public class CustomLoadingDialog extends Dialog {
/**
* 宽高由布局文件中指定(但是最底层的宽度无效,可以多嵌套一层解决)
*/
public CustomLoadingDialog(Context context, View layout, int style) {
super(context, style);
setContentView(layout);
Window window = getWindow();
WindowManager.LayoutParams params = window.getAttributes();
params.gravity = Gravity.CENTER;
window.setAttributes(params);
}
/**
* 宽高由该方法的参数设置
*/
public CustomLoadingDialog(Context context, int width, int height, View layout,
int style) {
super(context, style);
// 设置内容
setContentView(layout);
// 设置窗口属性
Window window = getWindow();
WindowManager.LayoutParams params = window.getAttributes();
// 设置宽度、高度、密度、对齐方式
float density = getDensity(context);
params.width = (int) (width * density);
params.height = (int) (height * density);
params.gravity = Gravity.CENTER;
window.setAttributes(params);
}
/**
* 获取显示密度
*
* @param context
* @return
*/
public float getDensity(Context context) {
Resources res = context.getResources();
DisplayMetrics dm = res.getDisplayMetrics();
return dm.density;
}
private void fullScreenImmersive(View view) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_FULLSCREEN;
view.setSystemUiVisibility(uiOptions);
}
}
@Override
public void show() {
this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
super.show();
// fullScreenImmersive(getWindow().getDecorView());
// this.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
}
}
@@ -0,0 +1,74 @@
package com.sw.platecabinet.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.model.response.ApiResponse
import com.sw.platecabinet.network.ApiClient
import com.sw.platecabinet.repository.RemoteRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
abstract class BaseViewModel() : ViewModel() {
protected val repository = RemoteRepository(ApiClient.apiService)
// 显示进度条
private val _showLoading = MutableStateFlow<Boolean>(false)
val showLoading: StateFlow<Boolean> = _showLoading
/**
* 带进度条的请求
*/
protected fun launchWithLoading(block: suspend () -> Unit) {
viewModelScope.launch {
try {
_showLoading.value = true
block()
} catch (e: Exception) {
// 错误处理可被子类重写
handleError(e)
} finally {
_showLoading.value = false
}
}
}
/**
* 不带进度条的请求
*/
protected fun launch(block: suspend () -> Unit) {
viewModelScope.launch() {
try {
block()
} catch (e: Exception) {
// 错误处理可被子类重写
handleError(e)
}
}
}
protected open fun parseResponse(response: ApiResponse<*>): Boolean {
if (response.isSuccess()) {
return true
}
Timber.d("msg = ${response.msg}, code = ${response.code}")
ToastUtils.showToast("${response.msg}(${response.code})")
return false
}
fun handleError(exception: Exception) {
Timber.d("handleError ${exception.message}")
}
// fun parseEquipmentInfo(equipmentInfo: EquipmentInfo) {
//// GlobalData.appBaseUrl = equipmentInfo.appPackageUrl!!
//// GlobalData.appBaseUrl="https://vip.shuziweidao.com"
//// GlobalData.sdkKey = equipmentInfo.arcsoftSdkKey!!
//// GlobalData.appId = equipmentInfo.arcsoftAppId!!
//// GlobalData.activeKey = equipmentInfo.arcsoftActiveKey!!
//// GlobalData.restId = equipmentInfo.canteenId!!
// }
}
@@ -0,0 +1,68 @@
//package com.sw.platecabinet.viewmodel
//
//import com.sw.inbound.utils.GsonUtils
//import com.sw.inbound.utils.SPUtil
//import com.sw.platecabinet.GlobalData
//import com.sw.platecabinet.GlobalKey
//import com.sw.platecabinet.model.response.EquipmentInfo
//import kotlinx.coroutines.flow.MutableStateFlow
//import kotlinx.coroutines.flow.StateFlow
//import timber.log.Timber
//import kotlin.jvm.java
//
///**
// * 初始化的viewmodel
// */
//class DeviceViewModel : BaseViewModel() {
// private val _deviceInfoResult = MutableStateFlow<Boolean?>(null)
// val deviceInfoResult: StateFlow<Boolean?> = _deviceInfoResult
//
// /**
// * 获取token
// */
// fun getDeviceToken(deviceId: String = GlobalData.deviceId) {
// launchWithLoading {
// val response = repository.getDeviceToken(deviceId)
// if (parseResponse(response)) {
// val response1 = repository.getDeviceInfo(deviceId, response.result!!)
//
// if (parseResponse(response1)) {
// val equipmentInfo = response1.result
// if (equipmentInfo == null) return@launchWithLoading
// try {
// parseEquipmentInfo(equipmentInfo)
// SPUtil.getInstance()
// .put(GlobalKey.KEY_EQUIPMENT_INFO, GsonUtils.toJson(equipmentInfo))
// _deviceInfoResult.value = true
// } catch (e: Exception) {
// Timber.e(e)
// }
// }
// }
// }
// }
//
// /**
// * 检查缓存数据
// */
// fun checkEquipmentInfo(): Boolean {
// val equipmentInfoStr = SPUtil.getInstance().get(GlobalKey.KEY_EQUIPMENT_INFO, "")
// if (equipmentInfoStr == null) {
// Timber.e("获取缓存设备信息失败")
// return false
// }
// val equipmentInfo =
// GsonUtils.fromJson<EquipmentInfo>(equipmentInfoStr, EquipmentInfo::class.java)
// if (equipmentInfo == null) {
// Timber.e("解析缓存设备信息失败")
// return false
// }
// try {
// parseEquipmentInfo(equipmentInfo)
// return true
// } catch (e: Exception) {
// Timber.e(e)
// return false
// }
// }
//}
@@ -0,0 +1,62 @@
//package com.sw.platecabinet.viewmodel
//
//import com.sw.platecabinet.model.response.UserFaceModel
//import kotlinx.coroutines.Dispatchers
//import kotlinx.coroutines.withContext
//import java.io.File
//
///**
// * 管理员操作viewmodel
// */
//class SettingViewModel : BaseViewModel() {
//
// companion object {
// const 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)
// }
// }
//
//}
@@ -0,0 +1,201 @@
package com.sw.platecabinet.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.entity.FaceEntity
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.GlobalKey
import com.sw.platecabinet.model.DeviceConfig
import com.sw.platecabinet.model.response.UserFaceModel
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 UserViewModel : BaseViewModel() {
companion object {
const val PAGE_SIZE = 100
}
private val faceApi: FaceApi = FaceApi()
fun getUserFace(
pageNo: Int,
pageSize: Int,
callback: (List<UserFaceModel>) -> Unit
) {
launchWithLoading {
val response = repository.getUserFaceCache(pageNum = pageNo, pageSize = pageSize)
val status = parseResponse(response)
if (status) callback(response.data ?: emptyList())
else emptyList<UserFaceModel>()
}
}
/**
* 获取人脸数据
*/
fun getUserFaceCache(
pageNo: Int = 1,
pageSize: Int = PAGE_SIZE,
callback: (Boolean, String?) -> Unit = { statue, msg -> }
) {
var currentPageNo = pageNo
launchWithLoading {
val response = repository.getUserFaceCache(pageNum = pageNo, pageSize = pageSize)
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) "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)) {
block(response.data)
} else {
block(null)
}
}
}
/**
* 获取人脸数据
*/
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)
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 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
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#000000" /> <!-- 整体黑色背景 -->
<stroke
android:width="2dp"
android:color="#333333" /> <!-- 边框颜色和宽度 -->
<corners android:radius="12dp" /> <!-- 圆角半径 -->
</shape>
Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

@@ -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="#ff08C6DC" />
<corners android:radius="8dp" />
</shape>
</item>
</ripple>
+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:width="1920px"
android:height="1080px">
<bitmap
android:scaleType="centerCrop"
android:src="@drawable/bg_collect_page3" />
</item>
<item
android:width="506dp"
android:height="483dp"
android:gravity="top|center_horizontal"
android:top="150dp">
<bitmap
android:scaleType="fitCenter"
android:src="@drawable/img_init" />
</item>
</layer-list>

Some files were not shown because too many files have changed in this diff Show More