Author SHA1 Message Date
mazengfei a769eae2a0 fix(global): 修正生产环境接口地址
- 更新生产环境基础URL地址为新的测试平台地址
- 注释掉旧的生产环境地址备份记录
- 保持开发和测试环境地址不变
- 确保接口调用指向正确的服务器环境
2026-08-10 14:01:38 +08:00
mazengfei 0241caa5fc fix(main): 修复计算吃饭数量返回值小于1的问题
- 修改返回值逻辑,当计算结果小于1时返回1
- 防止吃饭数量为0或负值导致的异常情况
- 优化代码可读性,提升鲁棒性
2026-08-05 17:57:38 +08:00
mazengfei 512e0b5b4f feat(weightBilling): 优化按重量计费逻辑并完善界面展示
- 规格栏显示改为直接展示规格重量,去除单价/100克显示
- 新增最小计费重量常量 MIN_BILLABLE_WEIGHT,净重不足时阻止下单
- 下单时检测净重,低于最小计费重量时弹出提示并拦截
- 计算价格时按净重计算,单价基于规格价与规格重量的比值
- 本地明细中 eatNum 仅用于展示份数,不参与金额计算
- SettlementOrder 用实际食用重量 eatWeight 赋值 num 字段,配合重量栏显示逻辑调整
2026-08-05 10:23:48 +08:00
mazengfei a8ade16058 fix(MainScreenPresentation): 修正取餐重量计算逻辑
- 去除净重计算,改以实际重量做后续份数、价格、总重量计算
- 修正计价和取餐份数计算中使用的重量参数
- 调整本地数据订单中数量赋值,使用实际重量替代净重
- 在营养计算和柱状图绘制中扣减餐具重量,确保显示净重
- 保持取餐模式余量计量逻辑兼容,优化逻辑判断条件
2026-07-28 09:18:36 +08:00
mazengfei ffd00266b8 fix(recognition): 防止识别时重复触发导致多次并发处理
- 新增 isRecognizing 标记防止识别过程重复进入
- 在识别开始时设置标记,结束时释放标记
- 多处识别终止和异常处理处均释放识别锁
- 在重量回调中增加去重逻辑,避免同一重量重复触发回调
- 更新 lastCallbackWeight,确保仅在重量变化时触发回调
2026-07-24 16:54:19 +08:00
mazengfei 43f5434cbd feat(business): 增加餐具重量配置及扣减功能
- 在 BusinessFragment 中添加实时秤重读取及餐具重量持久化存储
- 在界面显示餐具重量标签,配置时显示当前保存值
- 定义全局常量 KEY_DISH_WEIGHT 用于存储餐具重量
- 在 MainActivity 内部方法添加获取配置餐具重量接口
- 修改订餐逻辑,取餐提交时自动从就餐重量中扣减餐具重量
- 在主界面相关订单及计价逻辑中扣减餐具重量影响份数和价格
- 调整重量监听及订单添加部分逻辑以应用净重计算
- 更新定时任务延迟时间,提高效率和响应速度
2026-07-24 15:56:08 +08:00
mazengfei 76f60f8ee7 fix(build): 修复ObjectBox库的release依赖配置
- 取消注释并激活app模块中ObjectBox的releaseImplementation依赖
- 取消注释并激活lib_face模块中ObjectBox的releaseImplementation依赖

fix(ui): 修复异常时Toast显示的线程调度问题

- 在CollectFragment中使用lifecycleScope切换到主线程显示Toast

refactor(main): 优化图像处理流程中的协程使用

- MainActivity中将uri2File调用放入IO线程的lifecycleScope中
- 使用withContext切换至IO线程异步获取Bitmap后再进行处理

chore(objbox): 移除未使用的ObjectBoxLiveData导入声明
2026-07-17 09:53:43 +08:00
mazengfei 28a475ca43 feat(face): 优化人脸数据同步及类型映射
- 新增接口注释,完善全量及增量人脸数据获取说明
- FaceVO中faceUpdateTimestamp由Long改为String以防JS大数精度丢失
- 增加personType字段标识人员类型
- MainActivity保存人脸数据时映射完整字段,使用personType优先判断用户类型
- NetViewModelV2中获取人脸数据时改用完整构造函数创建FaceEntity
- 累积更新lastFaceTimestamp为当前页最大时间戳,避免时间戳遗漏
- 优化时间戳更新逻辑,避免直接使用列表最后一条数据
2026-07-15 10:56:40 +08:00
mazengfei 16dba32eb7 feat(face): 升级虹软SDK到5.0及完善人脸识别逻辑
- FaceHelper中集成独立口罩检测引擎maskEngine并调用口罩检测接口
- FaceEngine初始化中增加口罩检测引擎初始化及状态管理
- FaceServer注册人脸时打印特征信息日志,便于调试
- CompareResult新增similarPass字段,标记识别是否通过
- FaceApi新增针对人脸数据的增删查接口,方便管理
- FaceDatabase数据库版本升级至2,支持人脸特征库重建迁移
- FaceDao新增多用户查询、人脸计数及临时用户数据删除接口
- FaceEntity新增userId、userFaceId、member标记及更新时间字段及相关方法
- 优化人脸识别失败重试机制,连续失败后回调识别失败结果
- 优化识别回调逻辑,成功与失败结果均及时通知观察者
- 调整配置获取逻辑,禁用人脸活体检测时关闭相关初始化
- 精简并修正部分冗余代码与日志输出,提升代码清晰度与可维护性
2026-07-15 09:17:23 +08:00
mazengfei 5a7b090c02 fix(ml): 优化PyTorch模型推理流程与资源管理
- 修改API接口路径从serve切换到common,统一资源调用
- 增加独立推理线程,隔离ArcSoft线程污染,保障FPU状态稳定
- 推理模块预热,降低PyTorch线程初启功率峰值,避免电流保护触发
- 推理前后记录系统内存信息,辅助OOM重启排查
- 主屏相机与副屏人脸识别推理前暂停,规避硬件并发带宽冲突重启
- 控制相机HAL清理时长,确保DMC带宽资源释放后再启动推理
- 异步推理调用增加超时机制,避免长时间卡死
- 推理结束后恢复相机,确保正常工作流程
2026-07-08 16:23:37 +08:00
lvmeng b6ed47ff87 fix(camera): 解决相机拍照功能的竞态条件和重试机制问题
- 修复了相机回调中的竞态条件,使用isShowCamera标识查找空闲槽位
- 将图片处理逻辑移至IO线程,避免主线程阻塞
- 添加了照片拍摄失败回调处理函数
- 简化了相机拍照调用方式,直接传递回调函数
- 在FoodCollectionAdapter中使用Glide加载图片uri
- 添加了相机绑定失败时的自动重试机制,包含延迟重试逻辑
- 增加了详细的错误日志记录和异常处理
2026-06-30 15:54:45 +08:00
lvmeng 088aabc535 refactor(discount): 简化会员折扣空值处理逻辑
- 使用Elvis操作符替代条件表达式判断空值
- 统一两个位置的折扣计算逻辑实现方式
- 提高代码可读性和简洁性
2026-06-30 09:51:35 +08:00
mazengfeiandClaude Opus 4.6 061680b32b fix(objectbox): 修复32位系统向量查询崩溃,统一使用findIdsWithScores避免加载大对象
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-04 17:26:36 +08:00
mazengfei 5c3e51626c fix(network): 修正人脸识别接口路径并移除硬编码的SDK密钥
- 修复 ApiClient 中的人脸识别增量同步接口路径
- 将 InitActivity 中的 ArcSoft SDK 密钥配置改为注释状态
- 移除临时测试用的硬编码 SDK 配置逻辑
- 调整代码执行顺序以确保正确的初始化流程
2026-06-04 11:19:50 +08:00
lvmeng 936bf206dc ```
refactor(env): 调整环境配置和切换逻辑

- 将本地环境配置从注释状态启用,并更新对应的IP地址和端口
- 修改环境切换对话框中的文本标签,重新排列环境选项顺序
- 更新全局数据类中的环境URL常量定义,调整测试和UAT环境地址
- 移除未使用的SPUtil导入,改用SpTool工具类
- 修改应用启动时的默认环境判断逻辑,将特定设备指向本地环境
- 调整环境切换对话框中的RadioButton选中状态和URL映射关系
```
2026-06-03 16:34:16 +08:00
lvmeng 8e4dcb9f61 fix(arcface): 禁用活体检测功能
- 将enableLive变量强制设置为false以禁用活体检测
- 移除注释掉的调试代码行
2026-06-03 16:32:39 +08:00
lvmeng 8ba4ba6db0 feat(order): 开餐下单新增cookOrderId支持用于溯源追踪
- 在FoodInfo数据模型中新增cookOrderId字段
- 在NewFoodInfo数据模型中新增cookOrderId字段并实现映射
- 在PlaceOrderRequest请求模型中新增cookOrderId参数
- 在订单提交流程中传递cookOrderId参数
- 实现菜品列表到就餐环节的ID回传机制
2026-06-02 09:07:21 +08:00
lvmeng 37f86e202f refactor(camera): 优化相机拍照功能实现
- 移除 PhotoCaptureHelper 中的测试模式和无用的 onSuccess 回调参数
- 将 CollectFragment 中的 UI 操作迁移到正确的协程调度器 (Main 和 IO)
- 使用 lifecycleScope 替代 runOnUiThread 确保生命周期安全
- 重构 ImageUtil.uriToBitmap 方法,添加图片尺寸压缩逻辑以减少内存占用
- 从 InitActivity 中移除已注释的人脸识别相关初始化代码
- 修复 cameraCallback 中的拼写错误 (rerurn@ -> return@)
2026-05-29 13:52:07 +08:00
lvmeng 737baf8486 feat(api): 更新API端点并集成人脸数据库导入功能
- 将所有API端点中的/booth路径替换为/serve
- 新增searchFood API用于菜品搜索功能
- 新增getCollectVectorPage API用于向量数据分页获取
- 添加FaceDbImporter工具类实现从assets导入人脸数据
- 在初始化活动中集成人脸数据导入功能
- 更新订单创建逻辑适配新的API参数结构
- 重构菜品搜索和订单处理相关API调用
- 添加V2数据模型到V1模型的转换扩展函数
- 更新绑定订单接口以使用新的API端点
- 添加测试模式支持以跳过实际拍照操作
2026-05-28 19:57:40 +08:00
lvmeng bf6b2547f3 refactor(network): 重构网络请求配置和API接口实现
- 将BASE_URL改为使用GlobalData.PROD_BASE_URL常量
- 更新ApiClient中的URL匹配规则以支持新的营养模块接口
- 将ApiServiceV2中的接口路径从/neglect/booth改为/nutrition/neglect/booth
- 使用@Url注解动态传入完整URL地址
- 在CollectedFoodActivity中切换到NetViewModelV2和新API模型
- 更新采集功能的数据模型和接口调用方式
- 调整人脸数据获取逻辑以支持增量和全量同步
- 优化文件上传和删除操作的参数传递方式
2026-05-28 15:14:44 +08:00
lvmeng 6abd2f9b0b feat(network): 添加新系统API V2支持及相关数据模型
- 引入 ApiServiceV2 接口定义新系统API端点
- 添加 RemoteRepositoryV2 实现新API的数据访问层
- 创建 NetViewModelV2 提供新API的业务逻辑处理
- 定义 v2 包下的数据模型类包括请求响应对象
- 在 ApiClient 中集成新旧两套API服务实例
- 配置API日志拦截器识别新API调用并打上ApiV2标签
- 实现新系统菜品、人脸、订单、会员等完整功能接口
2026-05-28 09:33:05 +08:00
lvmeng c19d84a7a6 refactor(init): 优化结算模式配置逻辑
- 移除未使用的变量声明
- 将结算模式设置改为不可变值
- 简化支付类型判断逻辑
- 保持结算模式存储功能不变
2026-05-26 11:49:23 +08:00
lvmeng 8f1f15dd74 feat(payment): 添加会员折扣功能并重构支付逻辑
- 新增 getMemberDiscount 接口用于查询会员折扣
- 在人脸识别支付中集成会员折扣计算逻辑
- 将支付金额计算从 vipPrice 切换到 specPrice 并应用会员折扣
- 在初始化活动中添加设备配置获取功能
- 使用 ConstraintLayout 重构主餐品列表布局
- 添加 Timber 日志框架替换原有 Log 工具
- 重构 MainScreenPresentation 中的食物价格计算逻辑
- 移除重复的设备配置获取代码,统一在 InitActivity 中处理
2026-04-27 17:52:45 +08:00
lvmeng 416a8366e1 feat(MainScreenPresentation): 添加布局隐藏功能以支持结算模式
- 导入 invisible 扩展函数
- 在特定结算模式下隐藏 eat detail 布局
- 实现相机恢复后的条件性视图控制
2026-04-24 15:41:21 +08:00
lvmeng 03c5aee785 refactor(ui): 重构主界面食物列表布局和支付模式切换功能
- 将原有的RecyclerView替换为FrameLayout容器,实现动态布局加载
- 新增layout_main_food_list.xml和layout_main_food_list2.xml两个布局文件
- 实现联合支付和独立支付两种模式下的不同UI展示
- 集成食物详情显示功能,通过include方式引入layout_eat_detail.xml
- 更新数据绑定逻辑,支持两种支付模式下的食物列表显示
- 迁移SharedPreferences操作至SpTool工具类统一管理
- 优化食物识别结果显示逻辑和搜索按钮状态控制
- 重构RecyclerView初始化和适配器设置代码结构
2026-04-24 15:37:57 +08:00
mazengfei e3b01ff789 修改uat环境域名端口 2026-04-15 16:52:27 +08:00
mazengfeiandClaude Sonnet 4.6 b6c9fe5aed feat(log): 优化日志写入机制,改用内部存储与单线程执行器
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 17:23:50 +08:00
lvmeng 45562a9fc1 fix(order): 解决订单生成后餐品未拿走的检测逻辑
- 添加abs函数导入用于计算重量绝对值
- 在订单生成成功后增加重量检测逻辑
- 当称重绝对值大于10克时认为餐品未被拿走
- 添加详细的调试日志记录重量信息
- 集成FileUtil工具类用于保存操作日志
- 在人脸数据获取流程中添加日志记录时间戳变化
2026-04-14 15:19:09 +08:00
lvmeng 7f455f2d8f perf(FoodModule): 提高食物查询性能并优化排序逻辑
- 将默认查询数量从15增加到50
- 移除不必要的分数阈值过滤条件
- 移除复杂的按名称分组和计数逻辑
- 简化排序算法,直接按分数排序
- 添加调试日志输出结果数据
2026-04-13 17:20:51 +08:00
mazengfei dde0bbc4d5 优化余量计量模式下拿完所有数据时总重量不对的问题 2026-04-10 17:48:40 +08:00
lvmeng d378cdcf97 fix(main): 修复菜品切换和重量计算相关问题
- 移除未使用的LinearLayoutManager和FoodOrderAdapter导入
- 添加isSwitchFood标志用于菜品切换时重置数据
- 优化resetSelectedFood方法,根据用户状态决定是否重新打开识别功能
- 将currentWeight修改为public访问权限
- 在重量变化时添加判断避免重复更新
- 注释掉不需要的重量更新调用
- 在菜品识别完成时添加重量更新
- 在清空选中项目时重置isSwitchFood标志
- 优化updateWeight方法中的逻辑判断
- 将倒计时重置任务提取为单独方法
- 添加switchFood逻辑控制,避免重复人脸识别
- 修复余量计量模式下的最后餐品处理逻辑
2026-04-10 16:03:27 +08:00
lvmeng e5b682f021 refactor(face): 优化人脸识别状态管理与菜品选择逻辑
- 移除 MainFoodListAdapter 中未使用的适配器和视图参数
- 提取菜品重选逻辑到独立的 resetSelectedFood 方法中
- 删除临时的独立支付模式测试代码
- 在余量计量场景中添加人脸状态检查避免重复数据读取
- 修复倒计时任务状态判断逻辑并清理任务引用
- 添加空值检查防止 CompareResult 为空时的异常
- 实现 resetFaceState 方法清空人脸识别状态
- 在订单提交前重置人脸状态确保下次识别正常进行
2026-04-09 16:02:24 +08:00
lvmeng 0498e124b3 fix(weight): 修复称重逻辑和支付模式相关问题
- 将 WEIGHT_CHANGE_VALUE 从 25 修改为 20
- 添加 currentWeight 变量用于准确记录当前称重值
- 临时修改 settlementMode 为独立支付模式用于测试
- 更新日志输出以更好地跟踪称重变化
- 修复即放即取和余量计量模式下的称重处理逻辑
- 添加食物识别状态控制变量 foodRecognizeState
- 在不同模式下正确设置 eatWeight 和 foodWeight 计算方式
- 优化余量计量模式下的状态重置逻辑
- 添加倒计时功能用于余量计量模式状态重置
- 修复营养计算相关的日志输出格式
2026-04-09 11:59:25 +08:00
lvmeng 30b2b178a9 refactor(SensorScaleUtils): 优化重量读取逻辑
- 移除状态字符串转换的日志代码
- 简化稳定状态判断逻辑,移除重复的lastWeight比较
- 将重量单位转换提取为独立变量,提高代码可读性
- 保持原有的回调机制和重量单位转换功能
2026-04-08 10:59:07 +08:00
lvmeng 2ba55ef027 feat(common): 添加View点击区域扩展功能并优化主界面重量检测逻辑
- 在CommonExt.kt中新增expandClickArea扩展函数,支持扩大View的点击响应区域
- 新增TouchDelegate导入用于实现点击区域扩展功能
- 调整MainActivity.kt中的重量检测逻辑顺序,先处理联合支付模式下的特殊情况
- 添加对当前用户ID的检查,优化即放即取模式下的重量数据处理流程
- 重构重量重置识别条件的位置,确保正确的执行顺序
2026-04-08 10:52:09 +08:00
lvmengandClaude Sonnet 4.6 a45690d3be refactor(utils): 重构 FoodVectorTool 并优化 zero() 提示控制
- 重构 FoodVectorTool:移除单例 pageNum 状态污染问题,分页逻辑下沉为私有 fetchPage,saveFoodVector 改为私有,PAGE_SIZE 移入内部定义,对外统一暴露 loadAndSaveFoodVector 接口
- 更新 InitActivity、MainActivity 调用方,移除重复的 onPageFinish 回调
- SensorScaleUtils.zero() 新增 isShowToastRemind 可选参数,支持按需显示标定成功提示

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 09:25:03 +08:00
lvmeng ab0d9b35f5 feat(data): 更新食物营养数据模型并优化向量数据处理
- 在DataBean中将肉蛋相关字段重命名为肉蛋豆,并添加总重量字段
- 新增FoodVectorTool工具类用于处理食物向量数据的分页获取和存储
- 重构InitActivity中的食物向量数据获取逻辑,使用新的工具类替代原有实现
- 在MainActivity中集成食物向量数据同步功能,在清除本地数据后重新获取
- 更新MainScreenPresentation中营养信息显示逻辑,适配新的数据模型结构
- 修改ApiClient中的日志标记逻辑,为不同接口添加特定的标签
- 调整食物订单列表的数据更新方式,优化适配器通知策略
2026-04-03 18:04:59 +08:00
mazengfeiandClaude Sonnet 4.6 0127697560 fix(main): 优化重量识别逻辑与日志;防抖时间调整为2s;从版本控制中移除.idea和objectbox-models
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:09:14 +08:00
mazengfei dbe3753821 人脸数据重置后加刷新逻辑;人脸识别距离调整; 2026-04-03 10:40:57 +08:00
lvmeng b16147b1b2 feat(face): 添加人脸识别距离过滤功能
- 在FaceHelper中添加人脸宽度小于200像素时的过滤逻辑
- 当检测到人脸过小时自动清除左侧人脸数据
- 防止远距离小人脸影响识别准确性
- 保留原有的活体检测流程不变
2026-04-02 19:13:15 +08:00
lvmeng d6528d33fc refactor(MainActivity): 将人脸数据清除操作移至后台线程执行
- 使用 lifecycleScope.launch 在协程中执行人脸数据清除
- 通过 withContext(Dispatchers.IO) 确保清除操作在 IO 线程运行
- 避免主线程阻塞提升界面响应性能
- 保持原有的用户提示和缓存更新逻辑不变
2026-04-02 17:45:41 +08:00
lvmeng 5479091959 refactor(main): 将Thread替换为lifecycleScope并优化协程处理
- 替换CollectFragment中的Thread为activity.lifecycleScope.launch
- 在MainActivity中添加FaceServer相关依赖和协程调度器
- 修改环境切换对话框以支持人脸数据重置功能
- 将照片拍摄和食物向量保存操作从Thread迁移到lifecycleScope
- 优化人脸识别数据更新逻辑,移除不必要的线程创建
- 修改onTakePhotoSuccess方法为suspend函数并调整UI更新方式
- 更新主线程操作使用withContext(Dispatchers.Main)替代runOnUiThread
- 在MainScreenPresentation中添加相机暂停功能
- 修复SensorScaleUtils中标定调用问题
- 扩展UserViewModel的人脸数据获取接口,支持回调处理
2026-04-02 17:35:37 +08:00
mazengfei 12da153df3 优化 2026-04-01 17:11:00 +08:00
mazengfei 3ef93c5f5e 配置生产域名 2026-03-31 19:08:39 +08:00
lvmeng 91800a6bb1 feat(app): 添加环境切换功能并优化基础URL管理
- 在主界面标题时间区域添加点击事件,支持弹出环境切换对话框
- 新增EnvSwitchDialog类实现TEST/UAT/PROD三套环境切换功能
- 新增dialog_env_switch.xml布局文件定义环境切换弹窗UI
- 重构GlobalData类,统一管理基础URL常量并添加持久化支持
- 修改MyApp初始化逻辑,优先读取用户手动设置的环境配置
- 添加SpTool.baseUrl属性用于环境配置的本地存储
- 修复MainActivity布局文件中的TextView垂直内边距问题
- 调整食物识别防抖时间从500ms到3000ms,优化计算营养逻辑执行时机
2026-03-31 09:48:32 +08:00
mazengfei f40c2046f1 优化 2026-03-30 18:31:24 +08:00
mazengfei ee9f27bb9c 优化 2026-03-30 18:03:01 +08:00
lvmeng 5c230e38c3 refactor(activity): 重构 BaseActivity 权限管理和页面跳转功能
- 在 BaseActivity 中新增权限请求和页面跳转的通用方法
- 移除 InitActivity 中原有的相机权限请求相关代码
- 使用 BaseActivity 的通用权限请求方法替代原有实现
- 添加网络连接检测工具类 NetworkUtils
- 在 WiFi 设置页面返回后重新检查网络连接状态
- 优化按钮可见性控制逻辑
2026-03-30 14:01:13 +08:00
lvmeng 10d1f966da 配置接口新增结算模式字段;优化页面刷新; 2026-03-27 17:21:45 +08:00
lvmeng 57f8920442 优化订单显示档口机标识;优化当前菜品信息的问题; 2026-03-26 18:57:20 +08:00
lvmeng fb4278d2b7 联合支付余量计量模式订单显示当前取餐信息 2026-03-25 15:42:22 +08:00
lvmeng fcea0d785d 联合支付功能测试 2026-03-24 18:34:38 +08:00
lvmeng c4a6101a7b 联合支付功能调试 2026-03-23 18:13:55 +08:00
lvmeng bf8444e5d5 联合支付功能调试 2026-03-20 18:06:22 +08:00
lvmeng 549e8d303f 联合支付功能 2026-03-19 17:44:13 +08:00
lvmeng b51be58c64 联合支付模式使用人脸识别生成就餐记录 2026-03-19 14:19:20 +08:00
lvmeng a6d2441704 优化:修复眼睛图标显示效果 2026-03-18 16:14:47 +08:00
lvmeng e7d11530a6 优化:修改消费码输入框为密码模式,添加显示/隐藏密码切换功能 2026-03-18 15:55:06 +08:00
lvmeng c56769769b 优化:修改消费码输入框为密��模式,添加显示/隐藏密码切换功能 2026-03-18 15:42:50 +08:00
lvmeng 8673cec545 优化:修改人脸识别任务延迟参数和添加相机权限检查 2026-03-18 14:06:42 +08:00
lvmeng 8ec7ea6a79 优化 2026-03-13 15:42:37 +08:00
lvmeng a80e3d1ba6 ObjectBox数据库操作增加线程资源清理 2026-03-12 17:25:36 +08:00
mazengfei 6d825d7235 扫码支付增加会员id字段 2026-03-12 12:39:35 +08:00
lvmeng 4f1860eb37 存在待支付订单时停止识别 2026-03-12 11:43:20 +08:00
mazengfei 45a7bf8369 优化非支付页面扫码点击问题 2026-03-11 18:34:49 +08:00
lvmengandClaude Sonnet 4.6 f0b50ef82f 修复MainActivity中扫码枪按键事件触发页面按钮点击问题
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 14:54:52 +08:00
lvmeng 824dc5f867 优化扫码支付结果会员非会员显示 2026-03-11 13:55:29 +08:00
lvmengandClaude Sonnet 4.6 7232735073 修复MainActivity停止时CameraX NPE崩溃
1. 升级CameraX版本 1.3.0 → 1.4.1,从根本修复RequestWithCallback.abort() NPE
2. 跳转PayActivity前调用shutdownCamera(),防止in-flight拍照请求触发生命周期异常
3. onResume()顶部增加相机恢复逻辑,通过cameraProviderFuture==null判断是否需要重新初始化
4. shutdownCamera()中将cameraProviderFuture置null,作为相机已停止的可靠标志

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 19:05:28 +08:00
mazengfei 8a1630f2a4 优化扫码枪支付 2026-03-10 14:44:34 +08:00
lvmeng 39c0ca7d72 优化人脸识别区域效果 2026-03-10 11:10:23 +08:00
lvmeng dfde8c0fad 人脸全量接口只首次请求 2026-03-10 10:53:18 +08:00
lvmeng aca84f1e94 Rename .java to .kt 2026-03-10 10:53:18 +08:00
lvmeng 7bcb74d9e0 增加支付成功语音提示;增加扫码支付; 2026-03-10 09:41:34 +08:00
mazengfei 6c857dd1f7 优化 2026-03-09 21:59:04 +08:00
mazengfei fd0f9977b1 使用人脸接口中的member字段判断是否会员;逻辑优化; 2026-03-09 21:26:02 +08:00
lvmeng ec4a5801c2 支付页面统一副屏逻辑 2026-03-09 19:56:09 +08:00
lvmeng 59b3ad72fe 优化人脸识别显示形状 2026-02-11 17:00:08 +08:00
lvmeng 462755824d 优化 2026-02-11 15:00:27 +08:00
lvmeng 0c990ea7ee 打开增加检查网络情况 2026-02-11 14:48:30 +08:00
lvmeng ac30d95812 优化人脸识别区域 2026-02-10 09:48:26 +08:00
lvmeng 23546a967b 首页人脸识别区域改为圆形 2026-02-09 18:47:53 +08:00
lvmeng f227d15108 菜品信息查询接口增加deviceType字段 2026-02-06 11:20:58 +08:00
lvmeng e2c0ed35ec 优化识别及增加扫码枪逻辑 2026-02-04 18:00:46 +08:00
lvmeng 7596924d75 优化 2026-02-03 15:00:19 +08:00
lvmeng 6e7eb80715 优化 2026-01-26 15:53:43 +08:00
lvmeng 50d12de534 优化 2026-01-14 10:38:09 +08:00
lvmeng e86db688e1 优化 2026-01-13 14:26:18 +08:00
lvmeng c38bead8a3 优化 2026-01-13 09:30:12 +08:00
lvmeng ebc3d856d6 优化 2026-01-09 18:38:55 +08:00
lvmeng a6fa5f153a 优化 2026-01-09 16:10:57 +08:00
lvmeng a0452e985a 优化不足一份的识别问题 2026-01-09 11:59:24 +08:00
lvmeng a79f7db867 优化 2026-01-08 19:36:31 +08:00
马增飞 fce721bfc3 优化 2026-01-08 17:31:13 +08:00
马增飞 3cccba4f08 优化 2026-01-08 17:30:03 +08:00
lvmeng 2a55ddb617 采集向量优化调试 2026-01-08 15:02:44 +08:00
lvmeng 378e6088e7 采集向量数据接口调试 2026-01-06 18:49:33 +08:00
lvmeng fc04db9268 增量人脸查询优化 2025-12-31 11:54:17 +08:00
lvmeng 0d2a38391c 增量分页优化 2025-12-29 14:17:57 +08:00
lvmeng 7374f279b4 增量分页优化 2025-12-29 14:08:04 +08:00
lvmeng bb4230419a 增量接口时间戳来源于全量查询时间 2025-12-26 14:12:18 +08:00
lvmeng 5c7f222a0a 修改接口地址 2025-12-26 13:50:28 +08:00
lvmeng dfb734e25d 营养数据计算优化;人脸增加数据处理;其它优化; 2025-12-25 18:57:26 +08:00
lvmeng f080a8b73c 优化识别后未离开多次切换模式导致的不再识别问题 2025-12-22 14:18:46 +08:00
lvmeng 960b429e69 优化 2025-12-19 17:34:31 +08:00
lvmeng af56fb4aeb 优化 2025-12-19 11:10:56 +08:00
lvmeng ab3109f36a 支付完成倒计时3秒返回首页;秤默认置零取消,打开直接识别物品;人脸识别图片显示拉伸优化;设置页面切换是否收费模式首页逻辑处理; 2025-12-18 18:25:15 +08:00
lvmeng 1d0e1a62e2 增加消费码支付功能、支付逻辑优化、金额问题优化、订单绑定优化 2025-12-12 17:50:51 +08:00
lvmeng a0847d21b7 调试优化 2025-12-11 18:54:57 +08:00
lvmeng 6e318f8c82 支付调试优化 2025-12-10 17:56:24 +08:00
lvmeng 88d7530568 t调试 2025-12-09 18:38:26 +08:00
lvmeng 086845dd16 营养接口调试、支付接口联调 2025-12-08 18:06:48 +08:00
lvmeng 04e318f47b 增加支付接口相关逻辑 2025-12-05 18:03:31 +08:00
lvmeng 8899cae323 接口调试、增加人脸识别后主副屏支付金额逻辑、其它优化 2025-12-03 18:40:47 +08:00
lvmeng 29188c5944 接口联调 2025-11-28 18:46:41 +08:00
lvmeng 9bcd7c9983 人脸识别支付流程 2025-11-28 09:13:29 +08:00
lvmeng 88179e27b7 首页、支付功能 2025-11-26 18:57:53 +08:00
lvmeng 60b712f216 结算页面、设置页面 2025-11-19 18:35:53 +08:00
260 changed files with 21457 additions and 3990 deletions
+3 -6
View File
@@ -1,15 +1,12 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
/.idea/
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
app/objectbox-models/*.json
app/objectbox-models/*.json.bak
-3
View File
@@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>
-18
View File
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-11-07T10:55:54.677927600Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="Default" identifier="serial=192.168.1.56:5555;connection=2687c52a" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
</project>
-20
View File
@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="azul-17" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/lib_face" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>
-9
View File
@@ -1,9 +0,0 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<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">
<option name="id" value="Android" />
</component>
</project>
-17
View File
@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>
Generated
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+4
View File
@@ -0,0 +1,4 @@
kotlin version: 2.0.21
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
1. Kotlin compile daemon is ready
+14 -5
View File
@@ -51,13 +51,15 @@ android {
kotlinOptions {
jvmTarget = "11"
}
viewBinding {
enable = true
buildFeatures {
viewBinding = true
buildConfig = true
}
}
dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar"))))
//implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar"))))
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
@@ -108,12 +110,19 @@ dependencies {
implementation(libs.pytorch.android)
implementation(libs.pytorch.android.torchvision)
implementation(libs.baseRecyclerViewAdapterHelper4)
implementation(libs.refresh.layout.kernel)
implementation(libs.refresh.header.classics)
val objectboxVersion = "5.0.1"
debugImplementation("io.objectbox:objectbox-android-objectbrowser:$objectboxVersion")
// releaseImplementation("io.objectbox:objectbox-android:$objectboxVersion")
debugImplementation(libs.objectbox.android.objectbrowser)
releaseImplementation("io.objectbox:objectbox-android:$objectboxVersion")
// implementation("io.objectbox:objectbox-fulltext:5.0.1")
implementation(libs.androidx.recyclerview)
implementation(libs.eventbus)
}
apply(plugin = "io.objectbox")
+21
View File
@@ -0,0 +1,21 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.sw.dualscreen",
"variantName": "debug",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "1.0",
"outputFile": "档口双屏_1.0-debug.apk"
}
],
"elementType": "File",
"minSdkVersionForDexing": 29
}
Binary file not shown.
Binary file not shown.
-49
View File
@@ -1,49 +0,0 @@
{
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
"entities": [
{
"id": "1:594331511073099531",
"lastPropertyId": "4:1528837175750321569",
"name": "Food",
"properties": [
{
"id": "1:2317727855243736226",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:340951913928211738",
"name": "name",
"type": 9
},
{
"id": "3:5484846939472684412",
"name": "foodIdx",
"type": 5
},
{
"id": "4:1528837175750321569",
"name": "foodVector",
"indexId": "1:6010838397086628487",
"type": 28,
"flags": 8
}
],
"relations": []
}
],
"lastEntityId": "1:594331511073099531",
"lastIndexId": "1:6010838397086628487",
"lastRelationId": "0:0",
"lastSequenceId": "0:0",
"modelVersion": 5,
"modelVersionParserMinimum": 5,
"retiredEntityUids": [],
"retiredIndexUids": [],
"retiredPropertyUids": [],
"retiredRelationUids": [],
"version": 1
}
+37
View File
@@ -0,0 +1,37 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.sw.dualscreen",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "1.0",
"outputFile": "档口双屏_1.0-release.apk"
}
],
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/档口双屏_1.0-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/档口双屏_1.0-release.dm"
]
}
],
"minSdkVersionForDexing": 29
}
+18 -7
View File
@@ -8,6 +8,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
@@ -40,7 +41,8 @@
</receiver>
<activity
android:name=".activity.InitActivity"
android:exported="true">
android:exported="true"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -49,14 +51,23 @@
</activity>
<activity
android:name=".activity.MainActivity"
android:exported="true">
android:exported="true"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
</activity>
<activity
android:name=".activity.FoodCollectionActivity"
android:exported="true">
<!-- <activity-->
<!-- android:name=".activity.FoodCollectionActivity"-->
<!-- android:exported="true">-->
</activity>
<activity android:name="com.sw.dualscreen.activity.CollectedDataActivity" />
<!-- </activity>-->
<!-- <activity android:name="com.sw.dualscreen.activity.CollectedDataActivity" />-->
<activity android:name="com.sw.dualscreen.activity.SettingActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:launchMode="singleTask" />
<activity android:name="com.sw.dualscreen.activity.CollectedFoodActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
<activity android:name="com.sw.dualscreen.activity.PayActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:launchMode="singleTask"/>
<provider
android:name="androidx.core.content.FileProvider"
@@ -17,11 +17,22 @@ object GlobalData {
*/
var appVersion: String = "1"
/**
* 具体业务baseurl
*/
var appBaseUrl: String = ""
/**
* 具体业务 BaseUrl
*/
// const val LOCAL_BASE_URL = "http://192.168.1.201:14801"
const val LOCAL_BASE_URL = "http://192.168.10.101:24801"
// const val TEST_BASE_URL = "https://dev.yixiong-tech.com:8083"
const val TEST_BASE_URL = "https://dev.yixiong-tech.com:8081"
const val PROD_BASE_URL = "https://platform-api.uat.shuziweidao.com"
/**
* 菜品识别模型版本号,后边以接口返回为准
*/
var foodModelVersion: String = "1.0.0"
/**
* 横排数量
*/
@@ -39,7 +50,7 @@ object GlobalData {
var appId = "Hkz1rBk6PZXbS8KwKr67K2eZtsz8bRoMHLg64bUdgCZj"
var sdkKey = "AabXs3sHM8UhhE7oCGf4LVMLrdkGb1nJNksbjjTdVt7k"
var activeKey = "085F-118G-Q3LH-HPGZ"//2号楼"085F-118G-Q3AB-1WVJ" 5号楼"085F-118G-Q3LH-HPGZ"
var activeKey = "085F-118G-Q3NU-134V"//2号楼"085F-118G-Q3AB-1WVJ" 5号楼"085F-118G-Q3LH-HPGZ"
}
/**
@@ -61,4 +72,9 @@ object GlobalKey {
const val KEY_FIRST_RUN = "firstRun"
const val KEY_USER_INFO = "userInfoKey"
const val KEY_PICKUP_MODE = "pickupMode"
const val KEY_CHARGE_MODE = "chargeMode"
// 餐具重量(克),取餐提交时用于从就餐重量中扣减,默认 0 表示不扣减
const val KEY_DISH_WEIGHT = "dishWeight"
const val KEY_SETTLEMENT_MODE = "settlementMode"
const val KEY_BASE_URL = "baseUrlKey"
}
+58 -3
View File
@@ -1,7 +1,12 @@
package com.sw.dualscreen
import android.app.Activity
import android.os.Bundle
import com.sw.dualscreen.objbox.ObjectBox
import com.sw.dualscreen.sdk.SensorScaleUtils
import com.sw.dualscreen.utils.ActivityManager
import com.sw.dualscreen.utils.CrashHandler
import com.sw.dualscreen.utils.SpTool
import com.sw.plate.App
import com.sw.plate.utils.AppUtil
import timber.log.Timber
@@ -9,7 +14,7 @@ import timber.log.Timber
class MyApp : App() {
companion object {
const val DEBUG: Boolean = true
var instance: MyApp?=null
var instance: MyApp? = null
}
override fun onCreate() {
@@ -17,11 +22,61 @@ class MyApp : App() {
instance = this
Timber.plant(Timber.DebugTree())
var deviceId = AppUtil.getUDID(this)
Timber.d("UDID = ${AppUtil.getUDID(this)}")
deviceId = "3ea47dc0-3cf0-3c2f-909c-265a9a65572e"
Timber.d("deviceId = $deviceId")
// deviceId = "3ea47dc0-3cf0-3c2f-909c-265a9a65572e"
GlobalData.deviceId = deviceId
ObjectBox.init(this)
// 初始化崩溃处理器
CrashHandler.init(this)
SensorScaleUtils.startScale()
addActivityLifecycleListener()
// 优先读取用户手动切换后持久化的 url
val savedUrl = SpTool.baseUrl
if (!savedUrl.isNullOrEmpty()) {
GlobalData.appBaseUrl = savedUrl
} else {
// 未保存过则使用默认逻辑:特定设备走测试环境,其余走 UAT 测试档口机:2987f0c5-5754-33e9-b00a-251db5e2e55f
GlobalData.appBaseUrl =
if ("2987f0c5-5754-33e9-b00a-251db5e2e55f" == deviceId) GlobalData.TEST_BASE_URL else GlobalData.PROD_BASE_URL
}
}
private fun addActivityLifecycleListener() {
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
override fun onActivityCreated(
activity: Activity,
savedInstanceState: Bundle?
) {
ActivityManager.addActivity(activity)
}
override fun onActivityStarted(activity: Activity) {
}
override fun onActivityResumed(activity: Activity) {
}
override fun onActivityPaused(activity: Activity) {
}
override fun onActivityStopped(activity: Activity) {
}
override fun onActivitySaveInstanceState(
activity: Activity,
outState: Bundle
) {
}
override fun onActivityDestroyed(activity: Activity) {
ActivityManager.removeActivity(activity)
if (ActivityManager.isEmpty()) {
SensorScaleUtils.closeScale()
}
}
})
}
}
@@ -0,0 +1,42 @@
package com.sw.dualscreen
class Test {
private var lastWeight = 0
// private var currentWeight = 0
data class WeightRecord(
var eatWeight: Int = 0,
var deviceWeight: Int = 0,
var lastWeight: Int = 0,
var state: Boolean = false
)
private val weightRecord by lazy { WeightRecord() }
fun main() {
readWeight { weight ->
if (weightRecord.state) {
//数据已记录
return@readWeight
}
if (weight == 0 || lastWeight == 0) {
return@readWeight
}
if (weight < lastWeight) {
weightRecord.let {
it.deviceWeight = weight
it.eatWeight = it.deviceWeight - it.lastWeight
it.state = true
}
}
lastWeight = weight
}
}
fun readWeight(block: (Int) -> Unit) {
}
}
@@ -1,25 +1,43 @@
package com.sw.dualscreen.activity
import android.app.Dialog
import android.content.Context
import android.content.Intent
import android.graphics.drawable.ColorDrawable
import android.hardware.display.DisplayManager
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.Display
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import com.sw.dualscreen.R
import com.sw.dualscreen.databinding.DialogWaitingBinding
import com.sw.dualscreen.model.IActivityResult
import com.sw.dualscreen.model.response.PostEvent
import com.sw.dualscreen.utils.FileUtil
import com.sw.dualscreen.view.CustomDialog
import com.sw.dualscreen.viewmodel.BaseViewModel
import kotlinx.coroutines.launch
import com.sw.plate.utils.ScanGunKeyEventHelper
import com.sw.plate.utils.ToastUtils
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import timber.log.Timber
abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
val displays: Array<Display> by lazy {
val displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
displayManager.displays
}
protected lateinit var binding: VB
protected lateinit var context: Context
private var mDialogWaiting: CustomDialog? = null
@@ -27,6 +45,7 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
EventBus.getDefault().register(this)
enableEdgeToEdge()
context = this
@@ -40,6 +59,12 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
registerDataChange()
}
override fun onDestroy() {
keyEventHelper?.release()
EventBus.getDefault().unregister(this)
super.onDestroy()
}
abstract fun getViewModel(): BaseViewModel
protected abstract fun inflateViewBinding(): VB
@@ -49,37 +74,52 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
}
open fun registerDataChange() {
lifecycleScope.launch {
viewModel.showLoading.collect {
Timber.d("registerDataChange 用户 showLoading = $it")
if (it) {
showWaitingDialog("")
} else {
hideWaitingDialog()
}
}
}
// lifecycleScope.launch {
// viewModel.showLoading.collect {
// Timber.d("registerDataChange 用户 showLoading = $it")
// if (it) {
// showWaitingDialog("")
// } else {
// hideWaitingDialog()
// }
// }
// }
}
/**
* 显示等待提示框
*/
fun showWaitingDialog(tip: String?): Dialog? {
hideWaitingDialog()
val view = View.inflate(this, R.layout.dialog_waiting, null)
if (!TextUtils.isEmpty(tip)) (view.findViewById<View?>(R.id.tvTip) as TextView).text = tip
mDialogWaiting = CustomDialog(this, view, R.style.MyDialog)
mDialogWaiting!!.show()
mDialogWaiting!!.setCancelable(true)
return mDialogWaiting
fun showWaitingDialog(tip: String?) {
runOnUiThread {
hideWaitingDialog()
//val view = View.inflate(this, R.layout.dialog_waiting, null)
val dialogBinding = DialogWaitingBinding.inflate(LayoutInflater.from(this))
dialogBinding.tvTip.text = tip
mDialogWaiting = CustomDialog(this, dialogBinding.root)
mDialogWaiting?.show()
}
}
/**
* 隐藏等待提示框
*/
fun hideWaitingDialog() {
mDialogWaiting?.dismiss()
mDialogWaiting = null
runOnUiThread {
mDialogWaiting?.dismiss()
mDialogWaiting = null
}
}
fun showWaitingDialog2(tip: String?) {
if (mDialogWaiting == null) {
hideWaitingDialog()
val view = View.inflate(this, R.layout.dialog_waiting, null)
mDialogWaiting = CustomDialog(this, view, R.style.MyDialog)
mDialogWaiting?.show()
}
val contentView = mDialogWaiting?.findViewById<ViewGroup>(android.R.id.content)
val tvTip = contentView?.findViewById<TextView>(R.id.tvTip)
tvTip?.text = tip
}
private var launchPermissionCallback: IActivityResult.RequestPermissionCallback? = null
@@ -97,4 +137,98 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
) { result: Boolean ->
launchPermissionCallback?.callback(result)
}
fun log(msg: String) {
Timber.d(msg)
FileUtil.saveLog(msg)
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onPostEvent(event: PostEvent) {
}
/**
* 监听扫描枪扫描事件
*/
fun registerKeyEvent() {
keyEventHelper =
ScanGunKeyEventHelper( object : ScanGunKeyEventHelper.OnScanSuccessListener {
override fun onScanSuccess(barcode: String?) {
Timber.d("onScanSuccess barcode = $barcode")
if (barcode == null) return
scanGunKeyEventCallback(barcode)
}
})
}
protected var keyEventHelper: ScanGunKeyEventHelper? = null
/**
* 处理扫描枪数据
*/
fun scanGunKeyEventCallback(scanInfo: String){
if (this is PayActivity) {
qrCodePay(scanInfo)
}
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (keyEventHelper != null) {
if (keyEventHelper!!.isScanGunEvent(event)) {
keyEventHelper!!.analysisKeyEvent(event)
return true
}
}
return super.dispatchKeyEvent(event)
}
private var permissionCallback: ((isGranted: Boolean) -> Unit)? = null
private var activityCallback: ((intent: Intent?) -> Unit)? = null
fun requestMultiplePermissions(
permissions: Array<String>,
callback: (isGranted: Boolean) -> Unit
) {
this.permissionCallback = callback
requestMultiplePermissionsLauncher.launch(permissions)
}
fun requestPermission(permission: String, callback: (isGranted: Boolean) -> Unit) {
this.permissionCallback = callback
requestPermissionLauncher.launch(permission)
}
val requestMultiplePermissionsLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
var isGranted = true
permissions.entries.forEach {
if (!it.value) {
isGranted = false
}
}
permissionCallback?.invoke(isGranted)
}
// 权限请求回调
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)
}
}
@@ -1,134 +0,0 @@
package com.sw.dualscreen.activity
import android.annotation.SuppressLint
import androidx.activity.viewModels
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.LinearLayoutManager
import com.sw.dualscreen.R
import com.sw.dualscreen.adapter.CollectedFoodAdapter
import com.sw.dualscreen.databinding.ActivityCollectedDataBinding
import com.sw.dualscreen.databinding.LayoutEmptySearchBinding
import com.sw.dualscreen.dialog.WarnDialog
import com.sw.dualscreen.ext.addOnActionSearchListener
import com.sw.dualscreen.ext.hideKeyboard
import com.sw.dualscreen.objbox.CollectedFoodBean
import com.sw.dualscreen.objbox.Food
import com.sw.dualscreen.objbox.ObjectBox
import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel
import com.sw.plate.utils.ToastUtils
import io.objectbox.Box
import io.objectbox.kotlin.boxFor
import kotlin.getValue
class CollectedDataActivity : BaseActivity<ActivityCollectedDataBinding>() {
private val viewModel by viewModels<UserViewModel>()
override fun getViewModel(): BaseViewModel {
return viewModel
}
override fun inflateViewBinding(): ActivityCollectedDataBinding {
return ActivityCollectedDataBinding.inflate(layoutInflater)
}
private val list: MutableList<CollectedFoodBean> = mutableListOf()
private val adapter by lazy {
CollectedFoodAdapter(list).apply {
isStateViewEnable = true
addOnItemChildClickListener(R.id.ivDeleteFood) { _, _, position ->
deleteGoods(position)
}
}
}
override fun initialize() {
super.initialize()
binding.rvFoodList.let {
it.layoutManager = LinearLayoutManager(this)
it.adapter = adapter
}
binding.ivBack.setOnClickListener { finish() }
binding.ivGoodsSearch.setOnClickListener {
val searchName = binding.etInputGoods.text.toString().trim()
if (searchName.isBlank()) {
ToastUtils.showToast("请输入物品名称")
return@setOnClickListener
}
getCollectGoods(searchName)
}
binding.etInputGoods.let { v ->
v.addOnActionSearchListener {
val searchName = binding.etInputGoods.text.toString().trim()
if (searchName.isBlank()) {
ToastUtils.showToast("请输入物品名称")
return@addOnActionSearchListener
}
getCollectGoods(searchName)
}
v.addTextChangedListener {
if (it.isNullOrBlank()) {
getCollectGoods()
}
}
}
getCollectGoods()
}
private var box: Box<Food>? = null
@SuppressLint("NotifyDataSetChanged")
private fun getCollectGoods(searchName:String?=null) {
if (box == null) {
box = ObjectBox.boxStore.boxFor(Food::class)
}
list.clear()
var queryList = box?.all?.distinctBy { it.name }
if (searchName.isNullOrBlank().not()) {
queryList = queryList?.filter { it.name?.contains(searchName) == true }
}
queryList?.forEach {
list.add(CollectedFoodBean(foodName = it.name))
}
adapter.notifyDataSetChanged()
if (list.isEmpty()) {
loadEmptyView()
}
binding.root.hideKeyboard()
}
private var emptyBinding: LayoutEmptySearchBinding? = null
private fun loadEmptyView() {
if (emptyBinding == null) {
emptyBinding = LayoutEmptySearchBinding.inflate(layoutInflater, binding.rvFoodList, false)
}
emptyBinding?.root?.let { layout ->
layout.setOnClickListener { layout.hideKeyboard() }
adapter.stateView = layout
}
}
private fun deleteGoods(position: Int) {
WarnDialog(
context = context,
content = "请确认是否删除菜品:${list[position].foodName}",
confirmBlock = {
Thread {
runOnUiThread {
showWaitingDialog("加载中……")
}
val name = list[position].foodName
val filterIdList = box?.all?.filter { it.name == name }?.map { it.id }
box?.removeByIds(filterIdList)
runOnUiThread {
hideWaitingDialog()
adapter.removeAt(position)
ToastUtils.showToast("删除成功")
if (list.isEmpty()) {
loadEmptyView()
}
}
}.start()
}).show()
}
}
@@ -0,0 +1,234 @@
package com.sw.dualscreen.activity
import android.annotation.SuppressLint
import androidx.activity.viewModels
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.sw.dualscreen.R
import com.sw.dualscreen.adapter.CollectedFoodNewAdapter
import com.sw.dualscreen.databinding.ActivityCollectedFoodBinding
import com.sw.dualscreen.dialog.RemindDialog
import com.sw.dualscreen.ext.addOnActionSearchListener
import com.sw.dualscreen.ext.gone
import com.sw.dualscreen.ext.hideKeyboard
import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.model.response.v2.CollectedFoodV2
import com.sw.dualscreen.objbox.ObjectBox
import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.NetViewModelV2
import com.sw.plate.utils.ToastUtils
import kotlinx.coroutines.launch
class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
companion object {
private const val PAGE_SIZE = 100
}
private val viewModel by viewModels<NetViewModelV2>()
override fun getViewModel(): BaseViewModel {
return viewModel
}
override fun inflateViewBinding(): ActivityCollectedFoodBinding {
return ActivityCollectedFoodBinding.inflate(layoutInflater)
}
private val list: MutableList<CollectedFoodV2> = mutableListOf()
private val adapter by lazy {
CollectedFoodNewAdapter(list).apply {
// isStateViewEnable = true
addOnItemChildClickListener(R.id.ivDeleteFood) { _, _, position ->
loadDeleteDialog(position)
}
}
}
override fun initialize() {
super.initialize()
registerKeyEvent()
binding.root.setOnClickListener { it.hideKeyboard() }
binding.rvFoodList.let {
it.layoutManager = LinearLayoutManager(this)
it.adapter = adapter
}
binding.include.let {
it.tvPageTitle.text = "已采集餐品"
it.ivPageBack.setOnClickListener { finish() }
}
binding.ivFoodSearch.setOnClickListener {
val searchName = binding.etInputFood.text.toString().trim()
if (searchName.isBlank()) {
ToastUtils.showToast("请输入物品名称")
return@setOnClickListener
}
getCollectGoods(searchName)
}
binding.etInputFood.let { v ->
v.addOnActionSearchListener {
val searchName = binding.etInputFood.text.toString().trim()
if (searchName.isBlank()) {
ToastUtils.showToast("请输入物品名称")
return@addOnActionSearchListener
}
pageNo = 1
getCollectGoods(searchName)
}
v.addTextChangedListener {
if (it.isNullOrBlank()) {
pageNo = 1
getCollectGoods()
}
}
}
binding.refreshLayout.let {
it.setEnableRefresh(true)
it.setEnableLoadMore(false)
it.setOnRefreshListener {
pageNo = 1
getCollectGoods()
}
it.setOnLoadMoreListener {
getCollectGoods()
}
}
binding.emptyInclude.root.setOnClickListener {
it.hideKeyboard()
}
getCollectGoods()
}
@SuppressLint("NotifyDataSetChanged")
private fun getCollectGoods(searchName: String? = null) {
// if (box == null) {
// box = ObjectBox.boxStore.boxFor(Food::class)
// }
// list.clear()
// val totalList = box?.all
// ?.filter { it.name!=null }
// ?.groupBy { it.name!! }
// ?.map { CollectedFoodInfo(foodName = it.key, foodCount = it.value.size) }
// //var queryMap:Map<String, List<Food>> ?= null
// if (searchName.isNullOrBlank().not()) {
// //queryMap = totalMap?.filter { it.key.contains(searchName) }
// val temp = totalList?.filter { it.foodName?.contains(searchName) == true}
// if (temp.isNullOrEmpty().not()) {
// list.addAll(temp)
// }
// } else {
// if (totalList.isNullOrEmpty().not()) {
// list.addAll(totalList)
// }
// }
// adapter.notifyDataSetChanged()
// if (list.isEmpty()) {
// loadEmptyView()
// }
viewModel.getCollectPage(
pageNum = pageNo.toLong(),
pageSize = PAGE_SIZE.toLong(),
foodName = searchName,
onSuccess = { items ->
runOnUiThread {
loadFoodList(items)
}
},
onFailure = {
runOnUiThread {
finishRefresh()
ToastUtils.showToast(it)
}
}
)
}
@SuppressLint("NotifyDataSetChanged")
private fun loadFoodList(items: List<CollectedFoodV2>) {
finishRefresh()
if (pageNo == 1 && items.isEmpty()) {
list.clear()
adapter.notifyDataSetChanged()
loadEmptyView()
binding.refreshLayout.setEnableLoadMore(false)
return
}
binding.refreshLayout.visible()
binding.emptyInclude.root.gone()
if (pageNo == 1) {
list.clear()
}
list.addAll(items)
val isEnableLoadMore = items.size >= PAGE_SIZE
if (isEnableLoadMore) {
pageNo++
}
binding.refreshLayout.setEnableLoadMore(isEnableLoadMore)
adapter.notifyDataSetChanged()
binding.root.hideKeyboard()
}
private var pageNo = 1
// private var emptyBinding: LayoutEmptySearchBinding? = null
private fun loadEmptyView() {
binding.refreshLayout.gone()
binding.emptyInclude.root.visible()
//if (emptyBinding == null) {
// emptyBinding =
// LayoutEmptySearchBinding.inflate(layoutInflater, binding.rvFoodList, false)
//}
//emptyBinding?.root?.let { layout ->
// layout.setOnClickListener { layout.hideKeyboard() }
// adapter.stateView = layout
//}
}
private fun loadDeleteDialog(position: Int) {
RemindDialog(
context = context,
content = "请确认是否删除菜品:${list[position].foodName}",
confirmBlock = {
deleteGoods(position)
}).show()
}
private fun deleteGoods(position: Int) {
val food = list[position]
showWaitingDialog("加载中……")
viewModel.deleteCollect(food.foodId, food.version) { deleteSuccess ->
if (deleteSuccess) {
// Thread {}.start()
lifecycleScope.launch {
val filterFoodList = ObjectBox.filter(food.foodName)
filterFoodList.forEach { it.isDel = true }
if (filterFoodList.isNotEmpty()) {
ObjectBox.putAll(filterFoodList)
}
runOnUiThread {
hideWaitingDialog()
adapter.removeAt(position)
ToastUtils.showToast("删除成功")
if (list.isEmpty()) {
loadEmptyView()
}
}
}
}
}
}
private fun finishRefresh() {
binding.refreshLayout.let {
if (pageNo == 1) {
it.finishRefresh(500)
} else {
it.finishLoadMore(500)
}
}
}
}
@@ -1,370 +0,0 @@
package com.sw.dualscreen.activity
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Typeface
import android.net.Uri
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.activity.viewModels
import androidx.camera.view.PreviewView
import androidx.core.net.toUri
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.example.utils.FloatBase64Utils
import com.sw.dualscreen.R
import com.sw.dualscreen.adapter.FoodCollectionAdapter
import com.sw.dualscreen.adapter.GenericItemAdapter
import com.sw.dualscreen.adapter.GridSpacingItemDecoration
import com.sw.dualscreen.adapter.dpToPx
import com.sw.dualscreen.databinding.ActivityFoodCollectionBinding
import com.sw.dualscreen.databinding.ItemSearchFoodInfoBinding
import com.sw.dualscreen.databinding.LayoutCameraPreviewBinding
import com.sw.dualscreen.ext.clickWithDebounce
import com.sw.dualscreen.ext.dp
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.objbox.Food
import com.sw.dualscreen.objbox.FoodCollectionBean
import com.sw.dualscreen.objbox.FoodModule
import com.sw.dualscreen.objbox.ObjectBox
import com.sw.dualscreen.utils.BitmapCropper
import com.sw.dualscreen.utils.BitmapSaver
import com.sw.dualscreen.utils.CameraHelper
import com.sw.dualscreen.utils.CameraUtils
import com.sw.dualscreen.utils.Debouncer
import com.sw.dualscreen.utils.ImageUtil
import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel
import com.sw.plate.utils.ToastUtils
import io.objectbox.Box
import io.objectbox.kotlin.boxFor
import kotlinx.coroutines.launch
import org.pytorch.IValue
import org.pytorch.Module
import org.pytorch.torchvision.TensorImageUtils
import timber.log.Timber
import kotlin.math.max
class FoodCollectionActivity : BaseActivity<ActivityFoodCollectionBinding>() {
companion object {
val MAX_COUNT = 100
}
private val viewModel by viewModels<UserViewModel>()
private var selectedFoodId: String? = ""
private var selectedFoodName: String? = ""
private var box: Box<Food>? = null
private val foodCollectionList: MutableList<FoodCollectionBean> = mutableListOf()
// private lateinit var cameraHelper: CameraHelper
private lateinit var foodAdapter: GenericItemAdapter<FoodInfo, ItemSearchFoodInfoBinding>
private val foodList = mutableListOf<FoodInfo>() // 适配器内部维护的数据列表
private val debouncer = Debouncer(2000)
private lateinit var previewView: PreviewView
private val cameraUtils: CameraUtils by lazy {
CameraUtils(this)
}
private val collectionAdapter: FoodCollectionAdapter by lazy {
FoodCollectionAdapter(foodCollectionList).apply {
// setOnItemClickListener { _, _, position ->
// if (list[position].isShowCamera) {
//// takePhoto()
// cameraHelper.openCamera()
// }
// }
addOnItemChildClickListener(R.id.ivDelete) { _, _, position ->
// list.removeAt(position)
// collectionAdapter.notifyItemRemoved(position)
// collectionAdapter.notifyItemRangeChanged(position, list.size)
foodCollectionList[position].let {
it.bitmap = null
it.isShowCamera = true
it.isFinish = false
}
notifyItemChanged(position)
}
}
}
override fun getViewModel(): BaseViewModel {
return viewModel
}
override fun inflateViewBinding(): ActivityFoodCollectionBinding {
return ActivityFoodCollectionBinding.inflate(layoutInflater)
}
private val cameraCallback: (Uri) -> Unit = { uri ->
val index = foodCollectionList.indexOfFirst { it.bitmap == null }
if (index == -1) {
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
rerurn@ cameraCallback
}
ImageUtil.uriToBitmap(this, uri)?.let { bitmap ->
// val cropBitmap = BitmapCropper.cropCenter(
// original = bitmap,
// targetWidth = 1300, targetHeight = 900,
// //offsetX = 30, offsetY = 100
// )
val file = BitmapSaver.saveToAppFilesDir(
bitmap, this, "IMG_CROP_${System.currentTimeMillis()}.jpg"
)
Timber.d("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}")
foodCollectionList[index].let {
it.bitmap = bitmap
it.isShowCamera = false
it.imageUri = file?.toUri()
}
collectionAdapter.notifyItemChanged(index)
}
}
@SuppressLint("NotifyDataSetChanged")
private fun takePhoto() {
val count = foodCollectionList.count { it.bitmap != null }
if (count >= MAX_COUNT) {
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
return
}
cameraUtils.takePhoto(cameraCallback)
}
override fun initialize() {
cameraUtils.initCamera()
val previewBinding =
LayoutCameraPreviewBinding.inflate(layoutInflater, binding.flCameraPreview)
previewView = previewBinding.previewView.also {
it.updateLayoutParams {
width = 180.dp
height = 180.dp
}
}
cameraUtils.setPreviewController(previewView)
// 初始化 CameraHelper
// cameraHelper = CameraHelper(
// context = this,
// caller = this,
// authority = "${packageName}.fileprovider"
// ) { uri, path ->
// //if (foodCollectionList.size < 6) {
// // val insertIndex = if (foodCollectionList.isEmpty()) 0 else foodCollectionList.size - 1
// // foodCollectionList.add(insertIndex, FoodCollectionBean(imageUri = uri))
// //} else {
// // foodCollectionList[foodCollectionList.size - 1] = FoodCollectionBean(imageUri = uri)
// //}
// //collectionAdapter.notifyDataSetChanged()
// cameraCallback(uri)
// }
binding.ivBack.setOnClickListener {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
}
repeat(MAX_COUNT) {
foodCollectionList.add(FoodCollectionBean(isShowCamera = true))
}
binding.rvFoodCollection.let {
it.layoutManager = GridLayoutManager(this, 3, GridLayoutManager.VERTICAL, false)
it.adapter = collectionAdapter
}
binding.btnFoodSearch.setOnClickListener {
searchInfo()
}
binding.btnSave.setOnClickListener {
if (selectedFoodName.isNullOrBlank()) {
Toast.makeText(this, "请选择菜品名称", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
if (foodCollectionList.size <= 1) {
Toast.makeText(this, "请拍摄菜品照片", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
vectorThread()
}
binding.editFoodName.setOnEditorActionListener { v, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
searchInfo()
val imm =
v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(v.windowToken, 0)
true
} else {
false
}
}
binding.btnCollectedGoods.setOnClickListener {
startActivity(Intent(this, CollectedDataActivity::class.java))
}
binding.btnTakePhoto.clickWithDebounce {
binding.btnTakePhoto.text = "拍照"
val count = foodCollectionList.count { it.bitmap != null }
if (count >= MAX_COUNT) {
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
return@clickWithDebounce
}
// cameraHelper.openCamera()
takePhoto()
}
binding.btnClearData.setOnClickListener { clearData() }
foodAdapter = createAdapter()
binding.recyclerview.layoutManager = GridLayoutManager(context, 2)
// 添加间距装饰(12dp)
binding.recyclerview.addItemDecoration(
GridSpacingItemDecoration(
spanCount = 2,
spacing = dpToPx(30),
includeEdge = false // 包含边缘间距
)
)
binding.recyclerview.adapter = foodAdapter
}
private fun vectorThread() {
//Loading.show(this)
showWaitingDialog("加载中……")
Thread {
foodCollectionList
.filter { it.bitmap != null }
.forEachIndexed { index, it ->
image2VectorTask(it, index)
}
runOnUiThread {
window.decorView.postDelayed({
//Loading.dismiss()
hideWaitingDialog()
}, 1000)
}
}.start()
}
private fun initBox() {
if (box == null) {
box = ObjectBox.boxStore.boxFor(Food::class)
}
}
private fun image2VectorTask(item: FoodCollectionBean, position: Int) {
initBox()
val imageVector = FoodModule.bitmap2FloatArray(item.bitmap!!)
val base64Str = FloatBase64Utils.floatArrayToBase64(imageVector)
Timber.tag("mzf1").e(base64Str)
// viewModel.postImageData(
// context,
// foodId = selectedFoodId.toString(),
// foodName = selectedFoodName.toString(),
// foodVector = base64Str,
// uri = item.imageUri!!
// )
box?.put(Food(name = checkedItem!!.foodName, foodIdx = 0, foodVector = imageVector))
foodCollectionList[position].let {
it.imageVector = imageVector
it.isFinish = true
}
runOnUiThread {
collectionAdapter.notifyItemChanged(position)
}
}
private fun searchInfo() {
debouncer.debounce {
viewModel.searchByFoodName(binding.editFoodName.text.toString())
}
}
override fun registerDataChange() {
super.registerDataChange()
lifecycleScope.launch {
viewModel.searchFoodInfoList.collect {
// foodList.clear()
// foodList.addAll(it)
foodAdapter.updateData(it)
}
}
}
private var checkedItem: FoodInfo? = null
private fun createAdapter(): GenericItemAdapter<FoodInfo, ItemSearchFoodInfoBinding> {
return GenericItemAdapter(
items = emptyList(),
bindingInflater = ItemSearchFoodInfoBinding::inflate,
bindCallback = { item, position ->
this.tvName.text = item.foodName
if (item.id != checkedItem?.id) {
this.tvName.typeface = Typeface.defaultFromStyle(Typeface.NORMAL)
this.tvName.setTextColor(resources.getColor(R.color.search_normal))
this.llRoot.setBackgroundResource(R.drawable.grid_search_item_normal)
} else {
this.tvName.typeface = Typeface.defaultFromStyle(Typeface.BOLD)
this.tvName.setTextColor(resources.getColor(R.color.search_checked))
this.llRoot.setBackgroundResource(R.drawable.grid_search_item_checked)
}
this.llRoot.setOnClickListener {
Timber.d("itemClick ${item.foodName}, position = $position")
checkedItem = item
// viewModel.updateCurrentItem(item)
// itemClickCallback(item)
selectedFoodName = item.foodName
selectedFoodId = item.id
foodAdapter.notifyDataSetChanged()
}
}
)
}
private var clickIndex = -1
@SuppressLint("NotifyDataSetChanged")
private fun clearData() {
foodCollectionList.forEach {
it.bitmap = null
it.isShowCamera = true
it.isFinish = false
}
collectionAdapter.notifyDataSetChanged()
clickIndex = -1
binding.editFoodName.setText("")
// foodList.clear()
// foodAdapter.updateData(mutableListOf())
// foodAdapter.notifyDataSetChanged()
//loadEmptyView()
}
override fun onResume() {
super.onResume()
cameraUtils.bind()
binding.llCameraFlag.run {
visibility = View.VISIBLE
postDelayed({
visibility = View.GONE
}, 3000)
}
}
override fun onPause() {
super.onPause()
cameraUtils.unbind()
binding.llCameraFlag.visibility = View.VISIBLE
}
}
@@ -1,57 +1,96 @@
package com.sw.dualscreen.activity
import android.Manifest
import android.content.Intent
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import android.os.CountDownTimer
import android.provider.Settings
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat.startActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.databinding.ActivityInitBinding
import com.sw.dualscreen.utils.QRCodeUtil
import com.sw.dualscreen.dialog.RemindDialog
import com.sw.dualscreen.ext.clickWithDebounce
import com.sw.dualscreen.ext.invisible
import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.objbox.FoodModule
import com.sw.dualscreen.utils.FoodVectorTool
import com.sw.dualscreen.utils.L
import com.sw.dualscreen.utils.NetworkUtils
import com.sw.dualscreen.utils.SpTool
import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.DeviceViewModel
import com.sw.dualscreen.viewmodel.NetViewModelV2
import com.sw.plate.utils.AppUtil
import kotlinx.coroutines.flow.drop
import com.sw.plate.utils.ToastUtils
import kotlinx.coroutines.launch
class InitActivity : BaseActivity<ActivityInitBinding>() {
private val viewModel by viewModels<DeviceViewModel>()
companion object {
const val PAGE_SIZE = 100
}
private val userViewModel by viewModels<NetViewModelV2>()
private var pageNum = 1
override fun getViewModel(): BaseViewModel {
return viewModel
return userViewModel
}
override fun inflateViewBinding(): ActivityInitBinding {
return ActivityInitBinding.inflate(layoutInflater)
}
private fun getCollectedFoodVector(block: () -> Unit) {
FoodVectorTool.loadAndSaveFoodVector(
userViewModel = userViewModel,
lifecycleScope = lifecycleScope,
successBlock = { block() },
failureBlock = { getVectorErrorDialog() }
)
}
// private val box by lazy { ObjectBox.boxStore.boxFor(Food::class) }
override fun initialize() {
super.initialize()
val isSuccess = viewModel.checkEquipmentInfo()
if (isSuccess) {
goMainActivity()
return
}
binding.imgQr.setImageBitmap(
QRCodeUtil.generateQRCode(
content = GlobalData.deviceId,
size = 200
)
)
binding.initButton.setOnClickListener {
viewModel.getDeviceToken()
//写死,后续改为从接口获取结算类型 ------------------------------------------
//val settlementMode = 1
//SPUtil.getInstance().put(GlobalKey.KEY_SETTLEMENT_MODE, settlementMode)
registerKeyEvent()
initNetworkTimer()
binding.okButton.clickWithDebounce(500) {
checkNetworkTimer?.cancel()
checkNetworkTimer?.start()
}
showWaitingDialog("网络连接中...")
checkNetworkTimer?.start()
}
override fun registerDataChange() {
super.registerDataChange()
// lifecycleScope.launch {
// viewModel.deviceInfoResult.drop(1).collect {
// if (it != true) return@collect
// goMainActivity()
// }
// }
}
private fun getCollectedFoodVector() {
binding.okButton.invisible()
showWaitingDialog2("加载中……")
lifecycleScope.launch {
viewModel.deviceInfoResult.drop(1).collect {
if (it != true) return@collect
goMainActivity()
FoodModule.init(this@InitActivity) {
getCollectedFoodVector {
runOnUiThread {
hideWaitingDialog()
}
}
}
binding.root.postDelayed({
checkCameraPermissionAndGo()
}, 1000)
}
}
@@ -60,4 +99,162 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
startActivity(intent)
finish()
}
private fun hasCameraPermission(): Boolean {
return ContextCompat.checkSelfPermission(
this,
Manifest.permission.CAMERA
) == android.content.pm.PackageManager.PERMISSION_GRANTED
}
// 检查相机权限
private fun checkCameraPermissionAndGo() {
val isGrantedCameraPermission = hasCameraPermission()
if (isGrantedCameraPermission) {
// 有权限,直接打开 MainActivity
goMainActivity()
} else {
// 无权限,请求相机权限
requestPermission(Manifest.permission.CAMERA) { isGranted ->
checkCameraPermission(isGranted)
}
}
}
// 从设置页面返回后检查权限
private fun checkCameraPermission(isGranted: Boolean) {
if (isGranted) {
// 权限已授予,打开 MainActivity
goMainActivity()
} else {
// 权限仍未授予,再次显示弹窗
showPermissionDeniedDialog()
}
}
// 显示权限被拒绝的弹窗
private fun showPermissionDeniedDialog() {
RemindDialog(
context = context,
content = "暂无相机权限,请授权后重试",
confirmBlock = {
// 打开 App 设置页面
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = android.net.Uri.fromParts("package", packageName, null)
}
startActivity(intent) {
// 从设置页面返回,重新检查权限
val isGrantedCameraPermission = hasCameraPermission()
checkCameraPermission(isGrantedCameraPermission)
}
}
).show()
}
private var checkNetworkTimer: CountDownTimer? = null
private fun initNetworkTimer() {
checkNetworkTimer = object : CountDownTimer(5 * 300, 300) {
override fun onTick(millisUntilFinished: Long) {
}
override fun onFinish() {
L.e("检测网络连接状态" + AppUtil.isNetworkConnected(this@InitActivity))
if (!AppUtil.isNetworkConnected(this@InitActivity)) {
hideWaitingDialog()
binding.okButton.visible()
loadNetworkErrorDialog()
return
}
getDeviceConfig()
getCollectedFoodVector()
}
}
}
// var arcsoftAppId = "Hkz1rBk6PZXbS8KwKr67K2eZtsz8bRoMHLg64bUdgCZj"
// var arcsoftSdkKey = "AabXs3sHM8UhhE7oCGf4LVMLrdkGb1nJNksbjjTdVt7k"
// var arcsoftActiveKey = "085F-118G-Q4GH-B2YH"
private fun getDeviceConfig() {
userViewModel.getDeviceConfig(
onSuccess = { deviceConfig ->
runOnUiThread {
if (deviceConfig == null) {
ToastUtils.showToast("获取设备配置数据失败")
return@runOnUiThread
}
val settlementMode = if (deviceConfig.payType == 2) 0 else 1
SpTool.settlementMode = settlementMode
// // TODO: 临时写死用于测试----------------------------------------
// deviceConfig.arcsoftAppId = arcsoftAppId
// deviceConfig.arcsoftSdkKey = arcsoftSdkKey
// deviceConfig.arcsoftActiveKey = arcsoftActiveKey
// // TODO: 临时写死用于测试----------------------------------------
GlobalData.appId = deviceConfig.arcsoftAppId ?: ""
GlobalData.sdkKey = deviceConfig.arcsoftSdkKey ?: ""
GlobalData.activeKey = deviceConfig.arcsoftActiveKey ?: ""
}
},
onFailure = { runOnUiThread { ToastUtils.showToast(it) } }
)
}
private fun loadNetworkErrorDialog() {
RemindDialog(
context = context,
content = "网络连接异常,请检查WiFi连接状态后重试",
confirmBlock = {
val intent = Intent("android.settings.WIFI_SETTINGS")
startActivity(intent) {
// 从设置页面返回,重新检查网络连接状态
val isConnected = NetworkUtils.isNetworkConnected(this@InitActivity)
if (isConnected.not()) {
loadNetworkErrorDialog()
return@startActivity
}
getCollectedFoodVector()
}
}).show()
}
private fun getVectorErrorDialog() {
RemindDialog(
context = context,
content = "未查询到向量数据,请稍后重试?",
confirmBlock = {
getCollectedFoodVector()
}).show()
}
override fun onDestroy() {
checkNetworkTimer?.cancel()
super.onDestroy()
}
// private fun loadTestDb() {
// // 从测试库导入人脸数据
// FaceDbImporter.importFromAssets(this, object : FaceDbImporter.ImportCallback {
// public override fun onProgress(current: Int, total: Int) {
// showWaitingDialog("导入人脸数据 " + current + "/" + total)
// }
//
// public override fun onError(message: String?) {
// hideWaitingDialog()
// ToastUtils.showToast("离线数据导入失败:" + message)
// }
//
// public override fun onComplete() {
// hideWaitingDialog()
// ToastUtils.showToast("离线数据导入成功")
// // 后续流程会自动跳转
// }
// })
// }
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,381 @@
package com.sw.dualscreen.activity
import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
import androidx.fragment.app.Fragment
import com.sw.dualscreen.GlobalKey
import com.sw.dualscreen.R
import com.sw.dualscreen.activity.fragment.pay.CashPayFragment
import com.sw.dualscreen.activity.fragment.pay.FacePayFragment
import com.sw.dualscreen.activity.fragment.pay.NumberPayFragment
import com.sw.dualscreen.activity.fragment.pay.PayResultFragment
import com.sw.dualscreen.activity.fragment.pay.ScanQrCodePayFragment
import com.sw.dualscreen.databinding.ActivityPayBinding
import com.sw.dualscreen.ext.gone
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.MemberInfo
import com.sw.dualscreen.model.response.PaySuccessEvent
import com.sw.dualscreen.presentation.pay.PayPresentation
import com.sw.dualscreen.utils.Debouncer
import com.sw.dualscreen.utils.SPUtil
import com.sw.dualscreen.utils.SoundPoolUtil
import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.NetViewModelV2
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import org.greenrobot.eventbus.EventBus
import timber.log.Timber
class PayActivity : BaseActivity<ActivityPayBinding>() {
companion object {
const val FOOD_INFO = "foodInfo"
const val TOTAL_AMOUNT = "totalAmount"
const val FOOD_ORDER_ID = "foodOrderId"
const val FOOD_EAT_NUM = "foodEatNum"
// const val MEMBER_INFO = "memberInfo"
const val TAG_PAY_QR_CODE = "tagPayQrCode"
const val TAG_PAY_CASH = "tagPayCash"
const val TAG_PAY_NUMBER = "tagPayNumber"
const val TAG_PAY_FACE = "tagPayFace"
const val TAG_PAY_RESULT = "tagPayResult"
const val PAY_SUCCESS = "pay_success"
}
val userViewModel by viewModels<NetViewModelV2>()
val recognizeViewModel by viewModels<RecognizeViewModel>()
override fun getViewModel(): BaseViewModel {
return userViewModel
}
override fun inflateViewBinding(): ActivityPayBinding {
return ActivityPayBinding.inflate(layoutInflater)
}
var foodInfo: FoodInfo? = null
// var totalAmount:Double = 0.0
private var qrCodePayFragment: ScanQrCodePayFragment? = null
private var cashPayFragment: CashPayFragment? = null
private var numberPayFragment: NumberPayFragment? = null
var facePayFragment: FacePayFragment? = null
private var payResultFragment: PayResultFragment? = null
var foodOrderId: String = ""
var eatNum: Int = 1
var isMember: Boolean = false
val presentation by lazy {
PayPresentation(
activity = this@PayActivity,
userViewModel = userViewModel,
recognizeViewModel = recognizeViewModel,
display = displays[1]
)
}
fun playSoundOnPaySuccess() {
SoundPoolUtil.getInstance().play(PAY_SUCCESS, 0)
}
@Suppress("DEPRECATION")
override fun initialize() {
super.initialize()
presentation.show()
registerKeyEvent()
addBackEventListener()
val soundMap = hashMapOf<String, Int>()
soundMap[PAY_SUCCESS] = R.raw.pay_success
SoundPoolUtil.getInstance().loadR(this, soundMap)
foodInfo = intent.getParcelableExtra(FOOD_INFO)
foodOrderId = intent.getStringExtra(FOOD_ORDER_ID) ?: ""
eatNum = intent.getIntExtra(FOOD_EAT_NUM, 1)
// totalAmount = intent.getDoubleExtra(TOTAL_AMOUNT,0.0)
// memberInfo = intent.getParcelableExtra(MEMBER_INFO)
binding.tvFoodName.text = foodInfo?.foodName
binding.include.ivPageBack.setOnClickListener { finish() }
binding.include.tvPageTitle.text = "下单结算"
binding.btnPayQrCode.setOnClickListener {
isMember = false
if (binding.btnPayQrCode.isChecked) {
return@setOnClickListener
}
showScanQrCodePay()
}
binding.btnPayCash.setOnClickListener {
isMember = false
if (binding.btnPayCash.isChecked) {
return@setOnClickListener
}
cashPayFragment = CashPayFragment()
showFragment(cashPayFragment!!, TAG_PAY_CASH)
switchButton(false, true, false, false)
}
binding.btnPayNumber.setOnClickListener {
if (binding.btnPayNumber.isChecked) {
return@setOnClickListener
}
numberPayFragment = NumberPayFragment()
showFragment(numberPayFragment!!, TAG_PAY_NUMBER)
switchButton(false, false, true, false)
}
binding.btnPayFace.setOnClickListener {
if (binding.btnPayFace.isChecked) {
return@setOnClickListener
}
showFacePay()
}
showScanQrCodePay()
}
private fun showScanQrCodePay() {
qrCodePayFragment = ScanQrCodePayFragment()
showFragment(qrCodePayFragment!!, TAG_PAY_QR_CODE)
switchButton(true, false, false, false)
}
private fun switchButton(vararg arr: Boolean) {
binding.btnPayQrCode.isChecked = arr[0]
binding.btnPayCash.isChecked = arr[1]
binding.btnPayNumber.isChecked = arr[2]
binding.btnPayFace.isChecked = arr[3]
}
fun showFacePay() {
facePayFragment = FacePayFragment()
showFragment(facePayFragment!!, TAG_PAY_FACE)
switchButton(false, false, false, true)
}
private fun showFragment(fragment: Fragment, tag: String) {
runCatching {
supportFragmentManager.beginTransaction().apply {
replace(R.id.fragmentContainerView, fragment)
commitNowAllowingStateLoss()
}
}.onFailure {
it.printStackTrace()
}
}
override fun onDestroy() {
presentation.dismiss()
super.onDestroy()
}
var memberDiscount = 1.0
var memberInfo: MemberInfo? = null
fun showPayInfo(type: Int = 1, isVip: Boolean = false, memberInfo: MemberInfo? = null) {
this.memberInfo = memberInfo
payResultFragment = PayResultFragment.instance(type, memberInfo)
showFragment(payResultFragment!!, TAG_PAY_RESULT)
// if (displays.size > 1) {
// scanQrCodePayPresentation = ScanQrCodePayPresentation(
// activity = this,
// display = displays[1],
// type = type
// ) {
// scanQrCodePaypresentation.dismiss()
// }.also {
// it.foodName = foodInfo?.foodName
// //确认使用vipPrice 还是 specPrice
// val onePrice = if (isVip) foodInfo?.vipPrice else foodInfo?.specPrice
// it.totalPrice = (onePrice ?: 0.0) * eatNum
// }
// scanQrCodePaypresentation.show()
// }
presentation.let {
it.pageType = PayPresentation.QR_CODE_PAY
it.type = type
it.foodName = foodInfo?.foodName
//确认使用vipPrice 还是 specPrice
//val onePrice = if (isVip) foodInfo?.vipPrice else foodInfo?.specPrice
if (isVip) {
userViewModel.getMemberDiscount(memberInfo?.faceUserId?:"") { discount ->
memberDiscount = discount ?: 1.0
var onePrice = foodInfo?.specPrice ?: 0.0
onePrice = if (isVip) onePrice * memberDiscount else onePrice
it.totalPrice = onePrice * eatNum
presentation.initView()
}
} else {
it.totalPrice = (foodInfo?.specPrice ?: 0.0) * eatNum
presentation.initView()
}
}
}
fun updateQrCodeImage(qrCodeUrl: String?) {
// scanQrCodePaypresentation.loadQrCodeImage(qrCodeUrl)
presentation.loadQrCodeImage(qrCodeUrl)
}
private var debouncer = Debouncer(5000)
fun showPaySuccess(isVip: Boolean) {
debouncer.debounce {
playSoundOnPaySuccess()
EventBus.getDefault().post(PaySuccessEvent())
showPayInfo(type = 2, isVip = isVip, memberInfo = memberInfo)
hidePayTab()
}
}
fun getQrCodeImg(
orderId: String,
userId: String? = null,
totalFee: String? = null,
block: (String?) -> Unit
) {
userViewModel.getQrCodeImg(
orderId = orderId,
userId = userId,
totalFee = totalFee
) { qrCodeImg ->
runOnUiThread {
if (qrCodeImg.isNullOrBlank()) {
//ToastUtils.showToast("获取二维码失败")
return@runOnUiThread
}
block(qrCodeImg)
}
}
}
fun cashPay(param: HashMap<String, String>, block: (Boolean) -> Unit) {
userViewModel.cashPay(param) {
runOnUiThread {
block(it)
}
}
}
fun memberPay(param: HashMap<String, String?>, block: () -> Unit) {
userViewModel.memberPay(param) {
runOnUiThread {
if (it.not()) {
//ToastUtils.showToast("支付失败,请稍后重试")
return@runOnUiThread
}
block()
}
}
}
suspend fun queryOrderState(block: (Boolean) -> Unit) {
userViewModel.queryOrderState(orderId = foodOrderId) { payResult ->
runOnUiThread {
if (payResult) {
block(true)
} else {
block(false)
}
}
}
}
fun hidePayTab() {
binding.llPayTab.gone()
}
fun getMemberInfoByPhone(phone: String, key: String, block: (MemberInfo?) -> Unit) {
userViewModel.getMemberInfoByPhone(phone, key) { memberInfo ->
runOnUiThread {
block(memberInfo)
}
}
}
fun getMemberInfoById(userId: String, block: (MemberInfo?) -> Unit) {
userViewModel.getMemberInfoById(memberId = userId) { memberInfo ->
runOnUiThread {
block(memberInfo)
}
}
}
fun bindOrder(userId: String, block: (Boolean) -> Unit) {
//0-即放即取,1-余量计量
val pickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0)
val mode = if (pickupMode == 1) 1 else 2
val uid = userId.toLongOrNull() ?: run {
runOnUiThread { block(false) }
return
}
userViewModel.bindUserOrder(
userId = uid,
orderNo = foodOrderId,
mode = mode,
onSuccess = { runOnUiThread { block(true) } },
onFailure = { runOnUiThread { block(false) } }
)
}
fun qrCodePay(scanInfo: String) {
showWaitingDialog("支付中……")
userViewModel.qrCodePay(scanInfo, foodOrderId, memberInfo?.id) { payState, backData ->
Timber.d("qrCodePay支付返回数据:state=$payState,backData=$backData")
hideWaitingDialog()
if (payState) {
showPaySuccess(isMember)
}
}
}
// fun bindOrder(userId: String, block: () -> Unit) {
// val pickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0)
// val mode = if (pickupMode == 1) 1 else 2
// userViewModel.bindOrder(userId, foodOrderId, mode = mode) { bindResult ->
// runOnUiThread {
// if (bindResult.not()) {
// hideWaitingDialog()
// //ToastUtils.showToast("订单绑定失败")
// return@runOnUiThread
// }
// binding.root.postDelayed({
// hideWaitingDialog()
// showPayInfo(type = 1, isVip = true, memberInfo = memberInfo)
// hidePayTab()
// block()
// }, 1000)
// }
// }
// }
// fun getMemberInfoByPhone(phone: String, key: String, block: (MemberInfo) -> Unit) {
// userViewModel.getMemberInfoByPhone(phone, key) { memberInfo ->
// runOnUiThread {
// if (memberInfo == null) {
// hideWaitingDialog()
// //ToastUtils.showToast("查询会员信息失败,请稍后重试")
// return@runOnUiThread
// }
// block(memberInfo)
// }
// }
// }
// fun getMemberInfo(userId: String, block: (MemberInfo) -> Unit) {
// userViewModel.getMemberInfoById(memberId = userId) { memberInfo ->
// runOnUiThread {
// if (memberInfo == null) {
// hideWaitingDialog()
// //ToastUtils.showToast("查询会员信息失败,请稍后重试")
// return@runOnUiThread
// }
// block(memberInfo)
// }
// }
// }
fun addBackEventListener() {
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
}
})
}
}
@@ -0,0 +1,88 @@
package com.sw.dualscreen.activity
import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
import androidx.fragment.app.Fragment
import com.sw.dualscreen.R
import com.sw.dualscreen.activity.fragment.BusinessFragment
import com.sw.dualscreen.activity.fragment.CollectFragment
import com.sw.dualscreen.databinding.ActivitySettingBinding
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.ClickBackEvent
import com.sw.dualscreen.model.response.UpdateRefreshEvent
import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.NetViewModelV2
import org.greenrobot.eventbus.EventBus
class SettingActivity : BaseActivity<ActivitySettingBinding>() {
val viewModel by viewModels<NetViewModelV2>()
override fun getViewModel(): BaseViewModel {
return viewModel
}
override fun inflateViewBinding(): ActivitySettingBinding {
return ActivitySettingBinding.inflate(layoutInflater)
}
private val fragmentList = mutableListOf<Fragment>().apply {
add(BusinessFragment())
add(CollectFragment())
}
override fun initialize() {
super.initialize()
registerKeyEvent()
initBackDispatcher()
binding.include.let {
it.ivPageBack.setOnClickListener {
//// val intent = Intent(this, MainActivity::class.java)
//// startActivity(intent)
// EventBus.getDefault().post(ClickBackEvent())
finish()
}
it.tvPageTitle.text = "设置"
}
binding.rgSetting.setOnCheckedChangeListener { group, checkedId ->
if (checkedId == R.id.rbBusiness) {
showFragment(fragmentList[0])
return@setOnCheckedChangeListener
}
if (checkedId == R.id.rbCollect) {
EventBus.getDefault().post(UpdateRefreshEvent())
showFragment(fragmentList[1])
return@setOnCheckedChangeListener
}
}
binding.rgSetting.check(R.id.rbBusiness)
}
private fun showFragment(fragment: Fragment) {
runCatching {
supportFragmentManager.beginTransaction().apply {
replace(R.id.fragmentContainerView, fragment)
commit()
}
}.onFailure {
it.printStackTrace()
}
}
fun searchByFoodName(foodName:String, action: (List<FoodInfo>) -> Unit) {
viewModel.searchByFoodName(foodName, action)
}
private lateinit var backPressedCallback: OnBackPressedCallback
private fun initBackDispatcher() {
// 创建回调,true表示初始启用状态
// backPressedCallback = object : OnBackPressedCallback(true) {
// override fun handleOnBackPressed() {
//// EventBus.getDefault().post(ClickBackEvent())
// }
// }
// // 注册回调,使用lifecycleOwner确保生命周期安全
// onBackPressedDispatcher.addCallback(this, backPressedCallback)
}
}
@@ -0,0 +1,60 @@
package com.sw.dualscreen.activity.fragment
import android.content.Context
import android.hardware.display.DisplayManager
import android.os.Bundle
import android.view.Display
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
import com.sw.dualscreen.model.response.PostEvent
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
abstract class BaseFragment<VB : ViewBinding>: Fragment() {
val displays: Array<Display> by lazy {
val displayManager = requireActivity().getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
displayManager.displays
}
protected lateinit var binding: VB
protected abstract fun inflateViewBinding(): VB
abstract fun initialize()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = inflateViewBinding()
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initialize()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onPostEvent(event: PostEvent) {
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
EventBus.getDefault().register(this)
}
override fun onDestroy() {
super.onDestroy()
EventBus.getDefault().unregister(this)
}
}
@@ -0,0 +1,107 @@
package com.sw.dualscreen.activity.fragment
import android.view.LayoutInflater
import com.sw.dualscreen.GlobalKey
import com.sw.dualscreen.R
import com.sw.dualscreen.activity.SettingActivity
import com.sw.dualscreen.databinding.FragmentBusinessBinding
import com.sw.dualscreen.model.response.ChargeModeEvent
import com.sw.dualscreen.model.response.ResetRecognizeEvent
import com.sw.dualscreen.sdk.SensorScaleUtils
import com.sw.dualscreen.utils.SPUtil
import com.sw.dualscreen.utils.SpTool
import com.sw.plate.utils.ToastUtils
import org.greenrobot.eventbus.EventBus
import kotlin.math.roundToInt
class BusinessFragment: BaseFragment<FragmentBusinessBinding>() {
// 当前秤的实时读数(克),作为待保存的餐具重量
private var dishWeight: Int = 0
override fun inflateViewBinding(): FragmentBusinessBinding {
return FragmentBusinessBinding.inflate(LayoutInflater.from(context))
}
override fun initialize() {
// 初始化为已保存的餐具重量,未配置过则为 0
dishWeight = SPUtil.getInstance().get(GlobalKey.KEY_DISH_WEIGHT, 0) ?: 0
// 在标题后显示当前已保存的餐具重量
refreshDishWeightLabel()
binding.rgSettlement.let {
//val settlementMode = if (binding.rgSettlement.checkedRadioButtonId == R.id.rbJointPayment) 0 else 1
//val mode = SPUtil.getInstance().get(GlobalKey.KEY_SETTLEMENT_MODE, 1)
it.check(if (SpTool.settlementMode == 0) R.id.rbJointPayment else R.id.rbIndependentPayment)
binding.rbJointPayment.isEnabled = false
binding.rbIndependentPayment.isEnabled = false
}
binding.rgCharge.let {
//0-计费,1-不计费
val mode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
it.check(if (mode == 0) R.id.rbChargeYes else R.id.rbChargeNot)
}
binding.rgTakeFood.let {
//0-即放即取,1-余量计量
val mode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0)
it.check(if (mode == 0) R.id.rbPickAndPlace else R.id.rbSurplusCalculate)
}
binding.btnClearZero.setOnClickListener {
SensorScaleUtils.tare()
}
binding.btnConfirm.setOnClickListener { v ->
// if (binding.rgSettlement.checkedRadioButtonId == -1) {
// ToastUtils.showToast("请设置结算模式")
// return@setOnClickListener
// }
if (binding.rgCharge.checkedRadioButtonId == -1) {
ToastUtils.showToast("请设置计费模式")
return@setOnClickListener
}
if (binding.rgTakeFood.checkedRadioButtonId == -1) {
ToastUtils.showToast("请设置取餐模式")
return@setOnClickListener
}
val chargeMode = if (binding.rgCharge.checkedRadioButtonId == R.id.rbChargeYes) 0 else 1
//原来的支付模式
//0-计费,1-不计费
val oldChargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
if (chargeMode != oldChargeMode) {
//支付模式有变化通知首页页面刷新
EventBus.getDefault().post(ChargeModeEvent(chargeMode))
}
SPUtil.getInstance().put(GlobalKey.KEY_CHARGE_MODE, chargeMode)
val pickupMode = if (binding.rgTakeFood.checkedRadioButtonId == R.id.rbPickAndPlace) 0 else 1
SPUtil.getInstance().put(GlobalKey.KEY_PICKUP_MODE, pickupMode)
// 持久化餐具重量(克)
SPUtil.getInstance().put(GlobalKey.KEY_DISH_WEIGHT, dishWeight)
// 刷新标签文字,显示当前已保存的餐具重量
refreshDishWeightLabel()
EventBus.getDefault().post(ResetRecognizeEvent())
//ToastUtils.showToast("设置已保存")
(activity as SettingActivity).let {
it.showWaitingDialog("设置保存中")
v.postDelayed({
it.hideWaitingDialog()
it.finish()
}, 1000)
}
}
SensorScaleUtils.addWeightListener { value ->
//val realWeight = (value * 1000).roundToInt()
// 追踪秤的实时读数(克),作为待保存的餐具重量
dishWeight = value
binding.tvFoodWeight.text = "$value"
}
}
/**
* 刷新"餐具重量(克)"标签,在末尾显示当前已保存的配置值。
* 未配置(0)时只显示原标题。
*/
private fun refreshDishWeightLabel() {
binding.tvDishWeightLabel.text = if (dishWeight > 0) "餐具重量(克)${dishWeight}g" else "餐具重量(克)"
}
}
@@ -0,0 +1,440 @@
package com.sw.dualscreen.activity.fragment
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import androidx.camera.view.PreviewView
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.R
import com.sw.dualscreen.activity.CollectedFoodActivity
import com.sw.dualscreen.activity.SettingActivity
import com.sw.dualscreen.adapter.CollectFoodListAdapter
import com.sw.dualscreen.adapter.FoodCollectionAdapter
import com.sw.dualscreen.databinding.FragmentCollectBinding
import com.sw.dualscreen.databinding.LayoutCameraPreviewBinding
import com.sw.dualscreen.ext.clickWithDebounce
import com.sw.dualscreen.ext.dp
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.objbox.Food
import com.sw.dualscreen.objbox.FoodCollectionBean
import com.sw.dualscreen.objbox.FoodModule
import com.sw.dualscreen.objbox.ObjectBox
import com.sw.dualscreen.utils.BitmapSaver
import com.sw.dualscreen.utils.CameraUtils
import com.sw.dualscreen.utils.Debouncer
import com.sw.dualscreen.utils.ImageUploader
import com.sw.dualscreen.utils.ImageUtil
import com.sw.plate.utils.ToastUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import timber.log.Timber
@SuppressLint("NotifyDataSetChanged")
class CollectFragment : BaseFragment<FragmentCollectBinding>() {
companion object {
private const val TAG = "CollectFragment"
const val MAX_COUNT = 9
}
private var selectedFoodId: String? = ""
private var selectedFoodName: String? = ""
private val foodCollectionList = mutableListOf<FoodCollectionBean>().apply {
repeat(MAX_COUNT) {
add(FoodCollectionBean(isShowCamera = true))
}
}
private val searchFoodList = mutableListOf<FoodInfo>()
private var settingActivity: SettingActivity? = null
private var checkedItem: FoodInfo? = null
private val searchFoodAdapter by lazy {
CollectFoodListAdapter(searchFoodList).apply {
setOnItemClickListener { adapter, view, position ->
searchFoodList.forEachIndexed { index, item -> item.isChecked = index == position }
checkedItem = searchFoodList[position]
notifyDataSetChanged()
selectedFoodId = checkedItem!!.foodId
selectedFoodName = checkedItem!!.foodName
}
}
}
private val debouncer = Debouncer(2000)
private lateinit var previewView: PreviewView
private val cameraUtils: CameraUtils by lazy {
CameraUtils(requireActivity())
}
private val collectionAdapter: FoodCollectionAdapter by lazy {
FoodCollectionAdapter(foodCollectionList).apply {
addOnItemChildClickListener(R.id.ivDelete) { _, _, position ->
foodCollectionList[position].let {
it.bitmap = null
it.imageVector = null
it.imageFile = null
it.imageUri = null
it.isShowCamera = true
it.isFinish = false
it.uploadSuccess = false
}
notifyItemChanged(position)
}
}
}
override fun inflateViewBinding(): FragmentCollectBinding {
return FragmentCollectBinding.inflate(LayoutInflater.from(context))
}
private val cameraCallback: (Uri) -> Unit = { uri ->
try {
activity?.lifecycleScope?.launch(Dispatchers.Main) {
// 主线程原子操作:查找空闲槽位并立即标记,避免竞态
val index = foodCollectionList.indexOfFirst { it.isShowCamera }
if (index == -1) {
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
settingActivity?.hideWaitingDialog()
return@launch
}
foodCollectionList[index].let {
it.imageVector = null
it.bitmap = null
it.isShowCamera = false
it.imageFile = null
it.imageUri = uri
}
collectionAdapter.notifyItemChanged(index)
// IO线程处理bitmap
launch(Dispatchers.IO) {
ImageUtil.uriToBitmap(requireActivity(), uri)?.let { bitmap ->
getImageVector(index, bitmap)
}
}
}
} catch (e: Exception) {
e.printStackTrace()
settingActivity?.hideWaitingDialog()
ToastUtils.showToast("程序异常${e.message}")
settingActivity?.log("程序异常${e.message}")
}
}
private val photoFailCallback: (String) -> Unit = { errMsg ->
settingActivity?.hideWaitingDialog()
}
private fun getImageVector(index: Int, bitmap: Bitmap) {
// val bitmap = BitmapCropper.cropCenter(
// original = srcBmp,
// targetWidth = 900, targetHeight = 900,
//// offsetX = 30, offsetY = 100
// )
val imageVector = try {
FoodModule.bitmap2FloatArray(bitmap, false)
} catch (e: Exception) {
e.printStackTrace()
activity?.lifecycleScope?.launch(Dispatchers.Main) {
ToastUtils.showToast("操作失败")
}
settingActivity?.log("操作失败:${e.message}")
settingActivity?.hideWaitingDialog()
return
}
val file = BitmapSaver.saveToAppFilesDir(
bitmap, requireActivity(), "IMG_CROP_${System.currentTimeMillis()}.jpg"
)
settingActivity?.log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}")
foodCollectionList[index].let {
it.imageVector = imageVector
it.bitmap = null
it.isShowCamera = false
it.imageFile = file
}
settingActivity?.hideWaitingDialog()
if (bitmap.isRecycled.not()) {
bitmap.recycle()
}
}
@SuppressLint("NotifyDataSetChanged")
private fun takePhoto() {
val count = foodCollectionList.count { it.bitmap != null }
if (count >= MAX_COUNT) {
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
return
}
settingActivity?.showWaitingDialog("采集中……")
cameraUtils.takePhoto(succCallback = cameraCallback, failCallback = photoFailCallback)
}
private var cameraErrorCount = 0
override fun initialize() {
settingActivity = activity as SettingActivity
cameraUtils.initCamera()
val previewBinding =
LayoutCameraPreviewBinding.inflate(layoutInflater, binding.flCameraPreview)
previewView = previewBinding.previewView.also {
it.updateLayoutParams {
width = 456.dp
height = 342.dp
}
}
cameraUtils.setPreviewController(previewView)
binding.rvFoodList.let {
it.layoutManager =
GridLayoutManager(requireActivity(), 3, GridLayoutManager.VERTICAL, false)
it.adapter = collectionAdapter
}
binding.btnFoodSearch.setOnClickListener {
searchFood()
}
binding.btnSave.setOnClickListener {
if (checkedItem == null || checkedItem!!.isChecked.not()) {
ToastUtils.showToast("请选择菜品名称")
return@setOnClickListener
}
val count = foodCollectionList.count { it.imageFile != null }
if (count == 0) {
ToastUtils.showToast("请拍摄菜品照片")
return@setOnClickListener
}
upload()
}
binding.editFoodName.setOnEditorActionListener { v, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
searchFood()
val imm =
v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(v.windowToken, 0)
true
} else {
false
}
}
binding.btnCollectedFood.setOnClickListener {
startActivity(Intent(requireActivity(), CollectedFoodActivity::class.java))
}
binding.btnTakePhoto.clickWithDebounce {
val count = foodCollectionList.count { it.bitmap != null }
if (count >= MAX_COUNT) {
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
return@clickWithDebounce
}
takePhoto()
}
binding.btnClearData.setOnClickListener { clearData() }
binding.rvSearchFood.let {
it.layoutManager = GridLayoutManager(context, 2)
it.adapter = searchFoodAdapter
}
searchFood()
}
@SuppressLint("NotifyDataSetChanged")
private fun upload() {
lifecycleScope.launch {
val totalFileCount = foodCollectionList.count { it.imageFile != null }
settingActivity?.showWaitingDialog2("图片上传中0/$totalFileCount")
val foodId = checkedItem!!.foodId.toLongOrNull() ?: run {
ToastUtils.showToast("foodId 格式错误")
return@launch
}
val foodName = checkedItem!!.foodName ?: ""
val version = GlobalData.foodModelVersion
ImageUploader(totalList = foodCollectionList, uploadImage = { batch ->
val files = batch.mapNotNull { it.imageFile }
val foodVectorList = batch.filter { it.imageVector != null }.map {
it.imageVector!!.joinToString(
separator = ",", prefix = "[", postfix = "]"
)
}
val foodVectorJson =
foodVectorList.joinToString(separator = ",", prefix = "[", postfix = "]")
Timber.tag(TAG).d("json=$foodVectorJson")
settingActivity?.viewModel?.uploadCollect(
foodId = foodId,
foodName = foodName,
version = version,
foodVector = foodVectorJson,
fileList = files
)
}, onProgress = { count, batch, idList ->
//batch.forEach {
// it.uploadSuccess = true
//}
runBlocking {
activity?.runOnUiThread {
settingActivity?.showWaitingDialog2("图片上传中$count/$totalFileCount")
}
val foodList = batch.mapIndexed { index, it ->
Food(
collectId = if (index < idList.size) idList[index] else null,
foodId = checkedItem!!.foodId,
foodName = checkedItem!!.foodName,
foodVector = it.imageVector,
version = GlobalData.foodModelVersion
)
}
ObjectBox.putAll(foodList)
activity?.runOnUiThread {
batch.forEach { it.isFinish = true }
collectionAdapter.notifyDataSetChanged()
}
}
}, onError = {
activity?.runOnUiThread {
binding.root.postDelayed({
settingActivity?.hideWaitingDialog()
ToastUtils.showToast("上传失败,请稍后重试")
}, 1000)
}
}, onComplete = {
//vectorThread()
binding.root.postDelayed({
settingActivity?.hideWaitingDialog()
ToastUtils.showToast("上传成功")
}, 1000)
}).processUploads()
}
// for (index in foodCollectionList.indices step 5) {
// val end = if(index + 5 < foodCollectionList.size - 1) index + 5 else foodCollectionList.size - 1
// val subList = foodCollectionList.subList(index, end)
// val subFiles = subList.map { it.imageFile }
// uploadCollectFoodPics(subFiles, params) { isSuccess->
// Timber.tag(TAG).d("uploadMultipleImages: ${isSuccess}")
// subList.filter { it.imageFile!=null }.forEach { it.uploadSuccess = isSuccess }
//
//// val count = collectList.count { it.imageFile!=null && it.uploadSuccess.not() }
//// runOnUiThread {
//// binding.btnUploadImage.text = "待上传图片${count}张"
//// if (count == 0) {
//// Loading.dismiss()
//// }
//// }
// }
// }
}
// private fun vectorThread() {
// settingActivity?.showWaitingDialog("加载中……")
// Thread {
// foodCollectionList
//// .filter { it.bitmap != null }
//// .filter { it.imageVector != null }
// .forEachIndexed { index, it ->
// if (it.imageVector == null) {
// return@forEachIndexed
// }
// image2VectorTask(imageVector = it.imageVector!!, index)
// }
// activity?.runOnUiThread {
// binding.root.postDelayed({
// settingActivity?.hideWaitingDialog()
// }, 1000)
// }
// }.start()
// }
// private fun image2VectorTask(imageVector: FloatArray?, position: Int) {
// if (imageVector == null) return
////// val imageVector = FoodModule.bitmap2FloatArray(item.bitmap!!)
//////
//////// val base64Str = FloatBase64Utils.floatArrayToBase64(imageVector)
//////// Timber.tag("mzf1").e(base64Str)
////////// viewModel.postImageData(
////////// context,
////////// foodId = selectedFoodId.toString(),
////////// foodName = selectedFoodName.toString(),
////////// foodVector = base64Str,
////////// uri = item.imageUri!!
////////// )
//// box.put(
//// Food(
//// collectId = null,
//// foodId = checkedItem!!.foodId,
//// foodName = checkedItem!!.foodName,
//// foodVector = imageVector,
//// version = "1.0.0"
//// )
//// )
//// if (position > -1) {
//// foodCollectionList[position].isFinish = true
//// activity?.runOnUiThread {
//// collectionAdapter.notifyItemChanged(position)
//// }
//// }
// }
@SuppressLint("NotifyDataSetChanged")
private fun searchFood() {
debouncer.debounce {
settingActivity?.searchByFoodName(binding.editFoodName.text.toString()) {
searchFoodList.clear()
searchFoodList.addAll(it)
searchFoodAdapter.notifyDataSetChanged()
}
}
}
private var clickIndex = -1
@SuppressLint("NotifyDataSetChanged")
private fun clearData() {
foodCollectionList.forEach {
it.bitmap = null
it.imageVector = null
it.imageFile = null
it.imageUri = null
it.isShowCamera = true
it.isFinish = false
it.uploadSuccess = false
}
collectionAdapter.notifyDataSetChanged()
clickIndex = -1
binding.editFoodName.setText("")
searchFood()
}
override fun onResume() {
super.onResume()
cameraUtils.bind()
binding.llCameraFlag.run {
visibility = View.VISIBLE
postDelayed({
visibility = View.GONE
}, 3000)
}
}
override fun onPause() {
super.onPause()
cameraUtils.unbind()
binding.llCameraFlag.visibility = View.VISIBLE
}
}
@@ -0,0 +1,104 @@
package com.sw.dualscreen.activity.fragment.pay
import android.text.Spanned
import android.text.SpannedString
import android.text.style.AbsoluteSizeSpan
import androidx.core.text.buildSpannedString
import com.sw.dualscreen.activity.PayActivity
import com.sw.dualscreen.activity.fragment.BaseFragment
import com.sw.dualscreen.databinding.FragmentCashPayBinding
import com.sw.dualscreen.ext.format2String
import com.sw.dualscreen.presentation.pay.PayPresentation
import com.sw.plate.utils.ToastUtils
class CashPayFragment : BaseFragment<FragmentCashPayBinding>() {
private lateinit var payActivity: PayActivity
private var foodName: String? = null
private var payAmount: Double? = null
override fun inflateViewBinding(): FragmentCashPayBinding {
return FragmentCashPayBinding.inflate(layoutInflater)
}
override fun initialize() {
payActivity = requireActivity() as PayActivity
payActivity.foodInfo?.let {
foodName = it.foodName
payAmount = (it.specPrice ?: 0.0) * payActivity.eatNum
}
binding.tvRealAmount.text = getAmountText(payAmount.format2String(2))
binding.btnConfirmPayFinish.setOnClickListener {
payActivity.cashPay(
hashMapOf(
//标价金额(单位为元)
"totalFee" to payAmount.format2String(2),
//订单类型(0付款1会员充值)
"orderType" to "0",
//商户订单号
"orderNo" to payActivity.foodOrderId,
//支付来源:0线下收款 、3:线上纯会员支付 101:支付宝主动、102:支付宝被动、201:微信主动、202:微信被动
"paySource" to "0"
)
) { paySuccess ->
if (paySuccess.not()) {
ToastUtils.showToast("接口调用失败")
return@cashPay
}
payActivity.showPaySuccess(false)
}
}
showSubScreen()
}
// override fun onHiddenChanged(hidden: Boolean) {
// super.onHiddenChanged(hidden)
// if (hidden) {
// dismissSubScreen()
// return
// }
// //showSubScreen()
// }
//
// override fun onDestroy() {
// dismissSubScreen()
// super.onDestroy()
// }
//
// private fun dismissSubScreen() {
// }
private fun showSubScreen() {
// // 查找副屏(通常索引为1)
// if (displays.size > 1) {
// dismissSubScreen()
// presentation = PayPresentation(
// activity = payActivity,
// pageType = PayPresentation.CASH_PAY,
// userViewModel = payActivity.userViewModel,
// recognizeViewModel = payActivity.recognizeViewModel,
// display = displays[1]
// ) {
// presentation?.dismiss()
// }.also {
// it.foodName = foodName
// it.payAmount = payAmount.format2String(2)
// }
// presentation?.show()
// }
payActivity.presentation.let {
it.pageType = PayPresentation.CASH_PAY
it.foodName = foodName
it.payAmount = payAmount.format2String(2)
it.initView()
}
}
private fun getAmountText(amount: String): SpannedString {
return buildSpannedString {
append("¥", AbsoluteSizeSpan(36, true), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
append(amount, AbsoluteSizeSpan(60, true), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
}
@@ -0,0 +1,103 @@
package com.sw.dualscreen.activity.fragment.pay
import android.graphics.Bitmap
import com.sw.dualscreen.activity.PayActivity
import com.sw.dualscreen.activity.fragment.BaseFragment
import com.sw.dualscreen.databinding.FragmentFacePayBinding
import com.sw.dualscreen.ext.format2String
import com.sw.dualscreen.presentation.pay.PayPresentation
class FacePayFragment : BaseFragment<FragmentFacePayBinding>() {
private lateinit var payActivity: PayActivity
private var foodName: String? = null
private var payAmount: Double? = null
override fun inflateViewBinding(): FragmentFacePayBinding {
return FragmentFacePayBinding.inflate(layoutInflater)
}
override fun initialize() {
payActivity = activity as PayActivity
// binding.tvFaceState.setOnClickListener {
// payActivity?.showPayResult()
// }
payActivity.foodInfo?.let {
foodName = it.foodName
// payAmount = (it.vipPrice ?: 0.0) * payActivity.eatNum
val price = it.specPrice ?: 0.0
payAmount = price * payActivity.memberDiscount * payActivity.eatNum
}
showSubScreen()
}
//
// override fun onHiddenChanged(hidden: Boolean) {
// super.onHiddenChanged(hidden)
// if (hidden) {
//// dismissSubScreen()
// payActivity.presentation.pauseCamera()
// return
// }
// payActivity.presentation.resumeCamera()
// //showSubScreen()
// }
// override fun onDestroy() {
// dismissSubScreen()
// super.onDestroy()
// }
// private fun dismissSubScreen() {
// presentation?.let {
// if (it.isShowing) {
// it.dismiss()
// }
// }
// }
fun loadBitmap(frame: Bitmap) {
binding.ivFaceImage.setImageBitmap(frame)
}
private fun showSubScreen() {
// 查找副屏(通常索引为1
// if (displays.size > 1) {
// dismissSubScreen()
// presentation = PayPresentation(
// activity = payActivity,
// pageType = PayPresentation.FACE_PAY,
// userViewModel = payActivity.userViewModel,
// recognizeViewModel = payActivity.recognizeViewModel,
// display = displays[1]
// ) {
// presentation?.dismiss()
// }.also {
// it.foodName = foodName
// it.payAmount = payAmount.format2String(2)
// }
// presentation?.show()
// }
payActivity.presentation.let {
it.pageType = PayPresentation.FACE_PAY
it.foodName = foodName
it.payAmount = payAmount.format2String(2)
isResumeCamera = true
it.initView()
}
}
override fun onPause() {
super.onPause()
payActivity.presentation.pauseCamera()
}
private var isResumeCamera = false
override fun onResume() {
super.onResume()
if (isResumeCamera) {
payActivity.presentation.resumeCamera()
}
}
}
@@ -0,0 +1,185 @@
package com.sw.dualscreen.activity.fragment.pay
import android.text.InputType
import com.sw.dualscreen.R
import com.sw.dualscreen.activity.PayActivity
import com.sw.dualscreen.activity.fragment.BaseFragment
import com.sw.dualscreen.databinding.FragmentNumberPayBinding
import com.sw.dualscreen.ext.format2String
import com.sw.dualscreen.ext.hideKeyboard
import com.sw.dualscreen.presentation.pay.PayPresentation
import com.sw.plate.utils.ToastUtils
class NumberPayFragment : BaseFragment<FragmentNumberPayBinding>() {
private lateinit var payActivity: PayActivity
private var foodName: String? = null
private var payAmount: Double? = null
// 密码是否显示的标志
private var isPasswordVisible = false
override fun inflateViewBinding(): FragmentNumberPayBinding {
return FragmentNumberPayBinding.inflate(layoutInflater)
}
override fun initialize() {
binding.root.setOnClickListener { v ->
v.hideKeyboard()
}
payActivity = requireActivity() as PayActivity
payActivity.foodInfo?.let {
foodName = it.foodName
payAmount = (it.specPrice ?: 0.0) * payActivity.eatNum
}
// 密码显示/隐藏切换
binding.ivTogglePassword.setOnClickListener {
togglePasswordVisibility()
}
binding.btnConfirm.setOnClickListener { v ->
val phone = binding.etInputPhone.text.toString().trim()
val key = binding.etInputNumber.text.toString().trim()
if (phone.isBlank() || key.isBlank()) {
ToastUtils.showToast("请输入会员验证信息")
return@setOnClickListener
}
payActivity.showWaitingDialog("加载中,请稍后……")
v.hideKeyboard()
// payActivity.getMemberInfoByPhone(phone, key) { memberInfo ->
// payActivity.bindOrder(memberInfo.id?:"") {
// ToastUtils.showToast("会员验证成功")
// }
// }
payActivity.isMember = false
payActivity.getMemberInfoByPhone(phone, key) { memberInfo ->
if (memberInfo == null) {
binding.root.postDelayed({
payActivity.hideWaitingDialog()
ToastUtils.showToast("未查询到会员信息,请稍后重试")
}, 1000)
return@getMemberInfoByPhone
}
//肯定是会员
memberInfo.member = true
payActivity.isMember = true
//绑定订单使用faceUserId
payActivity.bindOrder(memberInfo.faceUserId ?: "") { bindResult ->
if (bindResult.not()) {
payActivity.hideWaitingDialog()
//ToastUtils.showToast("订单绑定失败")
return@bindOrder
}
binding.root.postDelayed({
payActivity.hideWaitingDialog()
payActivity.showPayInfo(type = 1, isVip = memberInfo.member, memberInfo = memberInfo)
payActivity.hidePayTab()
}, 1000)
}
}
// getMemberInfo(phone, key) { memberInfo ->
// bindOrder(memberInfo.id ?: "") {
// binding.root.postDelayed({
// payActivity.hideWaitingDialog()
// payActivity.showPayInfo(type = 1, isVip = true, memberInfo = memberInfo)
// payActivity.hidePayTab()
// }, 1000)
// }
// }
}
showSubScreen()
}
// override fun onHiddenChanged(hidden: Boolean) {
// super.onHiddenChanged(hidden)
// if (hidden) {
// dismissSubScreen()
// return
// }
// //showSubScreen()
// }
//
// override fun onDestroy() {
// dismissSubScreen()
// super.onDestroy()
// }
// private fun dismissSubScreen() {
// presentation?.let {
// if (it.isShowing) {
// it.dismiss()
// }
// }
// }
private fun showSubScreen() {
// 查找副屏(通常索引为1
// if (displays.size > 1) {
// dismissSubScreen()
// presentation = PayPresentation(
// activity = payActivity,
// pageType = PayPresentation.CASH_PAY,
// userViewModel = payActivity.userViewModel,
// recognizeViewModel = payActivity.recognizeViewModel,
// display = displays[1]
// ).also {
// it.foodName = foodName
// it.payAmount = payAmount.format2String(2)
// }
// presentation?.show()
// }
payActivity.presentation.let {
it.pageType = PayPresentation.CASH_PAY
it.foodName = foodName
it.payAmount = payAmount.format2String(2)
it.initView()
}
}
// 切换密码显示/隐藏
private fun togglePasswordVisibility() {
val editText = binding.etInputNumber
val imageView = binding.ivTogglePassword
// 保存当前光标位置
val cursorPosition = editText.selectionStart
if (isPasswordVisible) {
// 当前显示,改为隐藏
editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
imageView.setImageResource(R.drawable.ic_eye_close)
isPasswordVisible = false
} else {
// 当前隐藏,改为显示
editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
imageView.setImageResource(R.drawable.ic_eye_open)
isPasswordVisible = true
}
// 恢复光标位置
editText.setSelection(cursorPosition)
}
// private fun bindOrder(userId: String, block: () -> Unit) {
// payActivity.bindOrder(userId) { bindResult ->
// if (bindResult.not()) {
// payActivity.hideWaitingDialog()
// //ToastUtils.showToast("订单绑定失败")
// return@bindOrder
// }
// block()
// }
// }
// private fun getMemberInfo(phone: String, key: String, block: (MemberInfo) -> Unit) {
// payActivity.getMemberInfoByPhone(phone, key) { memberInfo ->
// if (memberInfo == null) {
// payActivity.hideWaitingDialog()
// //ToastUtils.showToast("查询会员信息失败,请稍后重试")
// return@getMemberInfoByPhone
// }
// block(memberInfo)
// }
// }
}
@@ -0,0 +1,319 @@
package com.sw.dualscreen.activity.fragment.pay
import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import com.sw.dualscreen.R
import com.sw.dualscreen.activity.PayActivity
import com.sw.dualscreen.activity.fragment.BaseFragment
import com.sw.dualscreen.databinding.FragmentPayResultBinding
import com.sw.dualscreen.ext.format2String
import com.sw.dualscreen.ext.gone
import com.sw.dualscreen.ext.invisible
import com.sw.dualscreen.ext.load
import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.model.response.MemberInfo
import com.sw.dualscreen.model.response.PaySuccessEvent
import com.sw.dualscreen.model.response.TextBean
import com.sw.dualscreen.utils.IntervalExecutor
import com.sw.dualscreen.utils.SpannedUtils
import com.sw.dualscreen.utils.countDownByFlow
import kotlinx.coroutines.Job
import org.greenrobot.eventbus.EventBus
class PayResultFragment : BaseFragment<FragmentPayResultBinding>() {
//type = 0,扫码支付默认显示二维码
//type = 1,会员结算显示二维码,是会员则显示名字、头像、手机号、可用余额
//type = 2,支付成功,是会员则显示名字、头像、手机号、可用余额
companion object {
const val PAGE_TYPE = "pageType"
const val MEMBER_INFO = "memberInfo"
fun instance(pageType: Int, memberInfo: MemberInfo?): PayResultFragment {
return PayResultFragment().apply {
arguments = Bundle().also {
it.putInt(PAGE_TYPE, pageType)
it.putParcelable(MEMBER_INFO, memberInfo)
}
}
}
}
private var pageType: Int = 1
private var memberInfo: MemberInfo? = null
override fun inflateViewBinding(): FragmentPayResultBinding {
return FragmentPayResultBinding.inflate(layoutInflater)
}
private var countDownJob: Job? = null
private fun closePageCountDown() {
countDownJob = countDownByFlow(
total = 3,
scope = lifecycleScope,
onStart = {
binding.btnBack.text = "返回(3s)..."
},
onTick = { seconds ->
binding.btnBack.text = "返回(${seconds}s)..."
},
onFinish = {
payActivity.finish()
}
)
}
private lateinit var payActivity: PayActivity
override fun initialize() {
payActivity = activity as PayActivity
binding.btnBack.setOnClickListener {
activity?.finish()
}
arguments?.let {
pageType = it.getInt(PAGE_TYPE, 0)
memberInfo = it.getParcelable(MEMBER_INFO)
}
if (memberInfo != null && memberInfo!!.member) {
// totalPrice = (payActivity.foodInfo?.vipPrice ?: 0.0) * payActivity.eatNum
val price = payActivity.foodInfo?.specPrice ?: 0.0
totalPrice = price * payActivity.memberDiscount * payActivity.eatNum
binding.layoutVip.visible()
balance = (memberInfo!!.topUpBalance ?: 0.0) + (memberInfo!!.rewardBalance ?: 0.0)
realPayPrice = if (balance >= totalPrice) 0.0 else totalPrice - balance
expensesBalance = if (balance >= totalPrice) totalPrice else balance
loadUserInfo(memberInfo!!)
} else {
binding.layoutVip.gone()
totalPrice = (payActivity.foodInfo?.specPrice ?: 0.0) * payActivity.eatNum
}
when (pageType) {
0 -> {
}
1 -> {
binding.layoutPayInfo.visible()
binding.layoutPaySuccess.gone()
binding.tvRealAmount.text = SpannedUtils.getAmountText(
listOf(
TextBean(text = "¥", textSize = 36),
TextBean(text = totalPrice.format2String(2), textSize = 60),
)
)
//余额扣除 -36.80 元,扣除后可用余额 10.00 元
//余额扣除 -16.80 元,还需支付 20.00 元
binding.tvUserBalance.text = SpannedUtils.getAmountText(
listOf(
TextBean(text = "¥", textSize = 24),
TextBean(text = balance.format2String(2), textSize = 36),
)
)
if (memberInfo != null && memberInfo!!.member) {
binding.tvAccountInfo.text = SpannedUtils.getAmountText(
getAmountList()
)
} else {
binding.tvAccountInfo.text = SpannedUtils.getAmountText(
getAmountListNotMember()
)
}
if (balance >= totalPrice) {
binding.ivPayQrCode.invisible()
binding.btnConfirmPay.run {
isEnabled = true
//点击支付,接口成功打开成功页面
setOnClickListener {
payActivity.showWaitingDialog("支付中,请稍后……")
memberPay(totalPrice.format2String(2)) {
binding.root.postDelayed({
payActivity.hideWaitingDialog()
payActivity.showPaySuccess(true)
}, 1000)
}
}
}
} else {
binding.ivPayQrCode.visible()
binding.btnConfirmPay.isEnabled = false
val payAmount = totalPrice - balance
payActivity.getQrCodeImg(
orderId = payActivity.foodOrderId,
userId = memberInfo!!.id,
//混合支付,实际支付金额
//totalFee = payAmount.format2String(2)
) { qrCodeImg ->
binding.ivPayQrCode.load(qrCodeImg)
//更新副屏二维码
payActivity.updateQrCodeImage(qrCodeImg)
}
paySuccessCallback {
//扫码成功回调打开成功页面,后端处理余额扣除
payActivity.showPaySuccess(true)
// if (balance > 0.0) {
// memberPay(balance.format2String(2)) {
// //扫码成功+会员支付完成后打开成功页面
// payActivity?.showPaySuccess(true)
// }
// } else {
// }
}
}
}
2 -> {
binding.layoutPayInfo.gone()
binding.layoutPaySuccess.visible()
if (memberInfo != null) {
val remainBalance = if (balance >= totalPrice) balance - totalPrice else 0.0
binding.tvUserBalance.text = SpannedUtils.getAmountText(
listOf(
TextBean(text = "¥", textSize = 24),
TextBean(text = remainBalance.format2String(2), textSize = 36),
)
)
val showTotalPrice = totalPrice.format2String(2)
binding.tvPayAmount.text = "收款金额 $showTotalPrice"
val showExpensesBalance = expensesBalance.format2String(2)
binding.tvPayInfo.text =
"应收 $showTotalPrice 元,余额扣除 $showExpensesBalance"
} else {
binding.tvPayAmount.text = "收款金额 ${totalPrice.format2String(2)}"
binding.tvPayInfo.text = "应收 ${totalPrice.format2String(2)}"
}
closePageCountDown()
}
else -> {}
}
}
//总价格
private var totalPrice = 0.0
//余额
private var balance = 0.0
//实际支付金额
private var realPayPrice = 0.0
//扣除余额
private var expensesBalance = 0.0
private fun getAmountListNotMember(): List<TextBean> {
val list: MutableList<TextBean> = mutableListOf()
list.add(TextBean(text = "应付金额 ", textSize = 30, textColor = "#FF889AC2"))
list.add(
TextBean(
text = "-${totalPrice.format2String(2)}",
textSize = 30,
textColor = "#FF0A1428",
isBold = true
)
)
list.add(TextBean(text = "", textSize = 30, textColor = "#FF889AC2"))
return list
}
private fun getAmountList(): List<TextBean> {
val list: MutableList<TextBean> = mutableListOf()
list.add(TextBean(text = "余额扣除 ", textSize = 30, textColor = "#FF889AC2"))
list.add(
TextBean(
text = "-${expensesBalance.format2String(2)}",
textSize = 30,
textColor = "#FF0A1428",
isBold = true
)
)
if (balance >= totalPrice) {
//余额大于等于总价格,使用余额支付
list.add(TextBean(text = " 元,扣除后可用余额 ", textSize = 30, textColor = "#FF889AC2"))
val remainingBalance = balance - totalPrice
list.add(
TextBean(
text = remainingBalance.format2String(2),
textSize = 30,
textColor = "#FF0A1428",
isBold = true
)
)
} else {
//余额小于总价格,使用余额+扫码支付
//实际支付金额
list.add(TextBean(text = " 元,还需支付 ", textSize = 30, textColor = "#FF889AC2"))
list.add(
TextBean(
text = realPayPrice.format2String(2),
textSize = 30,
textColor = "#FF0A1428",
isBold = true
)
)
}
list.add(TextBean(text = "", textSize = 30, textColor = "#FF889AC2"))
return list
}
private fun loadUserInfo(item: MemberInfo) {
binding.ivHeadPic.load(
if(item.faceUrl.isNullOrBlank()) R.drawable.ic_avatar_default
else item.faceUrl
)
binding.tvUserName.text = item.name
val phone = item.phone ?: ""
binding.tvUserPhone.text =
if (phone.length == 11)
phone.replace(phone.substring(3, 7), "****")
else
phone
}
private fun memberPay(total: String, callback: () -> Unit) {
payActivity.run {
memberPay(
param = hashMapOf(
//支付金额
"totalFee" to total,
//订单号
"orderNo" to payActivity.foodOrderId,
//用户id
"memberId" to memberInfo?.id
),
block = callback
)
}
}
override fun onDestroy() {
payTaskJob?.cancel()
super.onDestroy()
}
private val intervalExecutor by lazy { IntervalExecutor() }
private var payTaskJob: Job? = null
fun paySuccessCallback(callback: () -> Unit) {
payTaskJob = intervalExecutor.startIntervalTaskWithInitialDelay(200, 500) {
payActivity?.queryOrderState { paySuccess ->
if (paySuccess) {
callback()
payTaskJob?.cancel()
}
}
}
}
}
@@ -0,0 +1,143 @@
package com.sw.dualscreen.activity.fragment.pay
import android.text.Spanned
import android.text.SpannedString
import android.text.style.AbsoluteSizeSpan
import androidx.core.text.buildSpannedString
import androidx.lifecycle.lifecycleScope
import com.sw.dualscreen.activity.PayActivity
import com.sw.dualscreen.activity.fragment.BaseFragment
import com.sw.dualscreen.ext.load
import com.sw.dualscreen.utils.countDownByFlow
import kotlinx.coroutines.Job
import com.sw.dualscreen.databinding.FragmentScanQrcodePayBinding
import com.sw.dualscreen.ext.format2String
import com.sw.dualscreen.presentation.pay.PayPresentation
import com.sw.dualscreen.utils.IntervalExecutor
class ScanQrCodePayFragment : BaseFragment<FragmentScanQrcodePayBinding>() {
companion object {
}
private lateinit var payActivity: PayActivity
private var foodName: String? = null
private var payAmount: Double? = null
private var payQrCodePic: String? = null
private var countDownJob: Job? = null
override fun inflateViewBinding(): FragmentScanQrcodePayBinding {
return FragmentScanQrcodePayBinding.inflate(layoutInflater)
}
override fun initialize() {
payActivity = requireActivity() as PayActivity
payActivity.foodInfo?.let {
foodName = it.foodName
payAmount = (it.specPrice ?: 0.0) * payActivity.eatNum
}
binding.tvRealAmount.text = getAmountText(payAmount.format2String(2))
//不需要绑定会员id
payActivity.getQrCodeImg(
orderId = payActivity.foodOrderId,
//非会员扫码支付,不用传金额,后端处理,混合支付要传
//totalFee = payAmount.format2String(2)
) {
payQrCodePic = it
binding.ivPayQrCode.load(payQrCodePic)
binding.tvPayResult.text = "待支付(60s)..."
paySuccessCallback(false)
showSubScreen()
startCountDown()
}
}
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
// if (hidden) {
//// dismissSubScreen()
// return
// }
//showSubScreen()
}
override fun onDestroy() {
// dismissSubScreen()
countDownJob?.cancel() // 自动取消订阅,防止内存泄漏
payTaskJob?.cancel()
super.onDestroy()
}
private fun dismissSubScreen() {
// presentation?.let {
// if (it.isShowing) {
// it.dismiss()
// }
// }
}
private fun showSubScreen() {
// // 查找副屏(通常索引为1)
// if (displays.size > 1) {
// dismissSubScreen()
// presentation =
// ScanQrCodePayPresentation(activity = payActivity, type = 0, display = displays[1]) {
// presentation?.dismiss()
// }.also {
// it.foodName = foodName
// it.totalPrice = payAmount ?: 0.0
// it.payQrCodePic = payQrCodePic
// }
// presentation?.show()
// }
payActivity.presentation.let {
it.pageType = PayPresentation.QR_CODE_PAY
it.foodName = foodName
it.totalPrice = payAmount ?: 0.0
it.payQrCodePic = payQrCodePic
it.initView()
}
}
private fun getAmountText(amount: String): SpannedString {
return buildSpannedString {
append("¥", AbsoluteSizeSpan(36, true), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
append(amount, AbsoluteSizeSpan(60, true), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
private fun startCountDown() {
countDownJob = countDownByFlow(
total = 60,
scope = lifecycleScope,
onStart = {
binding.tvPayResult.text = "待支付(60s)..."
},
onTick = { seconds ->
binding.tvPayResult.text = "待支付(${seconds}s)..."
},
onFinish = {
binding.tvPayResult.text = "待支付"
}
)
}
private val intervalExecutor by lazy { IntervalExecutor() }
private var payTaskJob: Job? = null
fun paySuccessCallback(isVip: Boolean) {
payTaskJob = intervalExecutor.startIntervalTaskWithInitialDelay(200, 500) {
payActivity.queryOrderState { paySuccess ->
if (paySuccess) {
payActivity.showPaySuccess(isVip)
payTaskJob?.cancel()
}
}
}
}
}
@@ -0,0 +1,41 @@
package com.sw.dualscreen.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.graphics.toColorInt
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.dualscreen.R
import com.sw.dualscreen.databinding.ListItemSearchFood3Binding
import com.sw.dualscreen.ext.format2String
import com.sw.dualscreen.model.response.FoodInfo
class CollectFoodListAdapter(var list: MutableList<FoodInfo>) :
BaseQuickAdapter<FoodInfo, CollectFoodListAdapter.VH>(list) {
inner class VH(var binding: ListItemSearchFood3Binding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemSearchFood3Binding.inflate(inflater, parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: FoodInfo?) {
holder.binding.root.setBackgroundResource(R.drawable.bg_text_collect)
holder.binding.tvFoodName.run {
if (item!!.score == 0) {
text = item.foodName
} else {
val score = (item.score / 100.0).format2String(2)
text = item.foodName + (if (item.score != 0) "-${score}%" else "")
}
isChecked = item.isChecked
setTextColor(
if (isChecked) "#FF3232".toColorInt()
else "#5E7585".toColorInt()
)
}
}
}
@@ -1,34 +0,0 @@
package com.sw.dualscreen.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.dualscreen.R
import com.sw.dualscreen.databinding.ListItemCollectedDataBinding
import com.sw.dualscreen.databinding.ListItemFoodCollectionBinding
import com.sw.dualscreen.objbox.CollectedFoodBean
import com.sw.dualscreen.objbox.FoodCollectionBean
class CollectedFoodAdapter (var list: MutableList<CollectedFoodBean>) :
BaseQuickAdapter<CollectedFoodBean, CollectedFoodAdapter.VH>(list) {
inner class VH(var binding: ListItemCollectedDataBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemCollectedDataBinding.inflate(inflater, parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: CollectedFoodBean?) {
holder.binding.tvFoodName.text = item?.foodName
holder.binding.divider.run {
visibility = if (position == list.size - 1) View.GONE else View.VISIBLE
}
}
}
@@ -0,0 +1,32 @@
package com.sw.dualscreen.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.dualscreen.databinding.ListItemCollectedFoodBinding
import com.sw.dualscreen.ext.gone
import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.model.response.v2.CollectedFoodV2
class CollectedFoodNewAdapter(var list: MutableList<CollectedFoodV2>) :
BaseQuickAdapter<CollectedFoodV2, CollectedFoodNewAdapter.VH>(list) {
inner class VH(var binding: ListItemCollectedFoodBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemCollectedFoodBinding.inflate(inflater, parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: CollectedFoodV2?) {
holder.binding.tvFoodName.text = item?.foodName
holder.binding.tvCollectedNum.text = "已采集${item?.foodCount}"
holder.binding.divider.run {
if (position == list.size-1) gone() else visible()
}
}
}
@@ -0,0 +1,38 @@
package com.sw.dualscreen.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.graphics.toColorInt
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.dualscreen.R
import com.sw.dualscreen.databinding.ListItemSearchFood2Binding
import com.sw.dualscreen.ext.format2String
import com.sw.dualscreen.model.response.FoodInfo
class DialogSearchFoodAdapter(var list: MutableList<FoodInfo>) :
BaseQuickAdapter<FoodInfo, DialogSearchFoodAdapter.VH>(list) {
inner class VH(var binding: ListItemSearchFood2Binding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemSearchFood2Binding.inflate(inflater, parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: FoodInfo?) {
holder.binding.tvFoodName.run {
if (item!!.score == 0) {
text = item.foodName
} else {
val score = (item.score / 100.0).format2String(2)
text = item.foodName + (if (item.score != 0) "-${score}%" else "")
}
isChecked = item.isChecked
}
}
}
@@ -9,7 +9,10 @@ import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.dualscreen.R
import com.sw.dualscreen.databinding.ListItemFoodCollectionBinding
import com.sw.dualscreen.ext.dp
import com.sw.dualscreen.ext.load
import com.sw.dualscreen.objbox.FoodCollectionBean
import com.sw.dualscreen.utils.GlideUtils
class FoodCollectionAdapter (var list: MutableList<FoodCollectionBean>) :
BaseQuickAdapter<FoodCollectionBean, FoodCollectionAdapter.VH>(list) {
@@ -30,11 +33,25 @@ class FoodCollectionAdapter (var list: MutableList<FoodCollectionBean>) :
binding.imageView.run {
if (it.isShowCamera) {
scaleType = ImageView.ScaleType.CENTER
setImageResource(R.drawable.ic_camera256)
setImageResource(R.drawable.ic_camera_default)
} else {
scaleType = ImageView.ScaleType.FIT_CENTER
//scaleType = ImageView.ScaleType.FIT_CENTER
scaleType = ImageView.ScaleType.CENTER_CROP
//setImageURI(it.imageUri)
setImageBitmap(it.bitmap)
//setImageBitmap(it.bitmap)
if (it.imageUri!=null) {
load(it.imageUri)
} else {
load(it.imageFile)
}
// it.bitmap?.let { bitmap ->
// GlideUtils.loadRoundCornerWitBitmap(
// context,
// url = bitmap,
// imageView = this,
// radius = 8.dp
// )
// }
}
}
}
@@ -0,0 +1,66 @@
package com.sw.dualscreen.adapter
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.graphics.toColorInt
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.RecyclerView
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.dualscreen.databinding.ListItemFoodOrderBinding
import com.sw.dualscreen.ext.dp
import com.sw.dualscreen.ext.format2String
import com.sw.dualscreen.ext.gone
import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.model.response.FoodItem
class FoodOrderAdapter(list: MutableList<FoodItem>) :
BaseQuickAdapter<FoodItem, FoodOrderAdapter.VH>(list) {
inner class VH(var binding: ListItemFoodOrderBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemFoodOrderBinding.inflate(inflater, parent, false)
return VH(binding)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: VH, position: Int, item: FoodItem?) {
val binding = holder.binding
setTextColor(binding.tvFoodName, binding.tvPriceNorm, binding.tvWeight, binding.tvAmount)
item?.let {
binding.tvFoodName.text = it.foodName
binding.tvFoodName.setTextColor(if (it.isLocalData) Color.GREEN else "#FF33446A".toColorInt())
if (it.orderFrom == 2) {
binding.tvPriceNorm.text = if (it.specName.isNullOrBlank()) "${it.specWeight}" else "${it.specName}/${it.specWeight}"
} else {
// 营养秤结算:规格栏显示规格重量 specWeight
binding.tvPriceNorm.text = "${it.specWeight}"
}
//if (it.specName.isNullOrBlank().not()) {
// binding.tvPriceFlag.text = "[${it.specName}]"
//}
binding.tvWeight.text = "${it.num}"
binding.tvAmount.text = it.price.format2String(2)
binding.tvLabel.run {
if (it.orderFrom == 2) visible() else gone()
}
}
binding.root.updateLayoutParams<RecyclerView.LayoutParams> {
topMargin = 2.dp
bottomMargin = 2.dp
}
}
fun setTextColor(vararg tvList: TextView) {
tvList.forEach {
it.setTextColor("#FF33446A".toColorInt())
}
}
}
@@ -0,0 +1,44 @@
package com.sw.dualscreen.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.graphics.toColorInt
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.dualscreen.R
import com.sw.dualscreen.databinding.ListItemSearchFoodBinding
import com.sw.dualscreen.ext.format2String
import com.sw.dualscreen.model.response.FoodInfo
class MainFoodListAdapter(var list: MutableList<FoodInfo>) :
BaseQuickAdapter<FoodInfo, MainFoodListAdapter.VH>(list) {
inner class VH(var binding: ListItemSearchFoodBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemSearchFoodBinding.inflate(inflater, parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: FoodInfo?) {
holder.binding.root.setBackgroundResource(R.drawable.bg_text_main)
holder.binding.tvFoodName.run {
if (item!!.score == 0) {
text = item.foodName
} else {
val score = (item.score / 100.0).format2String(2)
text = item.foodName + (if (item.score != 0) "-${score}%" else "")
}
isChecked = item.isChecked
setTextColor(
if (isChecked) "#FF3232".toColorInt()
else "#0A1428".toColorInt()
)
}
}
}
@@ -0,0 +1,69 @@
package com.sw.dualscreen.dialog
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.databinding.DialogEnvSwitchBinding
import com.sw.dualscreen.ext.dp
import com.sw.dualscreen.utils.SpTool
import com.sw.plate.utils.ToastUtils
/**
* 环境切换弹窗
*
* 提供 TEST / UAT / PROD 三套环境的切换功能,
* 初始化时自动根据 [GlobalData.appBaseUrl] 选中当前环境,
* 确认后更新 [GlobalData.appBaseUrl] 并通过 [onEnvChanged] 回调通知外部。
*
* @param context 上下文
* @param onEnvChanged 确认切换后的回调,参数为新的 baseUrl
*/
class EnvSwitchDialog(
context: Context,
private val onEnvChanged: (newBaseUrl: String) -> Unit = {}
) : BaseDialog(
context,
defWidth = 600.dp,
defHeight = WindowManager.LayoutParams.WRAP_CONTENT
) {
private lateinit var binding: DialogEnvSwitchBinding
override fun getRootView(): View {
binding = DialogEnvSwitchBinding.inflate(LayoutInflater.from(context))
return binding.root
}
override fun initView() {
// 根据当前 appBaseUrl 预选对应 RadioButton,不匹配则不选
when (GlobalData.appBaseUrl) {
GlobalData.LOCAL_BASE_URL -> binding.rbTest.isChecked = true
GlobalData.TEST_BASE_URL -> binding.rbUat.isChecked = true
GlobalData.PROD_BASE_URL -> binding.rbProd.isChecked = true
}
// 取消按钮
binding.btnCancel.setOnClickListener { dismiss() }
// 确认按钮
binding.btnConfirm.setOnClickListener {
// 未选中任何选项时提示用户
if (binding.rgEnv.checkedRadioButtonId == -1) {
ToastUtils.showToast("请选择环境")
return@setOnClickListener
}
val newUrl = when (binding.rgEnv.checkedRadioButtonId) {
binding.rbTest.id -> GlobalData.LOCAL_BASE_URL
binding.rbUat.id -> GlobalData.TEST_BASE_URL
binding.rbProd.id -> GlobalData.PROD_BASE_URL
else -> return@setOnClickListener
}
GlobalData.appBaseUrl = newUrl
SpTool.baseUrl = newUrl
onEnvChanged(newUrl)
dismiss()
}
}
}
@@ -5,7 +5,7 @@ import android.view.LayoutInflater
import android.view.View
import com.sw.dualscreen.databinding.DialogWarnBinding
class WarnDialog(
class RemindDialog(
context: Context,
var content:String,
var cancelBlock: () -> Unit = {},
@@ -31,5 +31,4 @@ class WarnDialog(
}
}
}
@@ -7,8 +7,11 @@ import android.graphics.BitmapFactory
import android.graphics.ImageFormat
import android.graphics.Rect
import android.graphics.YuvImage
import android.util.DisplayMetrics
import android.util.TypedValue
import android.view.TouchDelegate
import android.view.View
import android.view.WindowManager
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
@@ -20,6 +23,8 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.ByteArrayOutputStream
import java.math.BigDecimal
import java.math.RoundingMode
/**
* 将 Int 值转换为 dp 值
@@ -65,8 +70,12 @@ val Float.sp: Float
Resources.getSystem().displayMetrics
)
fun Double.format2String(): String = "%.1f".format(this)
fun Double.format2String(num:Int): String = "%.${num}f".format(this)
fun Double.roundedDecimalPlace(num: Int = 2): Double {
return BigDecimal(this).setScale(num, RoundingMode.HALF_UP).toDouble()
}
fun Double?.format2String(): String = this.format2String(1)
fun Double?.format2String(num:Int): String = "%.${num}f".format(this?:0.0)
// 添加扩展函数
fun ImageProxy.toSafeBitmap(): Bitmap {
@@ -110,13 +119,78 @@ fun EditText.addOnActionSearchListener(searchCallback: () -> Unit) {
return@setOnEditorActionListener false
}
}
fun View.clickWithDebounce(delay: Long = 500, action: () -> Unit) {
var job: Job? = null
//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 = 300, action: () -> Unit) {
setOnClickListener {
job?.cancel()
job = CoroutineScope(Dispatchers.Main).launch {
delay(delay)
action()
// 用View的tag存储是否可点击的状态(默认可点击)
if (tag as? Boolean ?: true) {
tag = false // 标记为不可点击
action() // 立即执行点击逻辑
// 启动协程,延迟后恢复可点击状态
CoroutineScope(Dispatchers.Main).launch {
delay(delay)
tag = true // 恢复可点击
}
}
}
}
fun View.visible() {
visibility = View.VISIBLE
}
fun View.invisible() {
visibility = View.INVISIBLE
}
fun View.gone() {
visibility = View.GONE
}
val Context.screenSize: IntArray
get() {
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
val displayMetrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(displayMetrics)
return intArrayOf(
displayMetrics.widthPixels,
displayMetrics.heightPixels
)
}
/**
* 扩大 View 的点击区域
* @param horizontal 水平方向扩大距离(dp)→ 左右各扩大一半
* @param vertical 垂直方向扩大距离(dp)→ 上下各扩大一半
*/
fun View.expandClickArea(horizontal: Int, vertical: Int) {
val density = resources.displayMetrics.density
val hPx = (horizontal * density).toInt()
val vPx = (vertical * density).toInt()
post {
val rect = android.graphics.Rect()
getHitRect(rect)
// 扩大区域:负值 = 向外扩大
rect.inset(-hPx, -vPx)
//rect.bottom += downPx // 只往下扩大
(parent as View).touchDelegate = TouchDelegate(rect, this)
}
}
@@ -2,8 +2,10 @@ package com.sw.dualscreen.ext
import android.graphics.drawable.GradientDrawable
import android.view.View
import android.widget.ImageView
import androidx.annotation.ColorInt
import androidx.core.graphics.toColorInt
import com.sw.dualscreen.utils.GlideUtils
/**
* 设置圆角边框
@@ -27,4 +29,8 @@ fun View.setRoundedBorder(
private fun View.dpToPx(dp: Float): Float {
return dp * context.resources.displayMetrics.density
}
fun ImageView.load(url: Any?) {
GlideUtils.loadImage(context, url, this)
}
@@ -0,0 +1,11 @@
package com.sw.dualscreen.model.request.v2
/**
* 新系统绑定用户与订单请求参数
* 对应接口:/neglect/booth/order/bind-user
*/
data class BindUserOrderRequest(
val userId: Long,
val orderNo: String,
val mode: Int? = null
)
@@ -0,0 +1,25 @@
package com.sw.dualscreen.model.request.v2
import java.math.BigDecimal
/**
* 新系统开餐下单请求参数
* 对应接口:/neglect/booth/order/place
*/
data class PlaceOrderRequest(
val foodId: Long,
val foodName: String,
val foodWeight: BigDecimal,
val eatWeight: BigDecimal,
val eatNum: Int,
val notPay: Boolean,
val mode: Int,
val specId: Long?,
val userId: Long?,
val paymentFrom: Int? = null,
val foodMaterialId: Long? = null,
val member: Boolean? = null,
val remark: String? = null,
val deviceId: String? = null,
val cookOrderId: String? = null
)
@@ -1,17 +1,10 @@
package com.sw.dualscreen.model.response
data class ApiResponse<T>(
val code: Int? = 0,
val message: String? = "",
val code: String,
val msg: String? = "",
val data: T? = null,
val result: T? = null,
val success: Boolean? = false,
val timestamp: Long? = 0
) {
fun isSuccess(): Boolean {
return success == true
}
}
)
@@ -0,0 +1,201 @@
package com.sw.dualscreen.model.response
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
data class FoodSearchReq(
var nameList: List<String>,
//"订单来源权举:1-营养秤,2-档口
var deviceType: Int = 2
)
data class FoodOrder(
//设备id
var deviceId: String,
//菜品id
var foodId: String,
//菜品名称
var foodName: String,
//菜品营养表id
var foodMaterialId: String,
//餐品规格ID
var specId: String,
//用户id(档口机,内部员工直接取餐:获取的用户ID/memberId
var userId: String? = null,
//食物重量
var foodWeight: Int,
//食用重量
var eatWeight: Int,
//份数
var eatNum: Int,
//是否需要付款
var notPay: Boolean,
//备注
var remark: String? = "",
//即放即取-1,称重-2
var mode: Int,
//设备模式,1 结算终端,2档口机
var paymentFrom: Int,
//是否会员
var member: Boolean
)
@Parcelize
data class MemberInfo(
//会员id
val id: String?,
//刷脸识别用户ID
val faceUserId: String?,
//手机号
val phone: String?,
//姓名
val name: String?,
//头像
val faceUrl: String?,
//充值余额
val topUpBalance: Double?,
//赠送余额
val rewardBalance: Double?,
//积分余额
val integralBalance: Int?,
var member: Boolean = false
) : Parcelable
data class TextBean(
var text: String,
var textSize: Int = 0,
var textColor: String? = null,
var isBold: Boolean = false
)
data class UserNutrition(
//姓名
val name: String?,
//推荐能量
// val recommendEnergy: Double?,
//热量
val calorie: Double?,
//果蔬
val fruitsVegetables: Double?,
//肉蛋
val meatEggs: Double?,
//主食
val stapleFood: Double?,
//能量最大值
val maxCalorie: Double? = null,
//能量最小值
val minCalorie: Double? = null,
//推荐能量
val recommendCalorie: Double? = null,
//主食推荐值
val stapleFoodRecommend: String? = null,
//主食即将超量
val stapleFoodNearExcess: String? = null,
//主食超量
val stapleFoodExcess: String? = null,
//果蔬推荐值
val fruitsVegetablesRecommend: String? = null,
//果蔬即将超量
val fruitsVegetablesNearExcess: String? = null,
//果蔬超量
val fruitsVegetablesExcess: String? = null,
//肉蛋豆推荐值
val meatEggsBeansRecommend: String? = null,
//肉蛋豆即将超量
val meatEggsBeansNearExcess: String? = null,
//肉蛋豆超量
val meatEggsBeansExcess: String? = null,
//总重量
val eatWeightSum:Double? = null,
)
//{"code":"00000","data":"{\"orderType\":0,\"orderPayFrom\":2,\"payType\":\"1\",\"paySuc\":0,\"totalFee\":0.01,\"orderCode\":\"1998193614899372032\",\"message\":\"支付成功\",\"payCode\":\"1998193614899372032508227\"}","msg":"成功","total":0}
data class PayResult(
val paySuc: String? = null
)
data class UserEnergy(
var calorie: Double,
var grain: Double,
var fruitsVegetables: Double,
var meatEggs: Double,
)
data class RespCodeMsg(
val code: String?,
val msg: String?
)
data class PostEvent(var name: String = "")
data class UpdateRefreshEvent(var name: String = "")
data class PaySuccessEvent(
var name: String = ""
)
data class ChargeModeEvent(
var chargeMode: Int = 0
)
data class ResetRecognizeEvent(
var state: Int = 0
)
data class ClickBackEvent(
var name: String = ""
)
data class DeviceConfig(
var arcsoftAppId: String? = null,
var arcsoftSdkKey: String? = null,
var arcsoftActiveKey: String? = null,
//结算模式:1-独立支付模式,2-联合结算模式
var payType: Int = 1
)
data class FoodVector(
val id: String? = null,
val foodId: String? = null,
val foodName: String? = null,
val foodVector: String? = null,
val version: String? = null
)
data class ResetBoxEvent(
var num: Int = 0
)
data class WeightRecord(
var foodInfo: FoodInfo? = null,
var lastWeight: Int = 0,
var currentWeight: Int = 0
)
data class FoodItem(
var id: String? = null,
var foodId: String? = null,
var foodName: String? = null,
var foodMaterialId: String? = null,
var specId: String? = null,
var num: Int = 0,
var eatNum: Int = 0,
var price: Double = 0.0,
var foodPrice: Double = 0.0,
var discount: Double = 0.0,
var income: Double = 0.0,
var foodDifferenceId: Long = 0,
var createTime: String? = null,
var specName: String? = null,
var specWeight: Int = 0,
var orderFrom: Int = 0,
var isLocalData: Boolean = false
)
data class FoodOrderModel(
val calorie: Double = 0.0,
val incomeSum: Double = 0.0,
val discountSum: Double = 0.0,
val eatWeightSum: Int = 0,
val orderNo: String? = null,
val list: List<FoodItem>? = null
)
@@ -1,42 +1,47 @@
package com.sw.dualscreen.model.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
data class DinnerTypeInfo(
@SerializedName("dinnerType")
val dinnerType: DinnerType? = DinnerType()
) : Parcelable {
@Parcelize
data class DinnerType(
@SerializedName("createBy")
val createBy: String? = "",
@SerializedName("createTime")
val createTime: String? = "",
@SerializedName("delFlag")
val delFlag: Int? = 0,
@SerializedName("dinnerType")
val dinnerType: String? = "",
@SerializedName("endTime")
val endTime: String? = "",
@SerializedName("id")
val id: Int? = 0,
@SerializedName("isSync")
val isSync: Int? = 0,
@SerializedName("isSyncCopy")
val isSyncCopy: Int? = 0,
@SerializedName("restId")
val restId: String? = "",
@SerializedName("startTime")
val startTime: String? = "",
@SerializedName("sysOrgCode")
val sysOrgCode: String? = "",
@SerializedName("updateBy")
val updateBy: String? = "",
@SerializedName("updateTime")
val updateTime: String? = ""
) : Parcelable
}
//{"id":"1992804086323437569","dinnerType":"晚餐","chargeType":1,"fixedAmount":3.0000}
data class DinnerType(
val id: String? = "",
//餐次
val dinnerType: String? = "",
//餐次收费模式(0称重,1固定,2不收费)
val chargeType: Int = 0,
//固定收费(元) 仅charge_type = 1 时
val fixedAmount: Double?=null
)
//@Parcelize
//data class DinnerTypeInfo(
// @SerializedName("dinnerType")
// val dinnerType: DinnerType? = DinnerType()
//) : Parcelable
//
//@Parcelize
//data class DinnerType(
// @SerializedName("createBy")
// val createBy: String? = "",
// @SerializedName("createTime")
// val createTime: String? = "",
// @SerializedName("delFlag")
// val delFlag: Int? = 0,
// @SerializedName("dinnerType")
// val dinnerType: String? = "",
// @SerializedName("endTime")
// val endTime: String? = "",
// @SerializedName("id")
// val id: Int? = 0,
// @SerializedName("isSync")
// val isSync: Int? = 0,
// @SerializedName("isSyncCopy")
// val isSyncCopy: Int? = 0,
// @SerializedName("restId")
// val restId: String? = "",
// @SerializedName("startTime")
// val startTime: String? = "",
// @SerializedName("sysOrgCode")
// val sysOrgCode: String? = "",
// @SerializedName("updateBy")
// val updateBy: String? = "",
// @SerializedName("updateTime")
// val updateTime: String? = ""
//) : Parcelable
@@ -12,11 +12,11 @@ data class EquipmentInfo(
@SerializedName("appPackageUrl")
val appPackageUrl: String? = "",
@SerializedName("arcsoftActiveKey")
val arcsoftActiveKey: String? = "",
var arcsoftActiveKey: String? = "",
@SerializedName("arcsoftAppId")
val arcsoftAppId: String? = "",
var arcsoftAppId: String? = "",
@SerializedName("arcsoftSdkKey")
val arcsoftSdkKey: String? = "",
var arcsoftSdkKey: String? = "",
@SerializedName("arrayCross")
val arrayCross: Int? = 0,
@SerializedName("arrayMode")
@@ -24,9 +24,9 @@ data class EquipmentInfo(
@SerializedName("arrayVertical")
val arrayVertical: Int? = 0,
@SerializedName("canteenId")
val canteenId: String? = "",
var canteenId: String? = "",
@SerializedName("canteenName")
val canteenName: String? = "",
var canteenName: String? = "",
@SerializedName("clientServerIp")
val clientServerIp: String? = "",
@SerializedName("createBy")
@@ -1,6 +1,5 @@
package com.sw.dualscreen.model.response
import android.net.Uri
import android.os.Parcelable
import android.text.TextUtils
@@ -12,291 +11,317 @@ import kotlinx.parcelize.Parcelize
*/
@Parcelize
data class FoodInfo(
@SerializedName("foodLabel")
val foodLabel: String? = "",
@SerializedName("foodName")
val foodName: String? = "",
@SerializedName("foodTypeAndRealIntakeVoList")
val foodTypeAndRealIntakeVoList: List<FoodTypeAndRealIntakeVo?>? = listOf(),
/**
* 食物id
*/
//菜品id
val foodId: String,
//菜品名称
val foodName: String? = null,
//能量
val calorie: Double? = null,
//蛋白质
val protein: Double? = null,
//脂肪
val fat: Double? = null,
//碳水化合物
val carbohydrate: Double? = null,
//规格售卖价格(元)
val specPrice: Double? = null,
//VIP售卖价(元)
val vipPrice: Double? = null,
//餐品营养id
val foodMaterialId: String? = null,
//菜品规格id
val specId: String? = null,
//规格重量(g)
val specWeight: Double? = null,
//主食
val stapleFood: Double? = null,
//果蔬
val fruitsVegetables: Double? = null,
//肉蛋
val meatEggs: Double? = null,
//推荐能量
val recommendCalorie: Double? = null,
//图片
var foodImg: String? = "",
//按单烹制 ID(菜品列表返回,就餐时原样回传,用于溯源码追踪)
var cookOrderId: String? = "",
//-----------------------------------
var score: Int = 0,
var isChecked: Boolean = false,
var photoUri: Uri? = null,
//true-搜索结果数据,false-识别查询数据
var isFromSearch: Boolean? = null
// @SerializedName("foodTypeAndRealIntakeVoList")
// val foodTypeAndRealIntakeVoList: List<FoodTypeAndRealIntakeVo>? = listOf(),
// @SerializedName("stFoodInfoMaterial")
// val stFoodInfoMaterial: StFoodInfoMaterial? = StFoodInfoMaterial(),
// @SerializedName("stFoodInfoPagoda")
// val stFoodInfoPagoda: StFoodInfoPagoda? = StFoodInfoPagoda(),
// @SerializedName("stFoodInfoPagodaAPPVO")
// val stFoodInfoPagodaAPPVO: StFoodInfoPagodaAPPVO? = StFoodInfoPagodaAPPVO(),
// @SerializedName("stFoodInfoSetting")
// val stFoodInfoSetting: StFoodInfoSetting? = StFoodInfoSetting(),
// @SerializedName("stFoodInfoSpecificationList")
// val stFoodInfoSpecificationList: List<StFoodInfoSpecification>? = listOf(),
) : Parcelable
//@Parcelize
//data class FoodTypeAndRealIntakeVo(
// @SerializedName("childMaterClassName")
// val childMaterClassName: String? = "",
// @SerializedName("foodId")
// val foodId: String? = "",
// @SerializedName("goodsId")
// val goodsId: String? = "",
// @SerializedName("goodsName")
// val goodsName: String? = "",
// @SerializedName("materClassName")
// val materClassName: String? = "",
// @SerializedName("materId")
// val materId: String? = "",
// @SerializedName("materialType")
// val materialType: String? = "",
// @SerializedName("realityIntake")
// val realityIntake: String? = ""
//) : Parcelable
@Parcelize
data class StFoodInfoMaterial(
@SerializedName("ash")
val ash: Double? = 0.0,
@SerializedName("avitE")
val avitE: Double? = 0.0,
@SerializedName("ca")
val ca: Double? = 0.0,
@SerializedName("cho")
val cho: Double? = 0.0,
@SerializedName("cholesterol")
val cholesterol: Double? = 0.0,
@SerializedName("cu")
val cu: Double? = 0.0,
@SerializedName("delFlag")
val delFlag: String? = "",
@SerializedName("dietFiber")
val dietFiber: Double? = 0.0,
@SerializedName("elementI")
val elementI: Double? = 0.0,
@SerializedName("elementK")
val elementK: String? = "",
@SerializedName("elementP")
val elementP: String? = "",
@SerializedName("energyKcal")
val energyKcal: Double? = 0.0,
@SerializedName("energyKj")
val energyKj: Double? = 0.0,
@SerializedName("fat")
val fat: Double? = 0.0,
@SerializedName("fe")
val fe: Double? = 0.0,
@SerializedName("folate")
val folate: Double? = 0.0,
@SerializedName("foodId")
val foodId: String? = "",
@SerializedName("foodWeight")
val foodWeight: String? = "",
@SerializedName("historyStatus")
val historyStatus: String? = "",
@SerializedName("id")
val id: String? = "",
@SerializedName("imgUrl")
var imgUrl: String? = "",
/**
* 拍照结果
*/
var photoUri: Uri? = null,
@SerializedName("stFoodInfoMaterial")
val stFoodInfoMaterial: StFoodInfoMaterial? = StFoodInfoMaterial(),
@SerializedName("stFoodInfoPagoda")
val stFoodInfoPagoda: StFoodInfoPagoda? = StFoodInfoPagoda(),
@SerializedName("stFoodInfoPagodaAPPVO")
val stFoodInfoPagodaAPPVO: StFoodInfoPagodaAPPVO? = StFoodInfoPagodaAPPVO(),
@SerializedName("stFoodInfoSetting")
val stFoodInfoSetting: StFoodInfoSetting? = StFoodInfoSetting(),
@SerializedName("stFoodInfoSpecificationList")
val stFoodInfoSpecificationList: List<StFoodInfoSpecification?>? = listOf(),
var score: Int = 0
@SerializedName("isSync")
val isSync: String? = "",
@SerializedName("isSyncCopy")
val isSyncCopy: String? = "",
@SerializedName("mg")
val mg: Double? = 0.0,
@SerializedName("mn")
val mn: Double? = 0.0,
@SerializedName("na")
val na: Double? = 0.0,
@SerializedName("naicin")
val naicin: Double? = 0.0,
@SerializedName("protein")
val protein: Double? = 0.0,
@SerializedName("retionl")
val retionl: Double? = 0.0,
@SerializedName("riboflavin")
val riboflavin: Double? = 0.0,
@SerializedName("se")
val se: Double? = 0.0,
@SerializedName("thiamin")
val thiamin: Double? = 0.0,
@SerializedName("totCarotene")
val totCarotene: Double? = 0.0,
@SerializedName("vitA")
val vitA: Double? = 0.0,
@SerializedName("vitB12")
val vitB12: Double? = 0.0,
@SerializedName("vitB6")
val vitB6: Double? = 0.0,
@SerializedName("vitC")
val vitC: Double? = 0.0,
@SerializedName("vitE")
val vitE: Double? = 0.0,
@SerializedName("water")
val water: Double? = 0.0,
@SerializedName("zn")
val zn: Double? = 0.0
) : Parcelable
//@Parcelize
//data class StFoodInfoPagoda(
// @SerializedName("aquatic")
// val aquatic: String? = "",
// @SerializedName("birds")
// val birds: String? = "",
// @SerializedName("delFlag")
// val delFlag: String? = "",
// @SerializedName("egg")
// val egg: String? = "",
// @SerializedName("foodId")
// val foodId: String? = "",
// @SerializedName("fruits")
// val fruits: String? = "",
// @SerializedName("grain")
// val grain: String? = "",
// @SerializedName("id")
// val id: String? = "",
// @SerializedName("isSync")
// val isSync: String? = "",
// @SerializedName("isSyncCopy")
// val isSyncCopy: String? = "",
// @SerializedName("livestock")
// val livestock: String? = "",
// @SerializedName("milk")
// val milk: String? = "",
// @SerializedName("nuts")
// val nuts: String? = "",
// @SerializedName("oil")
// val oil: String? = "",
// @SerializedName("potato")
// val potato: String? = "",
// @SerializedName("salt")
// val salt: String? = "",
// @SerializedName("soya")
// val soya: String? = "",
// @SerializedName("sugar")
// val sugar: String? = "",
// @SerializedName("vegetable")
// val vegetable: String? = ""
//) : Parcelable
@Parcelize
data class StFoodInfoPagodaAPPVO(
@SerializedName("fruits")
val fruits: String? = "",
@SerializedName("fruitsRecommend")
val fruitsRecommend: String? = "",
@SerializedName("grain")
val grain: String? = "",
@SerializedName("grainRecommend")
val grainRecommend: String? = "",
@SerializedName("meat")
val meat: String? = "",
@SerializedName("meatRecommend")
val meatRecommend: String? = "",
@SerializedName("oil")
val oil: String? = "",
@SerializedName("salt")
val salt: String? = "",
@SerializedName("soya")
val soya: String? = "",
@SerializedName("soyaRecommend")
val soyaRecommend: String? = "",
@SerializedName("sugar")
val sugar: String? = "",
@SerializedName("vegetable")
val vegetable: String? = "",
@SerializedName("vegetableRecommend")
val vegetableRecommend: String? = ""
) : Parcelable {
@Parcelize
data class FoodTypeAndRealIntakeVo(
@SerializedName("childMaterClassName")
val childMaterClassName: String? = "",
@SerializedName("foodId")
val foodId: String? = "",
@SerializedName("goodsId")
val goodsId: String? = "",
@SerializedName("goodsName")
val goodsName: String? = "",
@SerializedName("materClassName")
val materClassName: String? = "",
@SerializedName("materId")
val materId: String? = "",
@SerializedName("materialType")
val materialType: String? = "",
@SerializedName("realityIntake")
val realityIntake: String? = ""
) : Parcelable
@Parcelize
data class StFoodInfoMaterial(
@SerializedName("ash")
val ash: Double? = 0.0,
@SerializedName("avitE")
val avitE: Double? = 0.0,
@SerializedName("ca")
val ca: Double? = 0.0,
@SerializedName("cho")
val cho: Double? = 0.0,
@SerializedName("cholesterol")
val cholesterol: Double? = 0.0,
@SerializedName("cu")
val cu: Double? = 0.0,
@SerializedName("delFlag")
val delFlag: String? = "",
@SerializedName("dietFiber")
val dietFiber: Double? = 0.0,
@SerializedName("elementI")
val elementI: Double? = 0.0,
@SerializedName("elementK")
val elementK: String? = "",
@SerializedName("elementP")
val elementP: String? = "",
@SerializedName("energyKcal")
val energyKcal: Double? = 0.0,
@SerializedName("energyKj")
val energyKj: Double? = 0.0,
@SerializedName("fat")
val fat: Double? = 0.0,
@SerializedName("fe")
val fe: Double? = 0.0,
@SerializedName("folate")
val folate: Double? = 0.0,
@SerializedName("foodId")
val foodId: String? = "",
@SerializedName("foodWeight")
val foodWeight: String? = "",
@SerializedName("historyStatus")
val historyStatus: String? = "",
@SerializedName("id")
val id: String? = "",
@SerializedName("isSync")
val isSync: String? = "",
@SerializedName("isSyncCopy")
val isSyncCopy: String? = "",
@SerializedName("mg")
val mg: Double? = 0.0,
@SerializedName("mn")
val mn: Double? = 0.0,
@SerializedName("na")
val na: Double? = 0.0,
@SerializedName("naicin")
val naicin: Double? = 0.0,
@SerializedName("protein")
val protein: Double? = 0.0,
@SerializedName("retionl")
val retionl: Double? = 0.0,
@SerializedName("riboflavin")
val riboflavin: Double? = 0.0,
@SerializedName("se")
val se: Double? = 0.0,
@SerializedName("thiamin")
val thiamin: Double? = 0.0,
@SerializedName("totCarotene")
val totCarotene: Double? = 0.0,
@SerializedName("vitA")
val vitA: Double? = 0.0,
@SerializedName("vitB12")
val vitB12: Double? = 0.0,
@SerializedName("vitB6")
val vitB6: Double? = 0.0,
@SerializedName("vitC")
val vitC: Double? = 0.0,
@SerializedName("vitE")
val vitE: Double? = 0.0,
@SerializedName("water")
val water: Double? = 0.0,
@SerializedName("zn")
val zn: Double? = 0.0
) : Parcelable
fun fruitsValue(): Double =
if (TextUtils.isEmpty(fruits))
0.0
else fruits!!.toDouble()
@Parcelize
data class StFoodInfoPagoda(
@SerializedName("aquatic")
val aquatic: String? = "",
@SerializedName("birds")
val birds: String? = "",
@SerializedName("delFlag")
val delFlag: String? = "",
@SerializedName("egg")
val egg: String? = "",
@SerializedName("foodId")
val foodId: String? = "",
@SerializedName("fruits")
val fruits: String? = "",
@SerializedName("grain")
val grain: String? = "",
@SerializedName("id")
val id: String? = "",
@SerializedName("isSync")
val isSync: String? = "",
@SerializedName("isSyncCopy")
val isSyncCopy: String? = "",
@SerializedName("livestock")
val livestock: String? = "",
@SerializedName("milk")
val milk: String? = "",
@SerializedName("nuts")
val nuts: String? = "",
@SerializedName("oil")
val oil: String? = "",
@SerializedName("potato")
val potato: String? = "",
@SerializedName("salt")
val salt: String? = "",
@SerializedName("soya")
val soya: String? = "",
@SerializedName("sugar")
val sugar: String? = "",
@SerializedName("vegetable")
val vegetable: String? = ""
) : Parcelable
fun grainValue(): Double =
if (TextUtils.isEmpty(grain))
0.0
else grain!!.toDouble()
@Parcelize
data class StFoodInfoPagodaAPPVO(
@SerializedName("fruits")
val fruits: String? = "",
@SerializedName("fruitsRecommend")
val fruitsRecommend: String? = "",
@SerializedName("grain")
val grain: String? = "",
@SerializedName("grainRecommend")
val grainRecommend: String? = "",
@SerializedName("meat")
val meat: String? = "",
@SerializedName("meatRecommend")
val meatRecommend: String? = "",
@SerializedName("oil")
val oil: String? = "",
@SerializedName("salt")
val salt: String? = "",
@SerializedName("soya")
val soya: String? = "",
@SerializedName("soyaRecommend")
val soyaRecommend: String? = "",
@SerializedName("sugar")
val sugar: String? = "",
@SerializedName("vegetable")
val vegetable: String? = "",
@SerializedName("vegetableRecommend")
val vegetableRecommend: String? = ""
) : Parcelable {
fun vegetableValue(): Double =
if (TextUtils.isEmpty(vegetable))
0.0
else vegetable!!.toDouble()
fun fruitsValue(): Double =
if (TextUtils.isEmpty(fruits))
0.0
else fruits!!.toDouble()
fun meatValue(): Double =
if (TextUtils.isEmpty(meat))
0.0
else meat!!.toDouble()
}
fun grainValue(): Double =
if (TextUtils.isEmpty(grain))
0.0
else grain!!.toDouble()
//@Parcelize
//data class StFoodInfoSetting(
// @SerializedName("addFoodWeight")
// val addFoodWeight: String? = "",
// @SerializedName("bowlPlateWeight")
// val bowlPlateWeight: String? = "",
// @SerializedName("delFlag")
// val delFlag: String? = "",
// @SerializedName("foodId")
// val foodId: String? = "",
// @SerializedName("foodStatus")
// val foodStatus: String? = "",
// @SerializedName("id")
// val id: String? = "",
// @SerializedName("inventoryStatus")
// val inventoryStatus: Boolean? = false,
// @SerializedName("isSync")
// val isSync: String? = "",
// @SerializedName("isSyncCopy")
// val isSyncCopy: String? = "",
// @SerializedName("replenishWeight")
// val replenishWeight: String? = "",
// @SerializedName("residueType")
// val residueType: String? = "",
// @SerializedName("tablewareStatus")
// val tablewareStatus: Boolean? = false,
// @SerializedName("tablewareWeight")
// val tablewareWeight: String? = "",
// @SerializedName("warningStatus")
// val warningStatus: String? = "",
// @SerializedName("weighStatus")
// val weighStatus: String? = "",
// @SerializedName("weighUnitId")
// val weighUnitId: String? = "",
// @SerializedName("weighUnitName")
// val weighUnitName: String? = ""
//) : Parcelable
fun vegetableValue(): Double =
if (TextUtils.isEmpty(vegetable))
0.0
else vegetable!!.toDouble()
fun meatValue(): Double =
if (TextUtils.isEmpty(meat))
0.0
else meat!!.toDouble()
}
@Parcelize
data class StFoodInfoSetting(
@SerializedName("addFoodWeight")
val addFoodWeight: String? = "",
@SerializedName("bowlPlateWeight")
val bowlPlateWeight: String? = "",
@SerializedName("delFlag")
val delFlag: String? = "",
@SerializedName("foodId")
val foodId: String? = "",
@SerializedName("foodStatus")
val foodStatus: String? = "",
@SerializedName("id")
val id: String? = "",
@SerializedName("inventoryStatus")
val inventoryStatus: Boolean? = false,
@SerializedName("isSync")
val isSync: String? = "",
@SerializedName("isSyncCopy")
val isSyncCopy: String? = "",
@SerializedName("replenishWeight")
val replenishWeight: String? = "",
@SerializedName("residueType")
val residueType: String? = "",
@SerializedName("tablewareStatus")
val tablewareStatus: Boolean? = false,
@SerializedName("tablewareWeight")
val tablewareWeight: String? = "",
@SerializedName("warningStatus")
val warningStatus: String? = "",
@SerializedName("weighStatus")
val weighStatus: String? = "",
@SerializedName("weighUnitId")
val weighUnitId: String? = "",
@SerializedName("weighUnitName")
val weighUnitName: String? = ""
) : Parcelable
@Parcelize
data class StFoodInfoSpecification(
@SerializedName("bowlPlateWeight")
val bowlPlateWeight: Double? = 0.0,
@SerializedName("defaultStatus")
val defaultStatus: String? = "",
@SerializedName("delFlag")
val delFlag: String? = "",
@SerializedName("foodId")
val foodId: String? = "",
@SerializedName("id")
val id: String? = "",
@SerializedName("isSync")
val isSync: String? = "",
@SerializedName("isSyncCopy")
val isSyncCopy: String? = "",
@SerializedName("specId")
val specId: String? = "",
@SerializedName("specName")
val specName: String? = "",
@SerializedName("specPrice")
val specPrice: String? = "",
@SerializedName("specWeight")
val specWeight: Double? = 0.0
) : Parcelable
}
//@Parcelize
//data class StFoodInfoSpecification(
// @SerializedName("bowlPlateWeight")
// val bowlPlateWeight: Double? = 0.0,
// @SerializedName("defaultStatus")
// val defaultStatus: String? = "",
// @SerializedName("delFlag")
// val delFlag: String? = "",
// @SerializedName("foodId")
// val foodId: String? = "",
// @SerializedName("id")
// val id: String? = "",
// @SerializedName("isSync")
// val isSync: String? = "",
// @SerializedName("isSyncCopy")
// val isSyncCopy: String? = "",
// @SerializedName("specId")
// val specId: String? = "",
// @SerializedName("specName")
// val specName: String? = "",
// @SerializedName("specPrice")
// val specPrice: String? = "",
// @SerializedName("specWeight")
// val specWeight: Double? = 0.0
//) : Parcelable
@@ -10,14 +10,43 @@ import kotlinx.parcelize.Parcelize
*/
@Parcelize
data class UserFaceModel(
@SerializedName("faceFeature")
val faceFeature: String? = "",
@SerializedName("faceFeatureString")
// @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? = "",
val faceFeatureStr: String? = "",
val faceUpdateTimestamp:Long?=null,
val faceDeleted: Boolean? = false,
/**
* 会员编号
*/
val cardNo: String,
/**
* 是否会员
*/
val member: Boolean
) : Parcelable
@Parcelize
data class UserFaceModel2(
val userId: String? = "",
val faceFeatureString: String? = "",
@SerializedName("faceType")
val faceType: String? = "",
@SerializedName("userFaceId")
val userFaceId: String? = "",
@SerializedName("userId")
val userId: String? = ""
) : Parcelable
val face: String? = ""
) : Parcelable
data class FaceData(
val nextPageIndex:Int,
val total:Int,
val size:Int,
val current:Int,
val pages:Int,
val records: List<UserFaceModel2>?=null
)
@@ -1,253 +1,253 @@
package com.sw.dualscreen.model.response
import android.os.Parcelable
import android.text.TextUtils
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
/**
* 就餐数据
*/
@Parcelize
data class UserNutritionData(
/**
* 总胆固醇
*/
@SerializedName("cho")
val cho: String? = "",
/**
* 饮食纤维
*/
@SerializedName("dietFiber")
val dietFiber: String? = "",
/**
* 能量
*/
@SerializedName("energy")
val energy: String? = "",
/**
* 脂肪
*/
@SerializedName("fat")
val fat: String? = "",
/**
* 食物类型和实际摄入量清单
*/
@SerializedName("foodTypeAndRealIntakeVoList")
val foodTypeAndRealIntakeVoList: List<FoodTypeAndRealIntakeVo?>? = listOf(),
@SerializedName("foodWeight")
val foodWeight: String? = "",
@SerializedName("message")
val message: String? = "",
/**
* 钠
*/
@SerializedName("na")
val na: String? = "",
/**
* 蛋白质
*/
@SerializedName("protein")
val protein: String? = "",
/**
* 最大推荐热量
*/
@SerializedName("recommendMax")
val recommendMax: String? = "",
/**
* 最小推荐热量
*/
@SerializedName("recommendMin")
val recommendMin: String? = "",
/**
* 热量详情
*/
@SerializedName("stFoodInfoPagoda")
val stFoodInfoPagoda: StFoodInfoPagoda? = StFoodInfoPagoda(),
@SerializedName("stUserFoodInfoList")
val stUserFoodInfoList: List<StUserFoodInfo?>? = listOf(),
/**
* 每日总热量
*/
@SerializedName("totalEnergyCalculateScore")
val totalEnergyCalculateScore: String? = "",
/**
* 用户所在部门
*/
@SerializedName("userDept")
val userDept: String? = "",
/**
* 用户id
*/
@SerializedName("userId")
val userId: String? = "",
/**
* 姓名
*/
@SerializedName("userName")
val userName: String? = ""
) : Parcelable {
fun recommendMaxValue(): Double =
if (TextUtils.isEmpty(recommendMax)) 0.0
else recommendMax!!.toDouble()
fun recommendMinValue(): Double =
if (TextUtils.isEmpty(recommendMin)) 0.0
else recommendMin!!.toDouble()
fun energyValue(): Double =
if (TextUtils.isEmpty(energy)) 0.0
else energy!!.toDouble()
fun totalEnergyCalculateScoreValue(): Double =
if (TextUtils.isEmpty(totalEnergyCalculateScore)) 0.0
else totalEnergyCalculateScore!!.toDouble()
@Parcelize
data class FoodTypeAndRealIntakeVo(
@SerializedName("childMaterClassName")
val childMaterClassName: String? = "",
@SerializedName("foodId")
val foodId: String? = "",
@SerializedName("goodsId")
val goodsId: String? = "",
@SerializedName("goodsName")
val goodsName: String? = "",
@SerializedName("materClassName")
val materClassName: String? = "",
@SerializedName("materId")
val materId: String? = "",
@SerializedName("materialType")
val materialType: String? = "",
@SerializedName("realityIntake")
val realityIntake: String? = ""
) : Parcelable
@Parcelize
data class StFoodInfoPagoda(
/**
* 水果
*/
@SerializedName("fruits")
val fruits: String? = "",
@SerializedName("fruitsRecommend")
val fruitsRecommend: String? = "",
/**
* 粮食
*/
@SerializedName("grain")
val grain: String? = "",
@SerializedName("grainRecommend")
val grainRecommend: String? = "",
/**
* 肉
*/
@SerializedName("meat")
val meat: String? = "",
@SerializedName("meatRecommend")
val meatRecommend: String? = "",
/**
* 油
*/
@SerializedName("oil")
val oil: String? = "",
/**
* 盐
*/
@SerializedName("salt")
val salt: String? = "",
/**
* 大豆
*/
@SerializedName("soya")
val soya: String? = "",
@SerializedName("soyaRecommend")
val soyaRecommend: String? = "",
/**
* 糖
*/
@SerializedName("sugar")
val sugar: String? = "",
/**
* 蔬菜
*/
@SerializedName("vegetable")
val vegetable: String? = "",
@SerializedName("vegetableRecommend")
val vegetableRecommend: String? = ""
) : Parcelable {
fun fruitsValue(): Double =
if (TextUtils.isEmpty(fruits))
0.0
else fruits!!.toDouble()
fun grainValue(): Double =
if (TextUtils.isEmpty(grain))
0.0
else grain!!.toDouble()
fun vegetableValue(): Double =
if (TextUtils.isEmpty(vegetable))
0.0
else vegetable!!.toDouble()
fun meatValue(): Double =
if (TextUtils.isEmpty(meat))
0.0
else meat!!.toDouble()
}
@Parcelize
data class StUserFoodInfo(
@SerializedName("canteenId")
val canteenId: String? = "",
@SerializedName("createBy")
val createBy: String? = "",
@SerializedName("createTime")
val createTime: String? = "",
@SerializedName("dataSource")
val dataSource: String? = "",
@SerializedName("dataType")
val dataType: String? = "",
@SerializedName("delFlag")
val delFlag: String? = "",
@SerializedName("deviceId")
val deviceId: String? = "",
@SerializedName("dinnerType")
val dinnerType: String? = "",
@SerializedName("eatDay")
val eatDay: String? = "",
@SerializedName("eatNum")
val eatNum: String? = "",
@SerializedName("eatWeight")
val eatWeight: Double? = 0.0,
@SerializedName("foodId")
val foodId: String? = "",
@SerializedName("foodImg")
val foodImg: String? = "",
@SerializedName("foodMaterialId")
val foodMaterialId: String? = "",
@SerializedName("foodName")
val foodName: String? = "",
@SerializedName("foodWeight")
val foodWeight: String? = "",
@SerializedName("id")
val id: String? = "",
@SerializedName("isSync")
val isSync: String? = "",
@SerializedName("isSyncCopy")
val isSyncCopy: String? = "",
@SerializedName("specId")
val specId: String? = "",
@SerializedName("stBasicDiningInformationUserESVo")
val stBasicDiningInformationUserESVo: String? = "",
@SerializedName("stallType")
val stallType: String? = "",
@SerializedName("type")
val type: String? = "",
@SerializedName("userId")
val userId: String? = ""
) : Parcelable
}
//package com.sw.dualscreen.model.response
//
//
//import android.os.Parcelable
//import android.text.TextUtils
//import com.google.gson.annotations.SerializedName
//import kotlinx.parcelize.Parcelize
//
///**
// * 就餐数据
// */
//@Parcelize
//data class UserNutritionData(
// /**
// * 总胆固醇
// */
// @SerializedName("cho")
// val cho: String? = "",
// /**
// * 饮食纤维
// */
// @SerializedName("dietFiber")
// val dietFiber: String? = "",
// /**
// * 能量
// */
// @SerializedName("energy")
// val energy: String? = "",
// /**
// * 脂肪
// */
// @SerializedName("fat")
// val fat: String? = "",
// /**
// * 食物类型和实际摄入量清单
// */
// @SerializedName("foodTypeAndRealIntakeVoList")
// val foodTypeAndRealIntakeVoList: List<FoodTypeAndRealIntakeVo?>? = listOf(),
// @SerializedName("foodWeight")
// val foodWeight: String? = "",
// @SerializedName("message")
// val message: String? = "",
// /**
// * 钠
// */
// @SerializedName("na")
// val na: String? = "",
// /**
// * 蛋白质
// */
// @SerializedName("protein")
// val protein: String? = "",
// /**
// * 最大推荐热量
// */
// @SerializedName("recommendMax")
// val recommendMax: String? = "",
// /**
// * 最小推荐热量
// */
// @SerializedName("recommendMin")
// val recommendMin: String? = "",
// /**
// * 热量详情
// */
// @SerializedName("stFoodInfoPagoda")
// val stFoodInfoPagoda: StFoodInfoPagoda? = StFoodInfoPagoda(),
// @SerializedName("stUserFoodInfoList")
// val stUserFoodInfoList: List<StUserFoodInfo?>? = listOf(),
// /**
// * 每日总热量
// */
// @SerializedName("totalEnergyCalculateScore")
// val totalEnergyCalculateScore: String? = "",
// /**
// * 用户所在部门
// */
// @SerializedName("userDept")
// val userDept: String? = "",
// /**
// * 用户id
// */
// @SerializedName("userId")
// val userId: String? = "",
// /**
// * 姓名
// */
// @SerializedName("userName")
// val userName: String? = ""
//) : Parcelable {
// fun recommendMaxValue(): Double =
// if (TextUtils.isEmpty(recommendMax)) 0.0
// else recommendMax!!.toDouble()
//
// fun recommendMinValue(): Double =
// if (TextUtils.isEmpty(recommendMin)) 0.0
// else recommendMin!!.toDouble()
//
// fun energyValue(): Double =
// if (TextUtils.isEmpty(energy)) 0.0
// else energy!!.toDouble()
//
// fun totalEnergyCalculateScoreValue(): Double =
// if (TextUtils.isEmpty(totalEnergyCalculateScore)) 0.0
// else totalEnergyCalculateScore!!.toDouble()
//
// @Parcelize
// data class FoodTypeAndRealIntakeVo(
// @SerializedName("childMaterClassName")
// val childMaterClassName: String? = "",
// @SerializedName("foodId")
// val foodId: String? = "",
// @SerializedName("goodsId")
// val goodsId: String? = "",
// @SerializedName("goodsName")
// val goodsName: String? = "",
// @SerializedName("materClassName")
// val materClassName: String? = "",
// @SerializedName("materId")
// val materId: String? = "",
// @SerializedName("materialType")
// val materialType: String? = "",
// @SerializedName("realityIntake")
// val realityIntake: String? = ""
// ) : Parcelable
//
// @Parcelize
// data class StFoodInfoPagoda(
// /**
// * 水果
// */
// @SerializedName("fruits")
// val fruits: String? = "",
// @SerializedName("fruitsRecommend")
// val fruitsRecommend: String? = "",
// /**
// * 粮食
// */
// @SerializedName("grain")
// val grain: String? = "",
// @SerializedName("grainRecommend")
// val grainRecommend: String? = "",
// /**
// * 肉
// */
// @SerializedName("meat")
// val meat: String? = "",
// @SerializedName("meatRecommend")
// val meatRecommend: String? = "",
// /**
// * 油
// */
// @SerializedName("oil")
// val oil: String? = "",
// /**
// * 盐
// */
// @SerializedName("salt")
// val salt: String? = "",
// /**
// * 大豆
// */
// @SerializedName("soya")
// val soya: String? = "",
// @SerializedName("soyaRecommend")
// val soyaRecommend: String? = "",
// /**
// * 糖
// */
// @SerializedName("sugar")
// val sugar: String? = "",
// /**
// * 蔬菜
// */
// @SerializedName("vegetable")
// val vegetable: String? = "",
// @SerializedName("vegetableRecommend")
// val vegetableRecommend: String? = ""
// ) : Parcelable {
// fun fruitsValue(): Double =
// if (TextUtils.isEmpty(fruits))
// 0.0
// else fruits!!.toDouble()
//
// fun grainValue(): Double =
// if (TextUtils.isEmpty(grain))
// 0.0
// else grain!!.toDouble()
//
// fun vegetableValue(): Double =
// if (TextUtils.isEmpty(vegetable))
// 0.0
// else vegetable!!.toDouble()
//
// fun meatValue(): Double =
// if (TextUtils.isEmpty(meat))
// 0.0
// else meat!!.toDouble()
//
// }
//
// @Parcelize
// data class StUserFoodInfo(
// @SerializedName("canteenId")
// val canteenId: String? = "",
// @SerializedName("createBy")
// val createBy: String? = "",
// @SerializedName("createTime")
// val createTime: String? = "",
// @SerializedName("dataSource")
// val dataSource: String? = "",
// @SerializedName("dataType")
// val dataType: String? = "",
// @SerializedName("delFlag")
// val delFlag: String? = "",
// @SerializedName("deviceId")
// val deviceId: String? = "",
// @SerializedName("dinnerType")
// val dinnerType: String? = "",
// @SerializedName("eatDay")
// val eatDay: String? = "",
// @SerializedName("eatNum")
// val eatNum: String? = "",
// @SerializedName("eatWeight")
// val eatWeight: Double? = 0.0,
// @SerializedName("foodId")
// val foodId: String? = "",
// @SerializedName("foodImg")
// val foodImg: String? = "",
// @SerializedName("foodMaterialId")
// val foodMaterialId: String? = "",
// @SerializedName("foodName")
// val foodName: String? = "",
// @SerializedName("foodWeight")
// val foodWeight: String? = "",
// @SerializedName("id")
// val id: String? = "",
// @SerializedName("isSync")
// val isSync: String? = "",
// @SerializedName("isSyncCopy")
// val isSyncCopy: String? = "",
// @SerializedName("specId")
// val specId: String? = "",
// @SerializedName("stBasicDiningInformationUserESVo")
// val stBasicDiningInformationUserESVo: String? = "",
// @SerializedName("stallType")
// val stallType: String? = "",
// @SerializedName("type")
// val type: String? = "",
// @SerializedName("userId")
// val userId: String? = ""
// ) : Parcelable
//}
@@ -0,0 +1,19 @@
package com.sw.dualscreen.model.response.v2
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* 新系统采集菜品信息
* 对应接口:/neglect/booth/collect/page
*/
@Parcelize
data class CollectedFoodV2(
val foodId: String?,
val foodName: String?,
val version: String?,
val foodVector: String?,
val picUrls: List<String>?,
/** 已采集数量(客户端手动赋值,非接口字段,用于兼容旧版 UI) */
var foodCount: Int = 0
) : Parcelable
@@ -0,0 +1,22 @@
package com.sw.dualscreen.model.response.v2
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* 新系统人脸数据 VO
* 对应接口:/nutrition/neglect/common/face/page 和 /nutrition/neglect/common/face/increment
*/
@Parcelize
data class FaceVO(
val userFaceId: String?,
val userId: String?,
val faceFeature: String?,
/** 后端返回 String 类型(防止 JS 大数精度丢失),映射时自行转为 Long */
val faceUpdateTimestamp: String?,
val cardNo: String?,
val member: Boolean?,
val faceDeleted: Boolean?,
/** 人员类型:由后端定义,如 "1"-会员、"2"-临时用户、"3"-其他等 */
val personType: String?
) : Parcelable
@@ -0,0 +1,57 @@
package com.sw.dualscreen.model.response.v2
import android.os.Parcelable
import com.sw.dualscreen.model.response.FoodInfo
import kotlinx.parcelize.Parcelize
import java.math.BigDecimal
/**
* 新系统菜品信息
* 对应接口:/neglect/booth/food/by-names
*/
@Parcelize
data class NewFoodInfo(
val foodId: Long,
val foodName: String?,
val calorie: BigDecimal?,
val protein: BigDecimal?,
val fat: BigDecimal?,
val carbohydrate: BigDecimal?,
val price: BigDecimal?,
val specPrice: BigDecimal?,
val specWeight: BigDecimal?,
val stapleFood: BigDecimal?,
val fruitsVegetables: BigDecimal?,
val meatEggs: BigDecimal?,
val foodMaterialId: Long?,
val specId: Long?,
val foodImg: String?,
val foodLabel: String?,
val vipPrice: BigDecimal? = null,
val recommendCalorie: Int = 0,
val tablewareStatus: Boolean = false,
val tablewareWeight: Int = 0,
//按单烹制 ID(菜品列表返回,就餐时原样回传,用于溯源码追踪)
var cookOrderId: String? = "",
) : Parcelable
/** NewFoodInfo → FoodInfo 映射(V2 数据模型兼容旧版 UI) */
fun NewFoodInfo.toFoodInfo() = FoodInfo(
foodId = foodId.toString(),
foodName = foodName,
calorie = calorie?.toDouble(),
protein = protein?.toDouble(),
fat = fat?.toDouble(),
carbohydrate = carbohydrate?.toDouble(),
specPrice = specPrice?.toDouble(),
vipPrice = vipPrice?.toDouble(),
foodMaterialId = foodMaterialId?.toString(),
specId = specId?.toString(),
specWeight = specWeight?.toDouble(),
stapleFood = stapleFood?.toDouble(),
fruitsVegetables = fruitsVegetables?.toDouble(),
meatEggs = meatEggs?.toDouble(),
recommendCalorie = recommendCalorie.toDouble(),
foodImg = foodImg,
cookOrderId = cookOrderId,
)
@@ -0,0 +1,35 @@
package com.sw.dualscreen.model.response.v2
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import com.sw.dualscreen.model.response.MemberInfo
import java.math.BigDecimal
/**
* 新系统会员信息
* 对应接口:/neglect/booth/member/info 和 /neglect/booth/member/info-by-phone
*/
@Parcelize
data class NewMemberInfo(
val id: String?,
val phone: String?,
val name: String?,
val faceUrl: String?,
val topUpBalance: BigDecimal?,
val rewardBalance: BigDecimal? = BigDecimal.ZERO,
val integralBalance: Int?,
val member: Boolean?
) : Parcelable
/** NewMemberInfo → MemberInfo 映射(V2 数据模型兼容旧版 UIfaceUserId 复用 id */
fun NewMemberInfo.toMemberInfo() = MemberInfo(
id = id,
faceUserId = id,
phone = phone,
name = name,
faceUrl = faceUrl,
topUpBalance = topUpBalance?.toDouble(),
rewardBalance = rewardBalance?.toDouble(),
integralBalance = integralBalance,
member = member ?: false
)
@@ -0,0 +1,55 @@
package com.sw.dualscreen.model.response.v2
import android.os.Parcelable
import com.sw.dualscreen.model.response.FoodItem
import com.sw.dualscreen.model.response.FoodOrderModel
import kotlinx.parcelize.Parcelize
import java.math.BigDecimal
/**
* 新系统结算订单数据
* 对应接口:/neglect/booth/order/settlement
*/
@Parcelize
data class SettlementOrder(
val calorie: BigDecimal?,
val incomeSum: BigDecimal?,
val discountSum: BigDecimal?,
val eatWeightSum: Int?,
val orderNo: String?,
val list: List<SettlementFoodItem>?
) : Parcelable
@Parcelize
data class SettlementFoodItem(
val foodId: String?,
val foodName: String?,
val specId: String?,
val specName: String?,
val specWeight: BigDecimal?,
val eatNum: Int?,
val eatWeight: Int?,
val price: BigDecimal?
) : Parcelable
/** SettlementOrder → FoodOrderModel 映射(V2 数据模型兼容旧版 UI) */
fun SettlementOrder.toFoodOrderModel() = FoodOrderModel(
calorie = calorie?.toDouble() ?: 0.0,
incomeSum = incomeSum?.toDouble() ?: 0.0,
discountSum = discountSum?.toDouble() ?: 0.0,
eatWeightSum = eatWeightSum ?: 0,
orderNo = orderNo,
list = list?.map { it.toFoodItem() }
)
private fun SettlementFoodItem.toFoodItem() = FoodItem(
foodId = foodId,
foodName = foodName,
specId = specId,
specName = specName,
specWeight = specWeight?.toInt() ?: 0,
eatNum = eatNum ?: 0,
price = price?.toDouble() ?: 0.0,
// 重量栏读取 num 字段,这里用实际食用重量 eatWeight 赋值
num = eatWeight ?: 0
)
@@ -1,8 +1,15 @@
package com.sw.dualscreen.network
import com.sw.dualscreen.BuildConfig
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.MyApp
import com.sw.dualscreen.network.api.ApiService
import com.sw.dualscreen.network.api.ApiServiceV2
import com.sw.dualscreen.network.interceptor.RequestInterceptor
import com.sw.dualscreen.repository.RemoteRepository
import com.sw.dualscreen.repository.v2.RemoteRepositoryV2
import com.sw.dualscreen.utils.FileUtil
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
@@ -11,22 +18,36 @@ import timber.log.Timber
import java.util.concurrent.TimeUnit
object ApiClient {
private const val BASE_URL = "http://device.shuziweidao.com:8889/"
// private const val BASE_URL = "http://device.shuziweidao.com:8889/"
private const val BASE_URL = GlobalData.PROD_BASE_URL
private const val TIME_OUT = 30L // 超时时间(秒)
private const val TIME_OUT = 30L
private val okHttpClient = OkHttpClient.Builder()
.connectTimeout(TIME_OUT, TimeUnit.SECONDS)
.readTimeout(TIME_OUT, TimeUnit.SECONDS)
.writeTimeout(TIME_OUT, TimeUnit.SECONDS)
.addNetworkInterceptor(HttpLoggingInterceptor(logger = {
Timber.d("okhttp logger ==>${it}")
}).apply {
level = if (MyApp.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
.addNetworkInterceptor(Interceptor { chain ->
val request = chain.request()
val url = request.url.toString()
val tag = if (
url.contains("terminal/neglect/common/app/faceFeature/increment/list") ||
url.contains("nutrition/neglect/serve/face/increment")
) "faceIncrement"
else if (url.contains("terminal/neglect/pay/app/turnOrderInfo")) "turnOrderInfo"
else if(url.contains("nutrition/neglect")) "V2"
else "ApiClient"
val loggingInterceptor = HttpLoggingInterceptor(logger = {
Timber.tag(tag).d("okhttp logger ==>${it}")
FileUtil.saveLog("okhttp logger ==>${it}")
}).apply {
level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
}
loggingInterceptor.intercept(chain)
})
.addInterceptor(RequestInterceptor())
.build()
@@ -35,10 +56,21 @@ object ApiClient {
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
// .addCallAdapterFactory(CoroutineCallAdapterFactory()) // 协程适配器
.build()
val apiService: ApiService by lazy {
retrofit.create(ApiService::class.java)
}
}
val apiServiceV2: ApiServiceV2 by lazy {
retrofit.create(ApiServiceV2::class.java)
}
val repository: RemoteRepository by lazy {
RemoteRepository(apiService)
}
val repositoryV2: RemoteRepositoryV2 by lazy {
RemoteRepositoryV2(apiServiceV2)
}
}
@@ -3,21 +3,31 @@ package com.sw.dualscreen.network.api
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.model.request.UserNutritionParam
import com.sw.dualscreen.model.response.ApiResponse
import com.sw.dualscreen.model.response.DinnerTypeInfo
import com.sw.dualscreen.model.response.EquipmentInfo
import com.sw.dualscreen.model.response.DeviceConfig
import com.sw.dualscreen.model.response.DinnerType
import com.sw.dualscreen.model.response.FaceData
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.UserFaceInfo
import com.sw.dualscreen.model.response.UserNutritionData
import com.sw.dualscreen.model.response.FoodOrder
import com.sw.dualscreen.model.response.FoodOrderModel
import com.sw.dualscreen.model.response.FoodSearchReq
import com.sw.dualscreen.model.response.FoodVector
import com.sw.dualscreen.model.response.MemberInfo
import com.sw.dualscreen.model.response.UserFaceModel
import com.sw.dualscreen.model.response.UserNutrition
import com.sw.dualscreen.objbox.CollectedFoodInfo
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.PartMap
import retrofit2.http.Query
import retrofit2.http.QueryMap
import retrofit2.http.Url
interface ApiService {
@@ -25,76 +35,119 @@ interface ApiService {
/**
* device获取token
*/
@GET("sys/getEquipmentToken")
suspend fun getDeviceToken(
@Query("qrcodeId") qrcodeId: String,
@Query("appVersion") appVersion: String = GlobalData.appVersion
): ApiResponse<String>
// @GET("sys/getEquipmentToken")
// suspend fun getDeviceToken(
// @Query("qrcodeId") qrcodeId: String,
// @Query("appVersion") appVersion: String = GlobalData.appVersion
// ): ApiResponse<String>
/**
*获取配置信息
*/
@GET("equipment/stEquipment/queryByEquipmentCode")
suspend fun getDeviceInfo(
@Query("equipmentCode") equipmentCode: String,
@Query("appVersion") appVersion: String = GlobalData.appVersion,
@Header("X-Access-Token") token: String
): ApiResponse<EquipmentInfo>
// @GET("equipment/stEquipment/queryByEquipmentCode")
// suspend fun getDeviceInfo(
// @Query("equipmentCode") equipmentCode: String,
// @Query("appVersion") appVersion: String = GlobalData.appVersion,
// @Header("X-Access-Token") token: String
// ): ApiResponse<EquipmentInfo>
/**
* 获取业务服务器token
*/
@GET
suspend fun getEquipmentToken(
@Url url: String = "${GlobalData.appBaseUrl}/sys/getEquipmentToken",
@Query("appVersion") appVersion: String = GlobalData.appVersion,
@Query("qrcodeId") qrcodeId: String
): ApiResponse<String>
// @GET
// suspend fun getEquipmentToken(
// @Url url: String = "${GlobalData.appBaseUrl}/sys/getEquipmentToken",
// @Query("appVersion") appVersion: String = GlobalData.appVersion,
// @Query("qrcodeId") qrcodeId: String
// ): ApiResponse<String>
/**
* 获取人脸数据
*/
@GET
@POST
suspend fun getUserFaceCache(
@Url url: String = "${GlobalData.appBaseUrl}/stapi/cquser/getUserFaceCache/v2",
@Query("appVersion") appVersion: String = GlobalData.appVersion,
@Query("pageIndex") pageIndex: Int
): ApiResponse<UserFaceInfo>
// @Url url: String = "${GlobalData.appBaseUrl}/stapi/cquser/getUserFaceCache/v2",
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/faceFeature/list",
// @Query("appVersion") appVersion: String = GlobalData.appVersion,
// @Query("pageIndex") pageIndex: Int
@Body param: Map<String, Int>
// @Query("pageNum") pageNum: Int,
// @Query("pageSize") pageSize: Int
): ApiResponse<List<UserFaceModel>?>
@GET
suspend fun getUserFaceCache2(
@Url url: String = "http://192.168.1.230:2223/userface/swUserFaceimgSub/list",
@Query("pageNum") pageNum: Int,
@Query("pageSize") pageSize: Int
): ApiResponse<FaceData>
/**
* 获取已采集数据列表
*/
@POST
suspend fun getCollectedFoodList(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/boothMachine/app/dishPage",
@Body param: Map<String, String>
): ApiResponse<List<CollectedFoodInfo>>
// /**
// * 获取档口菜品信息
// */
// @GET
// suspend fun getRestInfoFoodsByType(
//// @Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/getRestInfoFoodsByType/stall",
//// @Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/boothMachine/app/getFoodsByFoodNamePage",
// @Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/boothMachine/app/getFoodsByFoodNameList",
// @Query("name") foodName: String,
//// @Query("appVersion") appVersion: String = GlobalData.appVersion,
//// @Query("restId") restId: String,
//// @Query("type") type: Int,
//// @Query("foodName") foodName: String,
// ): ApiResponse<List<FoodInfo>>
/**
* 获取档口菜品信息
*/
@GET
@POST
suspend fun getRestInfoFoodsByType(
@Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/getRestInfoFoodsByType/stall",
@Query("appVersion") appVersion: String = GlobalData.appVersion,
@Query("restId") restId: String,
@Query("type") type: Int,
@Query("foodName") foodName: String,
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/boothMachine/app/getFoodsByFoodNameList",
@Body param: HashMap<String, String>
): ApiResponse<List<FoodInfo>>
// /**
// * 通过用户信息获取就餐数据
// */
// @GET
// suspend fun getUserNutritionData(
// @Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/getUserNutritionData/face",
// @Query("appVersion") appVersion: String = GlobalData.appVersion,
// @Query("restId") restId: String,
// @Query("userId") userId: String,
// @Query("foodId") foodId: String,
// ): ApiResponse<UserNutritionData>
/**
* 通过用户信息获取就餐数据
*/
@GET
suspend fun getUserNutritionData(
@Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/getUserNutritionData/face",
@Query("appVersion") appVersion: String = GlobalData.appVersion,
@Query("restId") restId: String,
@Query("userId") userId: String,
@Query("foodId") foodId: String,
): ApiResponse<UserNutritionData>
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/boothMachine/app/getUserCurrentFoodDetails",
@Query("id") userId: String
): ApiResponse<UserNutrition>
/**
* 获取当前餐点类型
*/
@GET
suspend fun getDinnerType(
@Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/getCanteenDinnerType",
@Query("appVersion") appVersion: String = GlobalData.appVersion,
@Query("canteenId") restId: String,
): ApiResponse<DinnerTypeInfo>
// @Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/getCanteenDinnerType",
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/getRegionRule",
// @Query("appVersion") appVersion: String = GlobalData.appVersion,
// @Query("canteenId") restId: String,
): ApiResponse<DinnerType>
/**
* 提交就餐数据
@@ -108,23 +161,173 @@ interface ApiService {
/**
* 获取菜品信息
*/
@GET
@POST
suspend fun getFoodInfo(
@Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/getRestInfoFoodsByType/stall/v2",
@Query("restId") restId: String,
@Query("foodNames") foodName: String,
// @Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/getRestInfoFoodsByType/stall/v2",
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/boothMachine/app/goodsList",
// @Query("restId") restId: String,
// @Query("foodNames") foodName: String
@Body req: FoodSearchReq
): ApiResponse<List<FoodInfo>>
// /**
// * 查询支付结果
// */
// @POST
// suspend fun queryOrderState(
// @Url url: String = "${GlobalData.appBaseUrl}/pay/yx-check-out-pay/turnOrderInfo",
// @Body param: HashMap<String, String>
// ): ApiResponse<PayResult?>
/**
* 查询支付结果
*/
@GET
suspend fun queryOrderState(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/pay/app/turnOrderInfo",
@Query("orderNo") orderNo: String
// @Body param: HashMap<String, String>
): ApiResponse<Any?>
/**
* 现金支付
*/
@POST
suspend fun cashPay(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/pay/app/cashPayment",
@Body param: HashMap<String, String>
): ApiResponse<Boolean?>
/**
* 会员支付
*/
@POST
suspend fun memberPay(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/pay/app/memberPay",
@Body param: HashMap<String, String?>
): ApiResponse<Any?>
/**
* 获取支付二维码
*/
@GET
suspend fun getQrCodeImg(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/pay/app/getQrCodeImg",
@Query("orderNo") orderNo: String,
@Query("memberId") memberId: String?,
@Query("totalFee") totalFee: String? = null
): ApiResponse<String?>
/**
* 二维码支付
* @param orderType 0付款 1会员充值
*/
@POST
suspend fun qrCodePay(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/pay/app/qrCodePay",
@Body param: HashMap<String, String?>
): ApiResponse<String?>
/**
* 开餐-生成订单
*/
@POST
suspend fun createOrder(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/boothMachine/app/userEatFood/doubleScale",
@Body order: FoodOrder
): ApiResponse<Any?>
/**
* APP根据id获取会员或员工信息及余额
*/
@GET
suspend fun getMemberInfoById(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/getUserInfoBalanceById",
@Query("id") memberId: String,
): ApiResponse<MemberInfo?>
/**
* 根据手机尾号和密钥查询员工信息及余额
*/
@POST
suspend fun getMemberInfoByPhone(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/queryUserInfoBalanceByPhone",
@Body param: HashMap<String, String>
): ApiResponse<MemberInfo?>
/**
* 绑定用户与订单号
*/
@GET
suspend fun bindOrder(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/boothMachine/app/bingOrder",
@Query("userId") userId: String,
@Query("orderNo") orderId: String,
@Query("mode") mode: Int
): ApiResponse<Any?>
/**
* 提交采集图片数据
*/
@Multipart
@POST
suspend fun postImageData(
@Url url: String = "http://192.168.1.201:14801/terminal/neglect/dishCollectionVectorData/add",
@PartMap params: Map<String, @JvmSuppressWildcards RequestBody>,
@Part image: MultipartBody.Part?
): ApiResponse<String>
suspend fun uploadCollectFoodPics(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/boothMachine/app/dishCollectionVectorData/add",
@PartMap params: HashMap<String, RequestBody>,
@Part foodPics: List<MultipartBody.Part>
): ApiResponse<List<String>?>
/**
* 获取设备配置数据
*/
@GET
suspend fun getDeviceConfig(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/getYxEquipmentByEquipmentCode"
): ApiResponse<DeviceConfig?>
/**
* 获取人脸增量数据
*/
@POST
suspend fun getFaceIncrementList(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/faceFeature/increment/list",
@Body param: Map<String, Long>
): ApiResponse<List<UserFaceModel>?>
/**
* 档口机向量数据分页查询
*/
@POST
suspend fun getCollectedFoodVector(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/boothMachine/app/collectionVectorPage",
@Body param: MutableMap<String, String>
): ApiResponse<List<FoodVector>?>
/**
* 档口机向量数据分页查询
*/
@DELETE
suspend fun deleteCollectFood(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/boothMachine/app/deleteBoothMachineCollection",
@QueryMap param: MutableMap<String, String?>
): ApiResponse<Any?>
/**
* 查询就餐数据
*/
@GET
suspend fun getFoodOrderList(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/settlement/terminal/app/getFoodInfoBySettlement",
@Query("userId") userId: String
): ApiResponse<FoodOrderModel?>
/**
* 查询会员折扣
*/
@GET
suspend fun getMemberDiscount(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/getMemberDiscount",
@Query("userId") userId: String
): ApiResponse<Double?>
}
@@ -0,0 +1,134 @@
package com.sw.dualscreen.network.api
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.model.request.v2.BindUserOrderRequest
import com.sw.dualscreen.model.request.v2.PlaceOrderRequest
import com.sw.dualscreen.model.response.ApiResponse
import com.sw.dualscreen.model.response.DeviceConfig
import com.sw.dualscreen.model.response.FoodSearchReq
import com.sw.dualscreen.model.response.UserNutrition
import com.sw.dualscreen.model.response.v2.CollectedFoodV2
import com.sw.dualscreen.model.response.v2.FaceVO
import com.sw.dualscreen.model.response.v2.NewFoodInfo
import com.sw.dualscreen.model.response.v2.NewMemberInfo
import com.sw.dualscreen.model.response.v2.SettlementOrder
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.PartMap
import retrofit2.http.Url
/**
* 新系统 API 接口服务
* 基于 /nutrition/neglect/booth 模块前缀
* 文档版本:2026-05-27
*/
interface ApiServiceV2 {
@GET
suspend fun getDeviceConfig(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/device/config"
): ApiResponse<DeviceConfig?>
/**
* 获取全量人脸数据
*/
@POST
suspend fun getFacePage(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/common/face/page",
@Body request: Map<String, Long>
): ApiResponse<List<FaceVO>?>
/**
* 获取增量人脸数据
*/
@POST
suspend fun getFaceIncrement(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/common/face/increment",
@Body request: Map<String, Long>
): ApiResponse<List<FaceVO>?>
@POST
suspend fun getFoodByNames(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/food/by-names",
@Body request: FoodSearchReq
): ApiResponse<List<NewFoodInfo>?>
@POST
suspend fun searchFood(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/food/search",
@Body request: Map<String, @JvmSuppressWildcards Any>
): ApiResponse<List<NewFoodInfo>?>
@POST
suspend fun getUserCurrentFood(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/user/current-food",
@Body request: Map<String, Long>
): ApiResponse<UserNutrition?>
@POST
suspend fun placeOrder(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/order/place",
@Body request: PlaceOrderRequest
): ApiResponse<String?>
@POST
suspend fun getSettlementOrders(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/order/settlement",
@Body request: Map<String, Long>
): ApiResponse<SettlementOrder?>
@POST
suspend fun getMemberInfo(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/member/info",
@Body request: Map<String, Long>
): ApiResponse<NewMemberInfo?>
@POST
suspend fun getMemberInfoByPhone(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/member/info-by-phone",
@Body request: Map<String, String>
): ApiResponse<NewMemberInfo?>
@POST
suspend fun getMemberDiscount(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/member/discount",
@Body request: Map<String, Long>
): ApiResponse<String?>
@POST
suspend fun bindUserOrder(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/order/bind-user",
@Body request: BindUserOrderRequest
): ApiResponse<Any?>
@POST
suspend fun getCollectPage(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/collect/page",
@Body request: Map<String, @JvmSuppressWildcards Any>
): ApiResponse<List<CollectedFoodV2>?>
@POST
suspend fun getCollectVectorPage(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/collect/vector-page",
@Body request: Map<String, Long>
): ApiResponse<List<CollectedFoodV2>?>
@Multipart
@POST
suspend fun uploadCollect(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/collect/upload",
@PartMap params: HashMap<String, RequestBody>,
@Part foodPics: List<MultipartBody.Part>
): ApiResponse<List<String>?>
@POST
suspend fun deleteCollect(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/collect/delete",
@Body request: Map<String, @JvmSuppressWildcards Any>
): ApiResponse<Any?>
}
@@ -1,6 +1,7 @@
package com.sw.dualscreen.network.interceptor
import android.text.TextUtils
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.GlobalKey
import com.sw.dualscreen.utils.SPUtil
import com.sw.plate.App
@@ -18,7 +19,11 @@ class RequestInterceptor : Interceptor {
.header("Content-Type", "application/json")
.header("Accept", "application/json")
// .header("Authorization", "Bearer ${getToken()}")
.header("X-Access-Token", getToken(originalRequest))
// .header("X-Access-Token", getToken(originalRequest))
.header("X-Access-Token", "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjYW50ZWVuSWQiOiJiZTE1NDgzMS0zNDY2LTNiYTItYTJlYS01NzY1MmM5MTlmZWQiLCJ0eXBlIjoiNCIsInVzZXJJZCI6IjAwMDAwMDAwMDAwMDAwMDAwMDEifQ.sN40cOC-O5WQFrF4IDUs8fFlkNdUKLbJt_rHyTsgYYM")
// .header("X-DEVICE-CODE", "bcf396ed-78f6-3864-9837-7c37c5b2ec41")
.header("X-DEVICE-CODE", GlobalData.deviceId)
.header("authorization", "57ee87183f2a4fa59683ec9ef41c8f5d")
val newRequest = requestBuilder.build()
@@ -2,6 +2,7 @@ package com.sw.dualscreen.objbox
import android.graphics.Bitmap
import android.net.Uri
import java.io.File
data class FoodClassInfo(
var class_names: List<String>,
@@ -17,13 +18,49 @@ data class FoodClassInfo(
//)
data class FoodCollectionBean(
var imageFile: File? = null,
var imageUri: Uri? = null,
var bitmap: Bitmap? = null,
var imageVector: FloatArray? = null,
var isShowCamera: Boolean = false,
var isFinish:Boolean = false
)
var isFinish:Boolean = false,
var uploadSuccess:Boolean = false
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
data class CollectedFoodBean(
var foodName: String?,
other as FoodCollectionBean
if (isShowCamera != other.isShowCamera) return false
if (isFinish != other.isFinish) return false
if (uploadSuccess != other.uploadSuccess) return false
if (imageFile != other.imageFile) return false
if (imageUri != other.imageUri) return false
if (bitmap != other.bitmap) return false
if (!imageVector.contentEquals(other.imageVector)) return false
return true
}
override fun hashCode(): Int {
var result = isShowCamera.hashCode()
result = 31 * result + isFinish.hashCode()
result = 31 * result + uploadSuccess.hashCode()
result = 31 * result + (imageFile?.hashCode() ?: 0)
result = 31 * result + (imageUri?.hashCode() ?: 0)
result = 31 * result + (bitmap?.hashCode() ?: 0)
result = 31 * result + (imageVector?.contentHashCode() ?: 0)
return result
}
}
data class CollectedFoodInfo(
// var id: String? = null,
// var placeId: String? = null,
var foodId: String? = null,
var foodName: String? = null,
// var foodPic: Any? = null,
// var foodVector: Any? = null,
var foodCount: Int = 0
)
@@ -1,15 +1,22 @@
package com.sw.dualscreen.objbox
import com.sw.dualscreen.utils.DateTimeUtil
import io.objectbox.annotation.Entity
import io.objectbox.annotation.HnswIndex
import io.objectbox.annotation.Id
import io.objectbox.annotation.VectorDistanceType
import java.time.LocalDateTime
@Entity
data class Food(
@Id var id: Long = 0,
var name: String? = null,
var foodIdx: Int = 0,
var collectId: String? = null,
var foodId: String? = null,
var foodName: String? = null,
var version: String? = null,
var createTime: String = DateTimeUtil.formatDateTime(LocalDateTime.now()),
var isDel: Boolean = false,
var otherField: String? = null,
@HnswIndex(dimensions = 512, distanceType = VectorDistanceType.DOT_PRODUCT)
var foodVector: FloatArray? = null
) {
@@ -20,8 +27,13 @@ data class Food(
other as Food
if (id != other.id) return false
if (foodIdx != other.foodIdx) return false
if (name != other.name) return false
if (isDel != other.isDel) return false
if (collectId != other.collectId) return false
if (foodId != other.foodId) return false
if (foodName != other.foodName) return false
if (version != other.version) return false
if (createTime != other.createTime) return false
if (otherField != other.otherField) return false
if (!foodVector.contentEquals(other.foodVector)) return false
return true
@@ -29,9 +41,15 @@ data class Food(
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + foodIdx
result = 31 * result + (name?.hashCode() ?: 0)
result = 31 * result + isDel.hashCode()
result = 31 * result + (collectId?.hashCode() ?: 0)
result = 31 * result + (foodId?.hashCode() ?: 0)
result = 31 * result + (foodName?.hashCode() ?: 0)
result = 31 * result + (version?.hashCode() ?: 0)
result = 31 * result + createTime.hashCode()
result = 31 * result + (otherField?.hashCode() ?: 0)
result = 31 * result + (foodVector?.contentHashCode() ?: 0)
return result
}
}
@@ -1,20 +1,21 @@
package com.sw.dualscreen.objbox
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.util.SparseLongArray
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import androidx.core.graphics.scale
import com.sw.dualscreen.MyApp
import com.sw.dualscreen.utils.AssetsTool
import com.sw.dualscreen.utils.GsonUtils
import com.sw.dualscreen.utils.ImageUtil
import com.sw.plate.utils.ToastUtils
import io.objectbox.Box
import io.objectbox.kotlin.boxFor
import io.objectbox.query.Query
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.pytorch.IValue
import org.pytorch.Module
import org.pytorch.Tensor
import org.pytorch.torchvision.TensorImageUtils
import timber.log.Timber
import java.io.File
@@ -23,168 +24,272 @@ import java.io.IOException
import java.io.InputStream
object FoodModule {
private const val TAG = "FoodModule"
private const val THRESHOLD = 0.8
private lateinit var module_mobile: Module
private lateinit var box: Box<Food>
private lateinit var embeddingsList: List<List<Float>>
private lateinit var labelsList: IntArray
private lateinit var classInfo: FoodClassInfo
// private const val THRESHOLD = 0.0
private var module: Module? = null
// 专用推理线程:与 Kotlin IO 调度器完全隔离,避免 ArcSoft 污染该线程的 FPU 状态(FPSCR)
private val inferenceExecutor = java.util.concurrent.Executors.newSingleThreadExecutor()
// private lateinit var embeddingsList: List<List<Float>>
// private lateinit var labelsList: IntArray
// private lateinit var classInfo: FoodClassInfo
val NO_MEAN_RGB = floatArrayOf(0.0f, 0.0f, 0.0f)
val NO_STD_RGB = floatArrayOf(1.0f, 1.0f, 1.0f)
val DEFAULT_FOOD_INDEX = -1
fun init(context: Context) {
Thread {
module_mobile = Module.load(copyAssetToCache(context, "best_embedding_model_mobile.pt"))
box = ObjectBox.boxStore.boxFor(Food::class)
//if (box.all.isNotEmpty()) {
// box.removeAll()
//}
// val DEFAULT_FOOD_INDEX = -1
// 1. 定义你的模型固定输入尺寸 (根据你的tflite模型修改,比如224x224)
private const val MODEL_INPUT_WIDTH = 224
private const val MODEL_INPUT_HEIGHT = 224
@SuppressLint("SuspiciousIndentation")
suspend fun init(context: Context, block: () -> Unit = {}) {
// Thread {}.start()
withContext(Dispatchers.IO) {
val modelPath = copyAssetToCache(context, "best_embedding_model_mobile.pt")
module = Module.load(modelPath)
// 预热:在相机启动前先跑一次 forward,触发 PyTorch 线程池创建
// 在预热前将当前线程优先级调低,使 PyTorch 内部新建的工作线程也继承低优先级
// 从而降低后续推理时的瞬间功率,避免触发 PMIC 过流保护
Timber.tag(TAG).d("预热 forward() 开始")
val warmupTensor = Tensor.fromBlob(
FloatArray(1 * 3 * 224 * 224),
longArrayOf(1, 3, 224, 224)
)
inferenceExecutor.submit { module?.forward(IValue.from(warmupTensor)) }.get()
context.let { ctx ->
val am = ctx.getSystemService(android.content.Context.ACTIVITY_SERVICE)
as android.app.ActivityManager
val mi = android.app.ActivityManager.MemoryInfo()
am.getMemoryInfo(mi)
Timber.tag(TAG).d("预热完成后系统可用内存: ${mi.availMem / 1024 / 1024} MB / 总: ${mi.totalMem / 1024 / 1024} MB")
}
Timber.tag(TAG).d("预热 forward() 完成")
//初始化默认重新拉取数据,先清空本地数据
val list = ObjectBox.getAll()
if (list.isNotEmpty()) {
ObjectBox.removeAll()
}
//if (box.all.isEmpty()) {
// initDefFoodData(context)
//}
}.start()
block()
}
}
fun uri2FloatArray(uri: Uri): FloatArray? {
return MyApp.instance?.let { context ->
ImageUtil.uriToBitmap(context, uri)?.let {
bitmap2FloatArray(it)
// fun uri2FloatArray(uri: Uri): FloatArray? {
// return MyApp.instance?.let { context ->
// ImageUtil.uriToBitmap(context, uri)?.let {
// bitmap2FloatArray(it)
// }
// }
// }
fun bitmap2FloatArray(originBitmap: Bitmap, isRecycle: Boolean): FloatArray? {
var rgb565Bitmap: Bitmap? = null
try {
val scaledBitmap = originBitmap.scale(
MODEL_INPUT_WIDTH,
MODEL_INPUT_HEIGHT
)
if (isRecycle) {
originBitmap.recycle()
}
rgb565Bitmap = scaledBitmap.copy(Bitmap.Config.RGB_565, false)
scaledBitmap.recycle()
val inputTensor = TensorImageUtils.bitmapToFloat32Tensor(
rgb565Bitmap,
NO_MEAN_RGB, // [0.485, 0.456, 0.406] TORCHVISION_NORM_MEAN_RGB
NO_STD_RGB // [0.229, 0.224, 0.225] TORCHVISION_NORM_STD_RGB
)
if (module == null) {
return null
}
Timber.tag(TAG).d("module:${module}")
val iValue = IValue.from(inputTensor)
Timber.tag(TAG).d("inputTensor:${inputTensor}")
// 推理前记录系统可用内存,排查是否因内存不足触发 OOM 重启
MyApp.instance?.let { ctx ->
val am = ctx.getSystemService(android.content.Context.ACTIVITY_SERVICE)
as android.app.ActivityManager
val mi = android.app.ActivityManager.MemoryInfo()
am.getMemoryInfo(mi)
Timber.tag(TAG).d("forward() 前系统可用内存: ${mi.availMem / 1024 / 1024} MB / 总: ${mi.totalMem / 1024 / 1024} MB, lowMemory=${mi.lowMemory}")
}
Timber.tag(TAG).d("forward() 开始: ${System.currentTimeMillis()}")
val outputIValue = inferenceExecutor.submit<IValue?> { module?.forward(iValue) }
.get(10, java.util.concurrent.TimeUnit.SECONDS) ?: return null
Timber.tag(TAG).d("forward() 结束: ${System.currentTimeMillis()}")
Timber.tag(TAG).d("outputIValue:${outputIValue}")
val outputTensor = outputIValue.toTensor()
Timber.tag(TAG).d("outputTensor:${outputTensor}")
return outputTensor.dataAsFloatArray
} catch (e: OutOfMemoryError) {
e.printStackTrace()
} finally {
if (rgb565Bitmap != null && rgb565Bitmap.isRecycled.not()) {
rgb565Bitmap.recycle()
}
//System.gc()
//System.runFinalization()
}
return null
}
fun bitmap2FloatArray(bitmap: Bitmap): FloatArray {
val inputTensor = TensorImageUtils.bitmapToFloat32Tensor(
bitmap,
NO_MEAN_RGB, // [0.485, 0.456, 0.406] TORCHVISION_NORM_MEAN_RGB
NO_STD_RGB // [0.229, 0.224, 0.225] TORCHVISION_NORM_STD_RGB
)
val outputTensor = module_mobile.forward(IValue.from(inputTensor)).toTensor()
return outputTensor.dataAsFloatArray
}
fun queryFood(uri: Uri, queryCount: Int = 15): List<String>? {
return uri2FloatArray(uri)?.let {
queryFood(it, queryCount)
}
}
// fun queryFood(uri: Uri, queryCount: Int = 15): List<String>? {
// return uri2FloatArray(uri)?.let {
// queryFood(it, queryCount)
// }
// }
/**
* 返回识别物品名称列表
*/
fun queryFood(bitmap: Bitmap, queryCount: Int = 15): List<String> {
val floatArray = bitmap2FloatArray(bitmap)
return queryFood(floatArray, queryCount)
}
// fun queryFood(bitmap: Bitmap, queryCount: Int = 15): List<String> {
// val floatArray = bitmap2FloatArray(bitmap)
// return queryFood(floatArray, queryCount)
// }
/**
* 返回识别物品IdNameScore对象列表
*/
fun queryFoodNameScore(bitmap: Bitmap, queryCount: Int = 15): List<IdNameScore> {
val floatArray = bitmap2FloatArray(bitmap)
return queryFoodNameScore(floatArray, queryCount)
}
// fun queryFoodNameScore(bitmap: Bitmap, queryCount: Int = 15): List<IdNameScore> {
// val floatArray = bitmap2FloatArray(bitmap)
// return queryFoodNameScore(floatArray, queryCount)
// }
fun queryFoodNameScore(floatArray: FloatArray, queryCount: Int = 15): List<IdNameScore> {
val query: Query<Food> =
box.query(Food_.foodVector.nearestNeighbors(floatArray, queryCount)).build()
suspend fun queryFoodNameScore(
floatArray: FloatArray?,
queryCount: Int = 15
): List<IdNameScore> {
if (floatArray == null) return emptyList()
val startTime = System.currentTimeMillis()
val size = ObjectBox.getAll().filter { it.isDel.not() }.size
Timber.tag(TAG).d("queryFoodNameScore-已采集向量总数:${size}")
//查询比较分数
// val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" }
val idScoreList = query.findIdsWithScores()
val nameScoreList = mutableListOf<IdNameScore>()
idScoreList.forEach {
nameScoreList.add(IdNameScore(id = it.id, name = box.get(it.id).name?:"", score = it.score))
// val idScoreList = query.findIdsWithScores()
val tripleList = ObjectBox.query(floatArray, queryCount)
Timber.tag(TAG).d("idScoreList:${GsonUtils.toJson(tripleList)}")
val nameScoreList = tripleList.map { (id, name, score) ->
IdNameScore(id = id, name = name ?: "", score = score)
}
Timber.tag("FoodModule").d("registerDataChange,queryFood数据:${GsonUtils.toJson(nameScoreList)}")
val nameScoreData = GsonUtils.toJson(nameScoreList)
Timber.tag(TAG).d("queryFood,耗时:${System.currentTimeMillis() - startTime},数据:$nameScoreData")
return nameScoreList
}
fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = 15): List<IdNameScore> {
val floatArray = bitmap2FloatArray(bitmap)
suspend fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = 15): List<IdNameScore> {
val startTime = System.currentTimeMillis()
val floatArray = withContext(Dispatchers.IO) {
bitmap2FloatArray(bitmap, false)
}
Timber.tag(TAG).d("bitmap2FloatArray,耗时:${System.currentTimeMillis() - startTime}")
val nameScoreList = queryFoodNameScore(floatArray, queryCount)
if (nameScoreList.isEmpty()) {
return emptyList()
}
val maxScoreList = nameScoreList
.filter { it.score < 1 - THRESHOLD }
// .filter { it.score < 1 - THRESHOLD }
.groupBy { it.name }
.map { (_, value) -> value.minByOrNull { it.score }!! }
.toMutableList()
val map = mutableMapOf<String, Int>()
nameScoreList.forEach {
val key = it.name
val count = map[key] ?: 0
map[key] = count + 1
}
val orderList = map.entries.sortedByDescending { it.value }.map { it.key }.toMutableList()
val firstFood = nameScoreList[0].name
orderList.remove(firstFood)
orderList.add(0, firstFood)
val sortedScoreList = maxScoreList.sortedWith(compareBy {
orderList.indexOf(it.name)
})
//Timber.tag("FoodModule").d("getFoodScoreList数据:${sortedScoreList.toJsonString()}")
// val map = mutableMapOf<String, Int>()
// nameScoreList.forEach {
// val key = it.name
// val count = map[key] ?: 0
// map[key] = count + 1
// }
// val orderList = map.entries.sortedByDescending { it.value }.map { it.key }.toMutableList()
// val firstFood = nameScoreList[0].name
// orderList.remove(firstFood)
// orderList.add(0, firstFood)
//
// val sortedScoreList = maxScoreList.sortedWith(compareBy {
// orderList.indexOf(it.name)
// })
val sortedScoreList = maxScoreList.sortedBy { it.score }
Timber.tag("FoodModule").d("getFoodScoreList数据:${sortedScoreList.toString()}")
return sortedScoreList
}
fun queryFood(floatArray: FloatArray, queryCount: Int = 15): List<String> {
val query: Query<Food> =
box.query(Food_.foodVector.nearestNeighbors(floatArray, queryCount)).build()
//查询比较分数
// val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" }
val map = mutableMapOf<String, Int>()
query.findIdsWithScores().forEach {
val food = box.get(it.id)
Timber.d("${food.name}|${food.foodIdx}|${it.score}")
if (1 - it.score >= THRESHOLD) {
//FoodQueryResult(id = it.id, name = food.name, foodIdx = food.foodIdx, score = it.score)
food.name?.let { key ->
val count = map[key] ?: 0
map.put(key, count + 1)
}
}
}
val list = map.entries.sortedByDescending { it.value }.map { it.key }
return list
// fun queryFood(floatArray: FloatArray, queryCount: Int = 15): List<String> {
// val query = box.query()
// .equal(Food_.isDel, false)
// .and()
// .nearestNeighbors(Food_.foodVector, floatArray, queryCount)
// .build()
//// val query: Query<Food> =
//// box.query(Food_.foodVector.nearestNeighbors(floatArray, queryCount)).build()
// //查询比较分数
//// val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" }
// val map = mutableMapOf<String, Int>()
// val nameScoreList = queryFoodNameScore(floatArray, queryCount)
// nameScoreList.filter { it.score < 0.05 }.forEach {
// val count = map[it.name] ?: 0
// map[it.name] = count + 1
// query.findIdsWithScores().forEach {
// val food = box.get(it.id)
// Timber.d("${food.foodName}|${food.foodId}|${food.collectId}|${food.version}|${food.otherField}|${it.score}")
// if (1 - it.score >= THRESHOLD) {
// //FoodQueryResult(id = it.id, name = food.name, foodIdx = food.foodIdx, score = it.score)
// food.foodName?.let { key ->
// val count = map[key] ?: 0
// map.put(key, count + 1)
// }
// }
// }
// val list = map.entries.sortedByDescending { it.value }.map { it.key }
// return list
}
//
//// val map = mutableMapOf<String, Int>()
//// val nameScoreList = queryFoodNameScore(floatArray, queryCount)
//// nameScoreList.filter { it.score < 0.05 }.forEach {
//// val count = map[it.name] ?: 0
//// map[it.name] = count + 1
//// }
//// val list = map.entries.sortedByDescending { it.value }.map { it.key }
//// return list
// }
data class IdNameScore(
val id:Long,
var name:String,
val id: Long,
var name: String,
val score: Double
)
fun initDefFoodData(context: Context, action:()-> Unit={}) {
val count = box.all.count { it.foodIdx == DEFAULT_FOOD_INDEX }
if (count > 0) {
return
}
val embeddingsJson = AssetsTool.readAssetsFile(context, "data/embeddings.json")
val labelsJson = AssetsTool.readAssetsFile(context, "data/labels.json")
val classInfoJson = AssetsTool.readAssetsFile(context, "data/class_info.json")
embeddingsList =
Gson().fromJson(embeddingsJson, object : TypeToken<List<List<Float>>>() {}.type)
labelsList = Gson().fromJson(labelsJson, IntArray::class.java)
classInfo =
Gson().fromJson(classInfoJson, FoodClassInfo::class.java)
val foodMap = classInfo.idx_to_class
embeddingsList.forEachIndexed { index, floatList ->
val classIdx = labelsList[index]
val foodName = foodMap["$classIdx"]
val array = floatList.toFloatArray()
box.put(Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX))
}
action()
}
// fun initDefFoodData(context: Context, action: () -> Unit = {}) {
// //val count = box.all.count { it.foodIdx == DEFAULT_FOOD_INDEX }
// //if (count > 0) {
// // return
// //}
// val embeddingsJson = AssetsTool.readAssetsFile(context, "data/embeddings.json")
// val labelsJson = AssetsTool.readAssetsFile(context, "data/labels.json")
// val classInfoJson = AssetsTool.readAssetsFile(context, "data/class_info.json")
//
// embeddingsList =
// Gson().fromJson(embeddingsJson, object : TypeToken<List<List<Float>>>() {}.type)
// labelsList = Gson().fromJson(labelsJson, IntArray::class.java)
// classInfo =
// Gson().fromJson(classInfoJson, FoodClassInfo::class.java)
//
// val foodMap = classInfo.idx_to_class
// embeddingsList.forEachIndexed { index, floatList ->
// val classIdx = labelsList[index]
// val foodName = foodMap["$classIdx"]
// val array = floatList.toFloatArray()
// box.put(
// Food(
// foodName = foodName,
// foodVector = array,
// //foodIdx = DEFAULT_FOOD_INDEX
// )
// )
// }
// action()
// }
/**
@@ -195,47 +300,75 @@ object FoodModule {
*
* 不可能反正SD
*/
fun copyAssetToCache(context: Context, fileName: String): String? {
// 此app的缓存目录 --> 会默认在 cache目录...,可以自己去看看哦
val cacheDir = context.getCacheDir()
if (!cacheDir.exists()) {
cacheDir.mkdirs() // TODO 如果没有缓存目录,就创建
}
val outPath = File(cacheDir, fileName) // TODO 创建输出的文件位置
if (outPath.exists()) {
outPath.delete() // TODO 如果该文件已经存在,就删掉
}
var `is`: InputStream? = null // 读取
var fos: FileOutputStream? = null // 写入
try {
// 创建文件,如果创建成功,就返回true
val res = outPath.createNewFile()
if (res) {
`is` = context.getAssets().open(fileName) // 拿到main/assets目录的输入流,用于读取字节
fos = FileOutputStream(outPath) // 读取出来的字节最终写到outPath
val buf = ByteArray(`is`.available()) // 缓存区
var byteCount: Int
// fun copyAssetToCache(context: Context, fileName: String): String? {
// // 此app的缓存目录 --> 会默认在 cache目录...,可以自己去看看哦
// val cacheDir = context.cacheDir
// if (!cacheDir.exists()) {
// cacheDir.mkdirs() // TODO 如果没有缓存目录,就创建
// }
// val outPath = File(cacheDir, fileName) // TODO 创建输出的文件位置
// if (outPath.exists()) {
// outPath.delete() // TODO 如果该文件已经存在,就删掉
// }
// var `is`: InputStream? = null // 读取
// var fos: FileOutputStream? = null // 写入
// try {
// // 创建文件,如果创建成功,就返回true
// val res = outPath.createNewFile()
// if (res) {
// `is` = context.assets.open(fileName) // 拿到main/assets目录的输入流,用于读取字节
// fos = FileOutputStream(outPath) // 读取出来的字节最终写到outPath
// val buf = ByteArray(`is`.available()) // 缓存区
// var byteCount: Int
//
// // 开始循环读取
// while ((`is`.read(buf).also { byteCount = it }) != -1) {
// fos.write(buf, 0, byteCount)
// }
// return outPath.absolutePath
// }
// } catch (e: IOException) {
// e.printStackTrace()
// } finally {
// try {
// // TODO 一定要记得关闭资源,为了不去性能的磨损
// fos!!.flush()
// `is`!!.close()
// fos.close()
// } catch (e: IOException) {
// e.printStackTrace()
// }
// }
// return null
// }
// 开始循环读取
while ((`is`.read(buf).also { byteCount = it }) != -1) {
fos.write(buf, 0, byteCount)
}
return outPath.getAbsolutePath()
fun copyAssetToCache(context: Context, fileName: String): String? {
val cacheFile = File(context.cacheDir, fileName)
val buffer = ByteArray(8 * 1024)
var inputStream: InputStream? = null
var outputStream: FileOutputStream? = null
try {
inputStream = context.assets.open(fileName)
outputStream = FileOutputStream(cacheFile)
var byteCount: Int
while (inputStream.read(buffer).also { byteCount = it } != -1) {
outputStream.write(buffer, 0, byteCount)
}
outputStream.channel.force(true) // 强制物理落盘,比flush更彻底
return cacheFile.absolutePath
} catch (e: IOException) {
e.printStackTrace()
} finally {
try {
// TODO 一定要记得关闭资源,为了不去性能的磨损
fos!!.flush()
`is`!!.close()
fos.close()
} catch (e: IOException) {
e.printStackTrace()
Timber.tag("FoodModule").d("文件拷贝失败 fileName=$fileName, error=${e.message}")
// 拷贝失败时删除残缺文件,避免下次读取到损坏文件
if (cacheFile.exists()) {
cacheFile.delete()
}
} finally {
outputStream?.close()
inputStream?.close()
}
return null
}
}
@@ -17,18 +17,34 @@
package com.sw.dualscreen.objbox
import android.content.Context
import android.os.Environment
import android.util.Log
import com.sw.dualscreen.BuildConfig
import com.sw.plate.App
import io.objectbox.Box
import io.objectbox.BoxStore
import io.objectbox.BoxStoreBuilder
import io.objectbox.android.Admin
import io.objectbox.android.ObjectBoxLiveData
import io.objectbox.config.DebugFlags
import io.objectbox.exception.DbException
import io.objectbox.exception.FileCorruptException
import io.objectbox.kotlin.boxFor
import io.objectbox.query.Query
import io.objectbox.sync.Sync
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.util.Date
import java.util.zip.GZIPOutputStream
import kotlin.also
import kotlin.and
import kotlin.io.copyTo
import kotlin.io.inputStream
import kotlin.io.outputStream
@@ -49,8 +65,7 @@ object ObjectBox {
private const val TAG = "ObjectBox"
lateinit var boxStore: BoxStore
private set
var boxStore: BoxStore? = null
/**
* If building the [boxStore] failed, contains the thrown error message.
@@ -62,10 +77,11 @@ object ObjectBox {
// On Android make sure to pass a Context when building the Store.
boxStore = try {
MyObjectBox.builder()
.androidContext(context.applicationContext)
.build()
.androidContext(context.applicationContext)
.debugFlags(DebugFlags.LOG_QUERY_PARAMETERS)
.build()
} catch (e: DbException) {
if (e.javaClass.equals(DbException::class.java) || e is FileCorruptException) {
if (e.javaClass == DbException::class.java || e is FileCorruptException) {
// Failed to build BoxStore due to database file issue, store message;
// checked in NoteListActivity to notify user.
dbExceptionMessage = e.toString()
@@ -76,13 +92,14 @@ object ObjectBox {
}
}
// if (BuildConfig.DEBUG) {
var syncAvailable = if (Sync.isAvailable()) "available" else "unavailable"
Log.d(TAG,"Using ObjectBox ${BoxStore.getVersion()} (${BoxStore.getVersionNative()}, sync $syncAvailable)")
if (BuildConfig.DEBUG) {
val syncAvailable = if (Sync.isAvailable()) "available" else "unavailable"
Timber.tag(TAG)
.d("Using ObjectBox ${BoxStore.getVersion()} (${BoxStore.getVersionNative()}, sync $syncAvailable)")
// Enable ObjectBox Admin on debug builds.
// https://docs.objectbox.io/data-browser
Admin(boxStore).start(context.applicationContext)
// }
}
}
@@ -94,7 +111,7 @@ object ObjectBox {
// Do not copy if database file is still in use.
// If it would be open, the copy will likely get corrupted
// as BoxStore may currently write data to the file.
Log.e(TAG, "Database file is still in use, can not copy.")
Timber.tag(TAG).e("Database file is still in use, can not copy.")
return false
}
@@ -109,4 +126,260 @@ object ObjectBox {
return true
}
// 获取指定实体的Box,自动关联BoxStore
inline fun <reified T> getBox(): Box<T>? = boxStore?.boxFor()
// 协程安全执行数据库操作(推荐所有操作使用此方法)
suspend fun <T> safeDbOp(operation: suspend () -> T): T? {
return withContext(Dispatchers.IO) {
try {
dbMutex.withLock { operation() }
} catch (e: FileCorruptException) {
// 操作中触发损坏,尝试重建数据库
//val context = boxStore.context
//deleteDbFiles(context)
init(App.getContext()!!)
null
} catch (e: Exception) {
e.printStackTrace()
null
} finally {
// 线程资源清理
boxStore?.closeThreadResources()
}
}
}
suspend fun query(floatArray: FloatArray, queryCount: Int) = safeDbOp {
val box = getBox<Food>() ?: return@safeDbOp emptyList<Triple<Long, String?, Double>>()
val query: Query<Food> = box.query()
.equal(Food_.isDel, false)
.and()
.nearestNeighbors(Food_.foodVector, floatArray, queryCount)
.build()
try {
// 统一使用 findIdsWithScores,避免在 32 位系统上因加载大对象而崩溃
val idScoreList = query.findIdsWithScores()
if (idScoreList.isNotEmpty()) {
// 根据 ID 查询完整对象,仅提取 id 和 name,丢弃 foodVector 以节省内存
val foodMap = box.get(idScoreList.map { it.id }).associateBy { it.id }
idScoreList.map { scoreId ->
val food = foodMap[scoreId.id]
Triple(scoreId.id, food?.foodName, scoreId.score)
}
} else {
emptyList()
}
} catch (e: DbException) {
Timber.tag(TAG).e(e, "向量查询失败")
emptyList()
} finally {
query.close()
}
} ?: emptyList()
// suspend fun queryWithScore(floatArray: FloatArray, queryCount: Int) = safeDbOp {
// val box = getBox<Food>() ?: return@safeDbOp emptyList()
//
// val query: Query<Food>? = box.query()
// ?.equal(Food_.isDel, false)
// ?.and()
// ?.nearestNeighbors(Food_.foodVector, floatArray, queryCount)
// ?.build()
//
// try {
// // 检测是否为 32 位系统
// val is32Bit = Build.SUPPORTED_ABIS.any {
// it.contains("armeabi") && !it.contains("arm64")
// }
//
// if (is32Bit) {
// // 32位系统:使用 findWithScoresAndIds 替代 findWithScores
// val scoreIds = query?.findWithScoresAndIds()
// if (scoreIds != null && scoreIds.isNotEmpty()) {
// // 如果需要分数,返回 ScoreId 对象
// // 注意:这需要修改返回类型或使用其他方式传递分数
// scoreIds.toList()
// } else {
// emptyList()
// }
// } else {
// // 64位系统:正常使用 findWithScores
// query?.findWithScores() ?: emptyList()
// }
// } catch (e: DbException) {
// // 如果仍然失败,记录日志并返回空列表
// Timber.tag(TAG).e(e, "Vector query with score failed, returning empty list")
// emptyList()
// } finally {
// query?.close()
// }
// } ?: emptyList()
// suspend fun query(floatArray: FloatArray, queryCount: Int) = safeDbOp {
// val box = getBox<Food>() ?: return@safeDbOp emptyList()
//
// val query: Query<Food>? = box.query()
// ?.equal(Food_.isDel, false)
// ?.and()
// ?.nearestNeighbors(Food_.foodVector, floatArray, queryCount)
// ?.build()
//
// try {
// // 检测是否为 32 位系统
// val is32Bit = Build.SUPPORTED_ABIS.any {
// it.contains("armeabi") && !it.contains("arm64")
// }
//
// if (is32Bit) {
// // 32位系统:先获取带分数的IDs,再批量获取完整对象
// val scoreIds = query?.findWithScoresAndIds()
// if (scoreIds != null && scoreIds.isNotEmpty()) {
// // 提取 IDs
// val ids = scoreIds.map { it.id }.toLongArray()
// // 批量获取完整对象
// val foods = box.get(ids.toList()).associateBy { it.id }
// // 按照分数顺序组装结果(保持排序)
// scoreIds.mapNotNull { scoreId ->
// foods[scoreId.id]
// }
// } else {
// emptyList()
// }
// } else {
// // 64位系统:直接使用 findWithScores
// query?.findWithScores() ?: emptyList()
// }
// } catch (e: DbException) {
// // 如果仍然失败,记录日志并返回空列表
// Timber.tag(TAG).e(e, "Vector query failed, returning empty list")
// emptyList()
// } finally {
// query?.close()
// }
// } ?: emptyList()
// ... existing code ...
suspend fun get(id: Long) = safeDbOp {
getBox<Food>()?.get(id)
}
suspend fun put(entity: Food) = safeDbOp {
getBox<Food>()?.put(entity)
}
suspend fun putAll(entities: List<Food>) = safeDbOp {
getBox<Food>()?.put(entities)
}
suspend fun getAll() = safeDbOp {
getBox<Food>()?.all
} ?: emptyList()
suspend fun filter(name: String?) = safeDbOp {
if (!name.isNullOrBlank()) {
getBox<Food>()?.query(Food_.foodName.contains(name))?.build()?.find()
?.distinctBy { it.foodName }
} else {
getBox<Food>()?.all?.distinctBy { it.foodName }
} ?: emptyList()
} ?: emptyList()
suspend fun removeAll() = safeDbOp {
getBox<Food>()?.removeAll()
}
suspend fun remove(name: String) = safeDbOp {
getBox<Food>()?.query(Food_.foodName.equal(name))?.build()?.remove()
}
private val dbMutex = Mutex() // 协程并发锁,保证写入操作原子性
private const val DB_DIR_NAME = "objectbox" // ObjectBox 默认数据库目录
private const val BACKUP_DIR_NAME = "objectbox_backup" // 备份目录
// 检查存储是否可读写(操作数据库前调用)
fun isStorageAvailable(context: Context): Boolean {
return try {
context.filesDir.canRead() && context.filesDir.canWrite()
} catch (e: Exception) {
false
}
}
// 备份数据库到应用私有目录(无权限要求,推荐)
suspend fun backupDb(context: Context): Boolean = withContext(Dispatchers.IO) {
if (!isStorageAvailable(context)) return@withContext false
val dbDir = File(context.filesDir, DB_DIR_NAME)
val backupDir = File(context.filesDir, BACKUP_DIR_NAME)
return@withContext copyDir(dbDir, backupDir)
}
// 从备份恢复数据库(恢复后会重建BoxStore)
suspend fun restoreDb(context: Context): Boolean = withContext(Dispatchers.IO) {
if (!isStorageAvailable(context)) return@withContext false
val dbDir = File(context.filesDir, DB_DIR_NAME)
val backupDir = File(context.filesDir, BACKUP_DIR_NAME)
if (!backupDir.exists()) return@withContext false
// 先关闭旧的BoxStore,删除损坏文件,再恢复备份
boxStore?.close()
deleteDbFiles(context)
val isSuccess = copyDir(backupDir, dbDir)
// 重新初始化
init(context)
return@withContext isSuccess
}
// 递归删除数据库文件
private fun deleteDbFiles(context: Context) {
val dbDir = File(context.filesDir, DB_DIR_NAME)
if (dbDir.exists()) deleteDirRecursively(dbDir)
}
// 递归删除目录
private fun deleteDirRecursively(file: File) {
if (file.isDirectory) {
file.listFiles()?.forEach { deleteDirRecursively(it) }
}
file.delete()
}
// 递归复制目录(核心备份/恢复逻辑)
private fun copyDir(srcDir: File, destDir: File): Boolean {
return try {
if (!srcDir.exists()) return false
if (!destDir.exists()) destDir.mkdirs()
srcDir.listFiles()?.forEach { srcFile ->
val destFile = File(destDir, srcFile.name)
if (srcFile.isDirectory) {
copyDir(srcFile, destFile)
} else {
copyFile(srcFile, destFile)
}
}
true
} catch (e: IOException) {
e.printStackTrace()
false
}
}
// 复制单个文件(使用NIO,高效稳定)
private fun copyFile(srcFile: File, destFile: File) {
FileInputStream(srcFile).channel.use { srcChannel ->
FileOutputStream(destFile).channel.use { destChannel ->
destChannel.transferFrom(srcChannel, 0, srcChannel.size())
}
}
}
// 关闭BoxStore(应用退出时调用,可选)
fun close() {
boxStore?.close()
boxStore = null
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -4,7 +4,8 @@ package com.sw.dualscreen.presentation
import android.text.TextUtils
import com.sw.dualscreen.ext.toSafeDouble
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.UserNutritionData
import com.sw.dualscreen.model.response.UserEnergy
import com.sw.dualscreen.model.response.UserNutrition
import timber.log.Timber
import kotlin.math.max
@@ -16,64 +17,112 @@ object UserNutritionUtils {
/**
* 计算热量信息
*/
fun calculateNutrition(
fun calculateNutrition2(
foodInfo: FoodInfo,
userModel: UserNutritionData,
nutrition: UserNutrition,
weight: Double,
dinnerType: String
): CalcResultInfo {
): UserEnergy {
// 初始化变量
var foodKcal = 0.0
var totalKcal = 0.0
var (vegetable, meat, fruits, grain) = List(4) { 0.0 }
//var totalKcal = 0.0
var (calorie, grain, fruitsVegetables, meatEggs) = List(4) { 0.0 }
// 处理食物信息
foodKcal = calculateValue(foodInfo.stFoodInfoMaterial?.energyKcal, weight)
Timber.d("calculateNutrition foodKcal = ${foodKcal}, energyKcal = ${foodInfo.stFoodInfoMaterial?.energyKcal}, weight = $weight")
grain = calculateValue(foodInfo.stFoodInfoPagodaAPPVO?.grainValue(), weight)
fruits = calculateValue(foodInfo.stFoodInfoPagodaAPPVO?.fruitsValue(), weight)
vegetable = calculateValue(foodInfo.stFoodInfoPagodaAPPVO?.vegetableValue(), weight)
meat = calculateValue(foodInfo.stFoodInfoPagodaAPPVO?.meatValue(), weight)
if (weight > 0.0) {
calorie = calculateValue2(foodInfo.calorie,weight)
grain = calculateValue2(foodInfo.stapleFood, weight)
fruitsVegetables = calculateValue2(foodInfo.fruitsVegetables, weight)
meatEggs = calculateValue2(foodInfo.meatEggs, weight)
}
// 合并用户数据
grain += userModel.stFoodInfoPagoda?.grainValue() ?: 0.0
//val maxKcal = (nutrition.calorie?:0.0) * calculateDinnerTypeRatio(dinnerType) / 10.0
calorie += nutrition.calorie ?: 0.0
grain += nutrition.stapleFood ?: 0.0
fruitsVegetables += nutrition.fruitsVegetables ?: 0.0
meatEggs += nutrition.meatEggs ?: 0.0
fruits += userModel.stFoodInfoPagoda?.fruitsValue() ?: 0.0
vegetable = vegetable.plus(userModel.stFoodInfoPagoda?.vegetableValue() ?: 0.0)
meat += userModel.stFoodInfoPagoda?.meatValue() ?: 0.0
totalKcal = foodKcal + userModel.energyValue()
Timber.d("calculateNutrition totalKcal = ${totalKcal}, energyValue = ${userModel.energyValue()}")
if (totalKcal < 0){
totalKcal = 0.0
}
//totalKcal = max(calorie, 0.0)
// 判断当餐最大热量
val maxKcal =
userModel.totalEnergyCalculateScoreValue() * calculateDinnerTypeRatio(dinnerType) / 10
Timber.d("calculateNutrition maxKcal = ${maxKcal}, CalculateScoreValue = ${userModel.totalEnergyCalculateScoreValue()}")
val pagoda = userModel.stFoodInfoPagoda
val fruitsInfo = parseRecommend(pagoda?.fruitsRecommend, fruits)
val vegetableInfo = parseRecommend(pagoda?.vegetableRecommend, vegetable)
val calcResult = CalcResultInfo(
totalKcal = CalcInfo(
max = maxKcal,
min = 0.0,
current = totalKcal
),
grain = parseRecommend(pagoda?.grainRecommend, grain),
fruits = CalcInfo( // 果蔬 = 水果+蔬菜
max = fruitsInfo.max + vegetableInfo.max,
min = fruitsInfo.min + vegetableInfo.min,
current = fruitsInfo.current + vegetableInfo.current
),
meat = parseRecommend(pagoda?.meatRecommend, meat)
//val maxKcal = userModel.totalEnergyCalculateScoreValue() * calculateDinnerTypeRatio(dinnerType) / 10
//Timber.d("calculateNutrition maxKcal = ${maxKcal}, CalculateScoreValue = ${userModel.totalEnergyCalculateScoreValue()}")
// val calcResult = CalcResultInfo(
// totalKcal = CalcInfo(
// max = maxKcal,
// min = 0.0,
// current = totalKcal
// ),
// grain = parseRecommend(foodInfo.stapleFoodRecommend, grain),
// fruits = parseRecommend(foodInfo.fruitsVegetablesRecommend, fruitsVegetables),
// meat = parseRecommend(foodInfo.meatEggsRecommend, meatEggs)
// )
return UserEnergy(
calorie = calorie,
grain = grain,
fruitsVegetables = fruitsVegetables,
meatEggs = meatEggs
)
return calcResult
}
// fun calculateNutrition(
// foodInfo: FoodInfo,
// userModel: UserNutritionData,
//// userModel: UserNutrition,
// weight: Double,
// dinnerType: String
// ): CalcResultInfo {
//
// // 初始化变量
// var foodKcal = 0.0
// var totalKcal = 0.0
// var (vegetable, meat, fruits, grain) = List(4) { 0.0 }
//
// // 处理食物信息
// foodKcal = calculateValue(foodInfo.stFoodInfoMaterial?.energyKcal, weight)
// Timber.d("calculateNutrition foodKcal = ${foodKcal}, energyKcal = ${foodInfo.stFoodInfoMaterial?.energyKcal}, weight = $weight")
// grain = calculateValue(foodInfo.stFoodInfoPagodaAPPVO?.grainValue(), weight)
// fruits = calculateValue(foodInfo.stFoodInfoPagodaAPPVO?.fruitsValue(), weight)
// vegetable = calculateValue(foodInfo.stFoodInfoPagodaAPPVO?.vegetableValue(), weight)
// meat = calculateValue(foodInfo.stFoodInfoPagodaAPPVO?.meatValue(), weight)
//
// // 合并用户数据
// grain += userModel.stFoodInfoPagoda?.grainValue() ?: 0.0
//
// fruits += userModel.stFoodInfoPagoda?.fruitsValue() ?: 0.0
// vegetable = vegetable.plus(userModel.stFoodInfoPagoda?.vegetableValue() ?: 0.0)
// meat += userModel.stFoodInfoPagoda?.meatValue() ?: 0.0
//
// totalKcal = foodKcal + userModel.energyValue()
// Timber.d("calculateNutrition totalKcal = ${totalKcal}, energyValue = ${userModel.energyValue()}")
// if (totalKcal < 0) {
// totalKcal = 0.0
// }
// // 判断当餐最大热量
// val maxKcal =
// userModel.totalEnergyCalculateScoreValue() * calculateDinnerTypeRatio(dinnerType) / 10
// Timber.d("calculateNutrition maxKcal = ${maxKcal}, CalculateScoreValue = ${userModel.totalEnergyCalculateScoreValue()}")
// val pagoda = userModel.stFoodInfoPagoda
// val fruitsInfo = parseRecommend(pagoda?.fruitsRecommend, fruits)
// val vegetableInfo = parseRecommend(pagoda?.vegetableRecommend, vegetable)
// val calcResult = CalcResultInfo(
// totalKcal = CalcInfo(
// max = maxKcal,
// min = 0.0,
// current = totalKcal
// ),
// grain = parseRecommend(pagoda?.grainRecommend, grain),
// fruits = CalcInfo( // 果蔬 = 水果+蔬菜
// max = fruitsInfo.max + vegetableInfo.max,
// min = fruitsInfo.min + vegetableInfo.min,
// current = fruitsInfo.current + vegetableInfo.current
// ),
// meat = parseRecommend(pagoda?.meatRecommend, meat)
// )
// return calcResult
// }
fun parseRecommend(recommend: String?, current: Double): CalcInfo {
var newCurrent = if (current < 0) 0.0 else current
val newCurrent = if (current < 0) 0.0 else current
if (TextUtils.isEmpty(recommend) || !recommend!!.contains("-")) {
Timber.e("parseRecommend recommend 格式错误")
return CalcInfo(
@@ -93,7 +142,10 @@ object UserNutritionUtils {
else -> 0
}
}
private fun calculateValue2(nutrient: Double?, weight: Double): Double {
if ((nutrient?:0.0) == 0.0) return 0.0
return nutrient!! * weight / 100.0
}
/**
* 计算每百克含量
*/
@@ -106,6 +158,36 @@ object UserNutritionUtils {
return max(max1, calcResultInfo.meat.max).toInt()
}
fun getCalorieArray(left:String?, mid:String?, right: String?):DoubleArray {
val leftArray = (left?.ifBlank { "0-0" } ?: "0-0").split("-").map { it.toSafeDouble() }
val midArray = (mid?.ifBlank { "0-0" } ?: "0-0").split("-").map { it.toSafeDouble() }
val rightArray = (right?.ifBlank { "0-0" } ?: "0-0").split("-").map { it.toSafeDouble() }
return doubleArrayOf(
0.0, leftArray[0], midArray[0], rightArray[0], rightArray[1]
)
}
fun findCalorieIndex(num: Double, array: DoubleArray): Int {
if (array.isEmpty()) return 0
if (num < array[0]) return 0
if (num >= array[array.lastIndex]) return array.lastIndex
var left = 0
var right = array.lastIndex
while (left < right) {
val mid = left + (right - left) / 2
if (array[mid] <= num && num < array[mid + 1]) {
return mid
} else if (num < array[mid]) {
right = mid
} else {
left = mid + 1
}
}
return 0
}
data class CalcInfo(
var max: Double,
var min: Double,
@@ -0,0 +1,49 @@
//package com.sw.dualscreen.presentation.pay
//
//import android.R
//import android.app.Presentation
//import android.os.Bundle
//import android.text.Spanned
//import android.text.SpannedString
//import android.text.style.AbsoluteSizeSpan
//import android.view.Display
//import androidx.core.text.buildSpannedString
//import com.sw.dualscreen.activity.PayActivity
//import com.sw.dualscreen.databinding.PresentationCashPayBinding
//import com.sw.dualscreen.ext.load
//
//class CashPayPresentation(
// val activity: PayActivity,
// display: Display,
// private val onDismissListener: () -> Unit = {}
//) : Presentation(activity, display) {
//
// private lateinit var binding: PresentationCashPayBinding
//
// override fun onCreate(savedInstanceState: Bundle?) {
// super.onCreate(savedInstanceState)
// binding = PresentationCashPayBinding.inflate(layoutInflater)
// setContentView(binding.root)
// window?.setBackgroundDrawableResource(R.color.transparent)
// initView()
// }
//
// var foodName: String? = null
// var payAmount: String? = null
// private fun initView() {
// binding.tvFoodName.text = foodName
// binding.tvRealAmount.text = getAmountText(payAmount ?: "")
// }
//
// private fun getAmountText(amount: String): SpannedString {
// return buildSpannedString {
// append("¥", AbsoluteSizeSpan(32, true), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
// append(amount, AbsoluteSizeSpan(48, true), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
// }
// }
//
// override fun onDisplayRemoved() {
// super.onDisplayRemoved()
// onDismissListener()
// }
//}
@@ -0,0 +1,585 @@
//package com.sw.dualscreen.presentation.pay
//
//import android.Manifest
//import android.app.Dialog
//import android.app.Presentation
//import android.content.pm.PackageManager
//import android.graphics.Point
//import android.hardware.Camera
//import android.os.Build
//import android.os.Bundle
//import android.text.TextUtils
//import android.view.Display
//import android.view.View
//import android.view.ViewGroup
//import android.view.ViewTreeObserver
//import android.widget.FrameLayout
//import android.widget.TextView
//import androidx.annotation.RequiresApi
//import androidx.core.content.ContextCompat
//import androidx.lifecycle.Observer
//import androidx.lifecycle.lifecycleScope
//import com.arcsoft.face.ErrorInfo
//import com.sw.dualscreen.GlobalKey
//import com.sw.dualscreen.R
//import com.sw.dualscreen.activity.PayActivity
//import com.sw.dualscreen.databinding.PresentationFacePayBinding
//import com.sw.dualscreen.ext.dp
//import com.sw.dualscreen.model.response.MemberInfo
//import com.sw.dualscreen.model.response.TextBean
//import com.sw.dualscreen.utils.SPUtil
//import com.sw.dualscreen.utils.SpannedUtils
//import com.sw.dualscreen.view.CustomDialog
//import com.sw.dualscreen.viewmodel.UserViewModel
//import com.sw.plate.utils.ToastUtils
//import com.sw.plate.utils.arcface.ConfigUtil
//import com.sw.plate.utils.arcface.ErrorCodeUtil
//import com.sw.plate.utils.arcface.FaceRectTransformer
//import com.sw.plate.utils.arcface.FaceRectView
//import com.sw.plate.utils.arcface.FaceRectView.DrawInfo
//import com.sw.plate.utils.arcface.PreviewConfig
//import com.sw.plate.utils.arcface.camera.CameraListener
//import com.sw.plate.utils.arcface.camera.DualCameraHelper
//import com.sw.plate.utils.arcface.face.constants.LivenessType
//import com.sw.plate.utils.arcface.face.model.CompareResult
//import com.sw.plate.utils.arcface.face.model.FacePreviewInfo
//import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration
//import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
//import kotlinx.coroutines.launch
//import timber.log.Timber
//
//class FacePayPresentation(
// val activity: PayActivity,
// display: Display,
// val userViewModel: UserViewModel,
// val recognizeViewModel: RecognizeViewModel,
//// val //parentTextureView: TextureView,
// private val onDismissListener: () -> Unit = {}
//) : Presentation(activity, display), ViewTreeObserver.OnGlobalLayoutListener {
//
// companion object {
// private const val TAG = "FacePayPresentation"
// }
//
// private lateinit var binding: PresentationFacePayBinding
//
// // 虹软人脸配置 ⬇
// private var isRecognition = false
// private var rgbCameraHelper: DualCameraHelper? = null
// private var rgbFaceRectTransformer: FaceRectTransformer? = null
// private val livenessType = LivenessType.IR
// private var openRectInfoDraw = false
//
// private var irCameraHelper: DualCameraHelper? = null
// private var irFaceRectTransformer: FaceRectTransformer? = null
//
// override fun onCreate(savedInstanceState: Bundle?) {
// super.onCreate(savedInstanceState)
// binding = PresentationFacePayBinding.inflate(layoutInflater)
// setContentView(binding.root)
// window?.setBackgroundDrawableResource(android.R.color.white)
// initView()
// setupArcCamera()
// registerDataChange()
//
// pauseCamera()
// }
//
// override fun show() {
// super.show()
//
// binding.root.postDelayed({
// resumeCamera()
// }, 1500)
// }
//
// var foodName: String? = null
// var payAmount: String? = null
// private fun initView() {
// binding.tvFoodName.text = foodName
// binding.tvRealAmount.text = SpannedUtils.getAmountText(
// listOf(
// TextBean(text = "¥", textSize = 32),
// TextBean(text = payAmount ?: "", textSize = 48),
// )
// )
//// binding.ivPreviewImage.let {
//// it.outlineProvider = object : ViewOutlineProvider() {
//// override fun getOutline(view: View, outline: Outline) {
//// outline.setRoundRect(0, 0, view.width, view.height, 12f.dp)
//// }
//// }
//// it.clipToOutline = true
//// }
// }
//
// override fun onGlobalLayout() {
// Timber.tag(TAG).d("onGlobalLayout")
// binding.dualCameraTexturePreviewRgb.getViewTreeObserver().removeOnGlobalLayoutListener(this)
// //parentTextureView.getViewTreeObserver().removeOnGlobalLayoutListener(this)
// openCamera()
// }
//
// private fun openCamera() {
// Timber.tag(TAG).d("openCamera")
// if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) !=
// PackageManager.PERMISSION_GRANTED
// ) {
// ToastUtils.showToast("无摄像头权限")
// return
// }
// try {
// val cameraCount = Camera.getNumberOfCameras()
// if (cameraCount < 3) {
// ToastUtils.showToast("摄像头数量异常")
// return
// }
// recognizeViewModel.init(
// PreviewConfig(
// 2, 1, 90, 90
// )
// )
// initRgbCamera()
// if (DualCameraHelper.hasDualCamera() && livenessType === LivenessType.IR) {
// initIrCamera()
// }
// } catch (e: Exception) {
// Timber.tag(TAG).e(e, "打开摄像头失败")
// }
// }
//
// /**
// * 启用虹软人脸失败
// */
// private fun setupArcCamera() {
// initArcViewModel()
// initArcView()
// openRectInfoDraw = true
// }
//
// private fun registerDataChange() {
// activity.lifecycleScope.launch {
// userViewModel.loadFaceResult.collect { needUpdate ->
// if (needUpdate) {
// recognizeViewModel.refreshFaceList()
// }
// }
// }
// }
//
// override fun onStop() {
// rgbCameraHelper?.release()
// rgbCameraHelper = null
// irCameraHelper?.release()
// irCameraHelper = null
// recognizeViewModel.destroy()
// super.onStop()
// }
//
// fun resumeCamera() {
// Timber.tag(TAG).d("resumeCamera isRecognition = $isRecognition")
// isRecognition = true
// if (rgbCameraHelper?.isStopped == true) {
// rgbCameraHelper?.start()
// }
// }
//
// fun pauseCamera() {
// Timber.tag(TAG).d("pauseCamera isRecognition = $isRecognition")
// isRecognition = false
//
// recognizeViewModel.onPreviewFrame(ByteArray(1382400), true)
// }
//
// private fun initArcViewModel() {
// recognizeViewModel.setLiveType(livenessType)
// recognizeViewModel.ftInitCode.observe(activity, Observer { ftInitCode: Int? ->
// if (ftInitCode != ErrorInfo.MOK) {
// val error: String? = context.getString(
// R.string.specific_engine_init_failed, "ftEngine",
// ftInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(ftInitCode!!)
// )
// Timber.tag(TAG).e("ftInitCode observe = $error")
// ToastUtils.showToast(error)
// }
// })
// recognizeViewModel.frInitCode.observe(activity, Observer { frInitCode: Int? ->
// if (frInitCode != ErrorInfo.MOK) {
// val error: String? = context.getString(
// R.string.specific_engine_init_failed, "frEngine",
// frInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(frInitCode!!)
// )
// Timber.tag(TAG).e("frInitCode observe = $error")
// ToastUtils.showToast(error)
// }
// })
// recognizeViewModel.flInitCode.observe(activity, Observer { flInitCode: Int? ->
// if (flInitCode != ErrorInfo.MOK) {
// val error: String? = context.getString(
// R.string.specific_engine_init_failed, "flEngine",
// flInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(flInitCode!!)
// )
// Timber.tag(TAG).e("flInitCode observe = $error")
// ToastUtils.showToast(error)
// }
// })
//
// recognizeViewModel.recognizeConfiguration
// .observe(activity, Observer { recognizeConfiguration: RecognizeConfiguration? ->
// Timber.tag(TAG)
// .i("recognizeConfiguration: observe = ${recognizeConfiguration.toString()}")
// })
// recognizeViewModel.recognizeNotice.observe(activity, Observer { notice: String? ->
// Timber.tag(TAG).i("recognizeNotice observe notice = $notice")
// })
//
// recognizeViewModel.recognizeUserId.observe(
// activity,
// Observer { compareResult: CompareResult ->
// Timber.tag(TAG)
// .i("recognizeUserId observe compareResult = ${compareResult.trackId}, userId = ${compareResult.faceEntity.userName}")
//// recognitionTime = System.currentTimeMillis()
//// recognitionWeight = lastWeight
// lastFaceTrackId = compareResult.trackId
// val faceEntity = compareResult.faceEntity
// val userId = faceEntity.userName
// if (userId == null) return@Observer
// // TODO: 测试支付用户id
//// userId = "1987710988425662466"
// faceRecSuccess(userId)
// })
//
// recognizeViewModel.drawRectInfoText.observe(activity, Observer { info ->
// Timber.tag(TAG).i("drawRectInfoText observe info = $info")
// })
// }
//
// private fun initArcView() {
// //在布局结束后才做初始化操作
// binding.dualCameraTexturePreviewRgb.getViewTreeObserver().addOnGlobalLayoutListener(this)
// //parentTextureView.getViewTreeObserver().addOnGlobalLayoutListener(this)
// recognizeViewModel.getCompareResultList().getValue()
// }
//
// /**
// * 调整View的宽高,使预览显示正常且采集框固定为
// *
// * @param rgbPreview RGB预览View
// * @param previewView 显示预览数据的view
// * @param faceRectView 画框的view
// * @param previewSize 预览大小
// * @param displayOrientation 相机旋转角度
// * @param scale 缩放比例
// * @return 调整后的LayoutParams
// */
// private fun adjustPreviewViewSize(
// rgbPreview: View,
// previewView: View,
// faceRectView: FaceRectView,
// previewSize: Camera.Size,
// displayOrientation: Int,
// scale: Float
// ): ViewGroup.LayoutParams {
// val w = (600.dp * 1.5).toInt()
// val h = 1068.dp
// val layoutParams = FrameLayout.LayoutParams(w, h)
//
// previewView.setLayoutParams(layoutParams)
// faceRectView.setLayoutParams(layoutParams)
// return layoutParams
// }
//
// private fun initRgbCamera() {
// val cameraListener: CameraListener = object : CameraListener {
// override fun onCameraOpened(
// camera: Camera,
// cameraId: Int,
// displayOrientation: Int,
// isMirror: Boolean
// ) {
// Timber.tag(TAG)
// .d("initRgbCamera Rgb onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation")
// activity.runOnUiThread({
// val previewSizeRgb = camera.getParameters().getPreviewSize()
// val layoutParams = adjustPreviewViewSize(
// binding.dualCameraTexturePreviewRgb,
// binding.dualCameraTexturePreviewRgb, binding.dualCameraFaceRectView,
// previewSizeRgb, displayOrientation, 0.6F
// )
//
// Timber.tag(TAG)
// .d("initRgbCamera previewSizeRgb = ${previewSizeRgb.width}-${previewSizeRgb.height}")
// Timber.tag(TAG)
// .d("initRgbCamera layoutParams = ${layoutParams.width}-${layoutParams.height}")
// Timber.tag(TAG).d(
// "initRgbCamera isMirror = ${isMirror}, isDrawRgbRectHorizontalMirror = ${
// ConfigUtil.isDrawRgbRectHorizontalMirror(
// context
// )
// }, isDrawRgbRectVerticalMirror = ${
// ConfigUtil.isDrawRgbRectVerticalMirror(
// context
// )
// }"
// )
// // 调整识别窗口位置
// rgbFaceRectTransformer = FaceRectTransformer(
// previewSizeRgb.width,
// previewSizeRgb.height,
//// layoutParams.width,
//// layoutParams.height,
// layoutParams.width,
// layoutParams.height,
// 90,
// cameraId,
// isMirror,
// true,
// true
// )
//
// recognizeViewModel.onRgbCameraOpened(camera)
// recognizeViewModel.setRgbFaceRectTransformer(rgbFaceRectTransformer)
// })
// }
//
// @RequiresApi(api = Build.VERSION_CODES.Q)
// override fun onPreview(nv21: ByteArray?, camera: Camera?) {
// if (!isRecognition) {
// return
// }
// binding.dualCameraFaceRectView.clearFaceInfo()
// val facePreviewInfoList: MutableList<FacePreviewInfo?>? =
// recognizeViewModel.onPreviewFrame(nv21, true)
// if (facePreviewInfoList != null && rgbFaceRectTransformer != null) {
// drawPreviewInfo(facePreviewInfoList)
// }
// recognizeViewModel.clearLeftFace(facePreviewInfoList)
// }
//
// override fun onCameraClosed() {
// Timber.tag(TAG).i("initRgbCamera onCameraClosed: ")
// }
//
// override fun onCameraError(e: java.lang.Exception) {
// Timber.tag(TAG).i("initRgbCamera onCameraError: %s", e.message)
// e.printStackTrace()
// }
//
// override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) {
// Timber.tag(TAG)
// .i("initRgbCamera onCameraConfigurationChanged: threadName = ${Thread.currentThread().name}")
// if (rgbFaceRectTransformer != null) {
// rgbFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation
// }
// Timber.tag(TAG)
// .i("initRgbCamera onCameraConfigurationChanged: $cameraID $displayOrientation")
// }
// }
// val measuredWidth = binding.dualCameraTexturePreviewRgb.measuredWidth
// val measuredHeight = binding.dualCameraTexturePreviewRgb.measuredHeight
// Timber.tag(TAG)
// .i("initRgbCamera measuredWidth=$measuredWidthmeasuredHeight=$measuredHeight")
//
// val previewConfig: PreviewConfig = recognizeViewModel.previewConfig
// rgbCameraHelper = DualCameraHelper.Builder()
// .previewViewSize(Point(measuredWidth, measuredHeight))
// .rotation(activity.windowManager.defaultDisplay.rotation)
// .additionalRotation(previewConfig.rgbAdditionalDisplayOrientation) // 角度
// .previewSize(recognizeViewModel.loadPreviewSize())
// .specificCameraId(previewConfig.rgbCameraId)
// .isMirror(true)
// .previewOn(binding.dualCameraTexturePreviewRgb)
// .cameraListener(cameraListener)
// .build()
// rgbCameraHelper!!.setSurfaceFrameCallback { frame ->
// activity.facePayFragment?.loadBitmap(frame)
// }
// rgbCameraHelper!!.init()
//// rgbCameraHelper!!.start()
// }
//
// /**
// * 初始化红外相机,若活体检测类型是可见光活体检测或不启用活体,则不需要启用
// */
// private fun initIrCamera() {
// Timber.tag(TAG).d("initIrCamera: livenessType = $livenessType")
// if (livenessType === LivenessType.RGB) {
// return
// }
// val irCameraListener: CameraListener = object : CameraListener {
// override fun onCameraOpened(
// camera: Camera,
// cameraId: Int,
// displayOrientation: Int,
// isMirror: Boolean
// ) {
// Timber.tag(TAG)
// .d("initIrCamera IR onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation")
// val previewSizeIr = camera.getParameters().getPreviewSize()
// val layoutParams = adjustPreviewViewSize(
// binding.dualCameraTexturePreviewRgb,
// binding.dualCameraTexturePreviewIr, binding.dualCameraFaceRectViewIr,
// previewSizeIr, displayOrientation, 0.25f
// )
//
// irFaceRectTransformer = FaceRectTransformer(
// previewSizeIr.width, previewSizeIr.height,
//// layoutParams.width, layoutParams.height,
// layoutParams.width, layoutParams.height,
// displayOrientation, cameraId, isMirror,
// ConfigUtil.isDrawIrRectHorizontalMirror(context),
// ConfigUtil.isDrawIrRectVerticalMirror(context)
// )
//
// recognizeViewModel.onIrCameraOpened(camera)
// recognizeViewModel.setIrFaceRectTransformer(irFaceRectTransformer)
// }
//
//
// override fun onPreview(nv21: ByteArray?, camera: Camera?) {
// recognizeViewModel.refreshIrPreviewData(nv21)
// }
//
// override fun onCameraClosed() {
// Timber.tag(TAG).i("initIrCamera onCameraClosed: ")
// }
//
// override fun onCameraError(e: java.lang.Exception) {
// Timber.tag(TAG).i("initIrCamera onCameraError: ${e.message}")
// e.printStackTrace()
// }
//
// override fun onCameraConfigurationChanged(
// cameraID: Int,
// displayOrientation: Int
// ) {
// if (irFaceRectTransformer != null) {
// irFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation
// }
// Timber.tag(TAG)
// .i("initIrCamera onCameraConfigurationChanged: cameraID = $cameraID, displayOrientation = $displayOrientation")
// }
// }
//
// val previewConfig = recognizeViewModel.previewConfig
// irCameraHelper = DualCameraHelper.Builder()
// .previewViewSize(
// Point(
// binding.dualCameraTexturePreviewIr.measuredWidth,
// binding.dualCameraTexturePreviewIr.measuredHeight
// )
// )
// .rotation(activity.windowManager.defaultDisplay.rotation)
// .specificCameraId(previewConfig.irCameraId)
// .previewOn(binding.dualCameraTexturePreviewIr)
// .cameraListener(irCameraListener)
// .isMirror(true)
// .previewSize(recognizeViewModel.loadPreviewSize()) //相机预览大小设置,RGB与IR需使用相同大小
// .additionalRotation(previewConfig.irAdditionalDisplayOrientation) //额外旋转角度
// .build()
// irCameraHelper!!.init()
// try {
// irCameraHelper!!.start()
// } catch (e: RuntimeException) {
// ToastUtils.showToast(e.message + context.getString(R.string.camera_error_notice))
// }
// }
//
// /**
// * 绘制RGB、IR画面的实时人脸信息
// *
// * @param facePreviewInfoList RGB画面的实时人脸信息
// */
// private fun drawPreviewInfo(facePreviewInfoList: MutableList<FacePreviewInfo?>) {
//// Timber.tag(TAG).d("drawPreviewInfo facePreviewInfoList = ${facePreviewInfoList.size}, rgbFaceRectTransformer = ${rgbFaceRectTransformer != null}")
// if (rgbFaceRectTransformer != null) {
// val rgbDrawInfoList: MutableList<DrawInfo?>? = recognizeViewModel.getDrawInfo(
// facePreviewInfoList,
// LivenessType.RGB,
// openRectInfoDraw
// )
// // 识别成功
// binding.dualCameraFaceRectView.drawRealtimeFaceInfo(rgbDrawInfoList)
// }
//
// if (facePreviewInfoList.isEmpty() || (lastFaceTrackId != facePreviewInfoList[0]!!.trackId)) {
// if (lastFaceTrackId != -1) {
// // mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
// // Timber.tag(TAG).i("$lastFaceTrackId 用户离开")
// // lastFaceTrackId = -1
// // postUserData()
// // if (mealPickupMode == 0) {
// // step1FoodRecognizing()
// // } else {
// // step2FaceRecognizing(currentFood!!)
// // }
//
// resumeCamera()
// }
// }
// }
//
// private var lastFaceTrackId: Int = -1 // 上一次的人脸信息
//
// override fun onDisplayRemoved() {
// super.onDisplayRemoved()
// onDismissListener()
// }
//
//
// private fun faceRecSuccess(userId: String) {
// activity.runOnUiThread {
// activity.showWaitingDialog("加载中,请稍后……")
// }
// activity.getMemberInfoById(userId) { memberInfo ->
// if (memberInfo == null) {
// activity.hideWaitingDialog()
// //ToastUtils.showToast("查询会员信息失败,请稍后重试")
// return@getMemberInfoById
// }
// activity.bindOrder(memberInfo.faceUserId?:"") { bindResult ->
// if (bindResult.not()) {
// activity.hideWaitingDialog()
// //ToastUtils.showToast("订单绑定失败")
// return@bindOrder
// }
// binding.root.postDelayed({
// activity.hideWaitingDialog()
// activity.showPayInfo(type = 1, isVip = true, memberInfo = memberInfo)
// activity.hidePayTab()
// binding.root.postDelayed({
// dismiss()
// }, 500)
// }, 1000)
// }
// }
// }
//
//// private fun bindOrder(userId: String, block: () -> Unit) {
//// val pickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0)
//// val mode = if (pickupMode == 1) 1 else 2
//// userViewModel.bindOrder(userId, activity.foodOrderId, mode = mode) { bindResult ->
//// activity.runOnUiThread {
//// if (bindResult.not()) {
//// activity.hideWaitingDialog()
//// //ToastUtils.showToast("订单绑定失败")
//// return@runOnUiThread
//// }
//// block()
//// }
//// }
//// }
//// private fun getMemberInfo(userId: String, block:(MemberInfo)-> Unit) {
//// userViewModel.getMemberInfoById(memberId = userId) { memberInfo ->
//// activity.runOnUiThread {
//// if (memberInfo == null) {
//// activity.hideWaitingDialog()
//// //ToastUtils.showToast("查询会员信息失败,请稍后重试")
//// return@runOnUiThread
//// }
//// block(memberInfo)
//// }
//// }
//// }
//
//}
@@ -0,0 +1,826 @@
package com.sw.dualscreen.presentation.pay
import android.Manifest
import android.annotation.SuppressLint
import android.app.Presentation
import android.content.pm.PackageManager
import android.graphics.Point
import android.hardware.Camera
import android.os.Build
import android.os.Bundle
import android.text.Spanned
import android.text.SpannedString
import android.text.style.AbsoluteSizeSpan
import android.view.Display
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.FrameLayout
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.core.text.buildSpannedString
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import com.arcsoft.face.ErrorInfo
import com.sw.dualscreen.R
import com.sw.dualscreen.activity.PayActivity
import com.sw.dualscreen.databinding.PresentationPayBinding
import com.sw.dualscreen.ext.dp
import com.sw.dualscreen.ext.format2String
import com.sw.dualscreen.ext.gone
import com.sw.dualscreen.ext.invisible
import com.sw.dualscreen.ext.load
import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.model.response.MemberInfo
import com.sw.dualscreen.model.response.TextBean
import com.sw.dualscreen.utils.SpannedUtils
import com.sw.dualscreen.viewmodel.NetViewModelV2
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.ConfigUtil
import com.sw.plate.utils.arcface.ErrorCodeUtil
import com.sw.plate.utils.arcface.FaceRectTransformer
import com.sw.plate.utils.arcface.FaceRectView
import com.sw.plate.utils.arcface.FaceRectView.DrawInfo
import com.sw.plate.utils.arcface.PreviewConfig
import com.sw.plate.utils.arcface.camera.CameraListener
import com.sw.plate.utils.arcface.camera.DualCameraHelper
import com.sw.plate.utils.arcface.face.constants.LivenessType
import com.sw.plate.utils.arcface.face.model.CompareResult
import com.sw.plate.utils.arcface.face.model.FacePreviewInfo
import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
class PayPresentation(
val activity: PayActivity,
display: Display,
val userViewModel: NetViewModelV2,
val recognizeViewModel: RecognizeViewModel,
private val onDismissListener: () -> Unit = {}
) : Presentation(activity, display), ViewTreeObserver.OnGlobalLayoutListener {
companion object {
private const val TAG = "PayPresentation"
const val CASH_PAY = 100
const val FACE_PAY = 200
const val QR_CODE_PAY = 300
}
private lateinit var binding: PresentationPayBinding
// 虹软人脸配置 ⬇
private var isRecognition = false
private var rgbCameraHelper: DualCameraHelper? = null
private var rgbFaceRectTransformer: FaceRectTransformer? = null
private val livenessType = LivenessType.IR
private var openRectInfoDraw = false
private var irCameraHelper: DualCameraHelper? = null
private var irFaceRectTransformer: FaceRectTransformer? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = PresentationPayBinding.inflate(layoutInflater)
setContentView(binding.root)
window?.setBackgroundDrawableResource(android.R.color.white)
setupArcCamera()
registerDataChange()
initView()
}
override fun show() {
super.show()
pauseCamera()
}
var pageType: Int = 0
var type: Int = 0
// fun updatePage(pageType:Int, type:Int = -1) {
// this.pageType == pageType
// this.type = type
// if (pageType == PayPresentation.CASH_PAY) {
//
// return
// }
// if (pageType == PayPresentation.FACE_PAY) {
//
// return
// }
// if (pageType == PayPresentation.QR_CODE_PAY) {
//
// return
// }
// }
var foodName: String? = null
var payAmount: String? = null
fun initView() {
when (pageType) {
FACE_PAY -> {
//人脸识别
binding.flCashPay.gone()
binding.flQrCodePay.gone()
binding.flFacePay.visible()
binding.tvFoodName.text = foodName
binding.tvRealAmount.text = SpannedUtils.getAmountText(
listOf(
TextBean(text = "¥", textSize = 32),
TextBean(text = payAmount ?: "", textSize = 48),
)
)
//pauseCamera()
resumeCamera()
// binding.root.postDelayed({
// resumeCamera()
// }, 1000)
}
QR_CODE_PAY -> {
//二维码
binding.flCashPay.gone()
binding.flQrCodePay.visible()
binding.flFacePay.gone()
initQrCodeView()
}
CASH_PAY -> {
binding.flCashPay.visible()
binding.flQrCodePay.gone()
binding.flFacePay.gone()
binding.tvFoodName2.text = foodName
binding.tvRealAmount2.text = getAmountText(payAmount ?: "")
}
}
}
private fun getAmountText(amount: String): SpannedString {
return buildSpannedString {
append("¥", AbsoluteSizeSpan(32, true), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
append(amount, AbsoluteSizeSpan(48, true), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
override fun onGlobalLayout() {
Timber.tag(TAG).d("onGlobalLayout")
binding.dualCameraTexturePreviewRgb.getViewTreeObserver().removeOnGlobalLayoutListener(this)
//parentTextureView.getViewTreeObserver().removeOnGlobalLayoutListener(this)
openCamera()
}
private fun openCamera() {
Timber.tag(TAG).d("openCamera")
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) !=
PackageManager.PERMISSION_GRANTED
) {
ToastUtils.showToast("无摄像头权限")
return
}
try {
val cameraCount = Camera.getNumberOfCameras()
if (cameraCount < 3) {
ToastUtils.showToast("摄像头数量异常")
return
}
recognizeViewModel.init(
PreviewConfig(
2, 1, 90, 90
)
)
initRgbCamera()
if (DualCameraHelper.hasDualCamera() && livenessType === LivenessType.IR) {
initIrCamera()
}
} catch (e: Exception) {
Timber.tag(TAG).e(e, "打开摄像头失败")
}
}
/**
* 启用虹软人脸失败
*/
private fun setupArcCamera() {
initArcViewModel()
initArcView()
openRectInfoDraw = true
}
private fun registerDataChange() {
activity.lifecycleScope.launch {
userViewModel.loadFaceResult.collect { needUpdate ->
if (needUpdate) {
recognizeViewModel.refreshFaceList()
}
}
}
}
override fun onStop() {
rgbCameraHelper?.release()
rgbCameraHelper = null
irCameraHelper?.release()
irCameraHelper = null
recognizeViewModel.destroy()
super.onStop()
}
fun resumeCamera() {
Timber.tag(TAG).d("resumeCamera isRecognition = $isRecognition")
isRecognition = true
if (rgbCameraHelper?.isStopped == true) {
rgbCameraHelper?.start()
}
}
fun pauseCamera() {
Timber.tag(TAG).d("pauseCamera isRecognition = $isRecognition")
isRecognition = false
recognizeViewModel.onPreviewFrame(ByteArray(1382400), true)
}
private fun initArcViewModel() {
recognizeViewModel.setLiveType(livenessType)
recognizeViewModel.ftInitCode.observe(activity, Observer { ftInitCode: Int? ->
if (ftInitCode != ErrorInfo.MOK) {
val error: String? = context.getString(
R.string.specific_engine_init_failed, "ftEngine",
ftInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(ftInitCode!!)
)
Timber.tag(TAG).e("ftInitCode observe = $error")
ToastUtils.showToast(error)
}
})
recognizeViewModel.frInitCode.observe(activity, Observer { frInitCode: Int? ->
if (frInitCode != ErrorInfo.MOK) {
val error: String? = context.getString(
R.string.specific_engine_init_failed, "frEngine",
frInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(frInitCode!!)
)
Timber.tag(TAG).e("frInitCode observe = $error")
ToastUtils.showToast(error)
}
})
recognizeViewModel.flInitCode.observe(activity, Observer { flInitCode: Int? ->
if (flInitCode != ErrorInfo.MOK) {
val error: String? = context.getString(
R.string.specific_engine_init_failed, "flEngine",
flInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(flInitCode!!)
)
Timber.tag(TAG).e("flInitCode observe = $error")
ToastUtils.showToast(error)
}
})
recognizeViewModel.recognizeConfiguration
.observe(activity, Observer { recognizeConfiguration: RecognizeConfiguration? ->
Timber.tag(TAG)
.i("recognizeConfiguration: observe = ${recognizeConfiguration.toString()}")
})
recognizeViewModel.recognizeNotice.observe(activity, Observer { notice: String? ->
Timber.tag(TAG).i("recognizeNotice observe notice = $notice")
})
recognizeViewModel.recognizeUserId.observe(
activity,
Observer { compareResult: CompareResult ->
Timber.tag(TAG)
.i("recognizeUserId observe compareResult = ${compareResult.trackId}, userId = ${compareResult.faceEntity.userName}")
// recognitionTime = System.currentTimeMillis()
// recognitionWeight = lastWeight
if (isExistNotPayOrder.get()) return@Observer
lastFaceTrackId = compareResult.trackId
val faceEntity = compareResult.faceEntity
val userId = faceEntity.userName
val isMember = faceEntity.userType == "1"
activity.isMember = isMember
if (userId.isNullOrBlank()) return@Observer
// TODO: 测试支付用户id
// userId = "1987710988425662466"
isExistNotPayOrder.set(true)
faceRecSuccess(userId, isMember)
})
recognizeViewModel.drawRectInfoText.observe(activity, Observer { info ->
Timber.tag(TAG).i("drawRectInfoText observe info = $info")
})
}
private var isExistNotPayOrder = AtomicBoolean(false)
private fun initArcView() {
//在布局结束后才做初始化操作
binding.dualCameraTexturePreviewRgb.getViewTreeObserver().addOnGlobalLayoutListener(this)
//parentTextureView.getViewTreeObserver().addOnGlobalLayoutListener(this)
recognizeViewModel.getCompareResultList().getValue()
}
/**
* 调整View的宽高,使预览显示正常且采集框固定为
*
* @param rgbPreview RGB预览View
* @param previewView 显示预览数据的view
* @param faceRectView 画框的view
* @param previewSize 预览大小
* @param displayOrientation 相机旋转角度
* @param scale 缩放比例
* @return 调整后的LayoutParams
*/
private fun adjustPreviewViewSize(
rgbPreview: View,
previewView: View,
faceRectView: FaceRectView,
previewSize: Camera.Size,
displayOrientation: Int,
scale: Float
): ViewGroup.LayoutParams {
val w = (600.dp * 1.5).toInt()
val h = 1068.dp
val layoutParams = FrameLayout.LayoutParams(w, h)
previewView.setLayoutParams(layoutParams)
faceRectView.setLayoutParams(layoutParams)
return layoutParams
}
private fun initRgbCamera() {
val cameraListener: CameraListener = object : CameraListener {
override fun onCameraOpened(
camera: Camera,
cameraId: Int,
displayOrientation: Int,
isMirror: Boolean
) {
Timber.tag(TAG)
.d("initRgbCamera Rgb onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation")
activity.runOnUiThread({
val previewSizeRgb = camera.getParameters().getPreviewSize()
val layoutParams = adjustPreviewViewSize(
binding.dualCameraTexturePreviewRgb,
binding.dualCameraTexturePreviewRgb, binding.dualCameraFaceRectView,
previewSizeRgb, displayOrientation, 0.6F
)
Timber.tag(TAG)
.d("initRgbCamera previewSizeRgb = ${previewSizeRgb.width}-${previewSizeRgb.height}")
Timber.tag(TAG)
.d("initRgbCamera layoutParams = ${layoutParams.width}-${layoutParams.height}")
Timber.tag(TAG).d(
"initRgbCamera isMirror = ${isMirror}, isDrawRgbRectHorizontalMirror = ${
ConfigUtil.isDrawRgbRectHorizontalMirror(
context
)
}, isDrawRgbRectVerticalMirror = ${
ConfigUtil.isDrawRgbRectVerticalMirror(
context
)
}"
)
// 调整识别窗口位置
rgbFaceRectTransformer = FaceRectTransformer(
previewSizeRgb.width,
previewSizeRgb.height,
// layoutParams.width,
// layoutParams.height,
layoutParams.width,
layoutParams.height,
90,
cameraId,
isMirror,
true,
true
)
recognizeViewModel.onRgbCameraOpened(camera)
recognizeViewModel.setRgbFaceRectTransformer(rgbFaceRectTransformer)
})
}
@RequiresApi(api = Build.VERSION_CODES.Q)
override fun onPreview(nv21: ByteArray?, camera: Camera?) {
if (!isRecognition) {
return
}
binding.dualCameraFaceRectView.clearFaceInfo()
val facePreviewInfoList: MutableList<FacePreviewInfo?>? =
recognizeViewModel.onPreviewFrame(nv21, true)
if (facePreviewInfoList != null && rgbFaceRectTransformer != null) {
drawPreviewInfo(facePreviewInfoList)
}
recognizeViewModel.clearLeftFace(facePreviewInfoList)
}
override fun onCameraClosed() {
Timber.tag(TAG).i("initRgbCamera onCameraClosed: ")
}
override fun onCameraError(e: java.lang.Exception) {
Timber.tag(TAG).i("initRgbCamera onCameraError: %s", e.message)
e.printStackTrace()
}
override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) {
Timber.tag(TAG)
.i("initRgbCamera onCameraConfigurationChanged: threadName = ${Thread.currentThread().name}")
if (rgbFaceRectTransformer != null) {
rgbFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation
}
Timber.tag(TAG)
.i("initRgbCamera onCameraConfigurationChanged: $cameraID $displayOrientation")
}
}
val measuredWidth = binding.dualCameraTexturePreviewRgb.measuredWidth
val measuredHeight = binding.dualCameraTexturePreviewRgb.measuredHeight
Timber.tag(TAG)
.i("initRgbCamera measuredWidth=$measuredWidthmeasuredHeight=$measuredHeight")
val previewConfig: PreviewConfig = recognizeViewModel.previewConfig
rgbCameraHelper = DualCameraHelper.Builder()
.previewViewSize(Point(measuredWidth, measuredHeight))
.rotation(activity.windowManager.defaultDisplay.rotation)
.additionalRotation(previewConfig.rgbAdditionalDisplayOrientation) // 角度
.previewSize(recognizeViewModel.loadPreviewSize())
.specificCameraId(previewConfig.rgbCameraId)
.isMirror(true)
.previewOn(binding.dualCameraTexturePreviewRgb)
.cameraListener(cameraListener)
.build()
rgbCameraHelper!!.setSurfaceFrameCallback { frame ->
activity.facePayFragment?.loadBitmap(frame)
}
rgbCameraHelper!!.init()
// rgbCameraHelper!!.start()
}
/**
* 初始化红外相机,若活体检测类型是可见光活体检测或不启用活体,则不需要启用
*/
private fun initIrCamera() {
Timber.tag(TAG).d("initIrCamera: livenessType = $livenessType")
if (livenessType === LivenessType.RGB) {
return
}
val irCameraListener: CameraListener = object : CameraListener {
override fun onCameraOpened(
camera: Camera,
cameraId: Int,
displayOrientation: Int,
isMirror: Boolean
) {
Timber.tag(TAG)
.d("initIrCamera IR onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation")
val previewSizeIr = camera.getParameters().getPreviewSize()
val layoutParams = adjustPreviewViewSize(
binding.dualCameraTexturePreviewRgb,
binding.dualCameraTexturePreviewIr, binding.dualCameraFaceRectViewIr,
previewSizeIr, displayOrientation, 0.25f
)
irFaceRectTransformer = FaceRectTransformer(
previewSizeIr.width, previewSizeIr.height,
// layoutParams.width, layoutParams.height,
layoutParams.width, layoutParams.height,
displayOrientation, cameraId, isMirror,
ConfigUtil.isDrawIrRectHorizontalMirror(context),
ConfigUtil.isDrawIrRectVerticalMirror(context)
)
recognizeViewModel.onIrCameraOpened(camera)
recognizeViewModel.setIrFaceRectTransformer(irFaceRectTransformer)
}
override fun onPreview(nv21: ByteArray?, camera: Camera?) {
recognizeViewModel.refreshIrPreviewData(nv21)
}
override fun onCameraClosed() {
Timber.tag(TAG).i("initIrCamera onCameraClosed: ")
}
override fun onCameraError(e: java.lang.Exception) {
Timber.tag(TAG).i("initIrCamera onCameraError: ${e.message}")
e.printStackTrace()
}
override fun onCameraConfigurationChanged(
cameraID: Int,
displayOrientation: Int
) {
if (irFaceRectTransformer != null) {
irFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation
}
Timber.tag(TAG)
.i("initIrCamera onCameraConfigurationChanged: cameraID = $cameraID, displayOrientation = $displayOrientation")
}
}
val previewConfig = recognizeViewModel.previewConfig
irCameraHelper = DualCameraHelper.Builder()
.previewViewSize(
Point(
binding.dualCameraTexturePreviewIr.measuredWidth,
binding.dualCameraTexturePreviewIr.measuredHeight
)
)
.rotation(activity.windowManager.defaultDisplay.rotation)
.specificCameraId(previewConfig.irCameraId)
.previewOn(binding.dualCameraTexturePreviewIr)
.cameraListener(irCameraListener)
.isMirror(true)
.previewSize(recognizeViewModel.loadPreviewSize()) //相机预览大小设置,RGB与IR需使用相同大小
.additionalRotation(previewConfig.irAdditionalDisplayOrientation) //额外旋转角度
.build()
irCameraHelper!!.init()
try {
irCameraHelper!!.start()
} catch (e: RuntimeException) {
ToastUtils.showToast(e.message + context.getString(R.string.camera_error_notice))
}
}
/**
* 绘制RGB、IR画面的实时人脸信息
*
* @param facePreviewInfoList RGB画面的实时人脸信息
*/
private fun drawPreviewInfo(facePreviewInfoList: MutableList<FacePreviewInfo?>) {
// Timber.tag(TAG).d("drawPreviewInfo facePreviewInfoList = ${facePreviewInfoList.size}, rgbFaceRectTransformer = ${rgbFaceRectTransformer != null}")
if (rgbFaceRectTransformer != null) {
val rgbDrawInfoList: MutableList<DrawInfo?>? = recognizeViewModel.getDrawInfo(
facePreviewInfoList,
LivenessType.RGB,
openRectInfoDraw
)
// 识别成功
binding.dualCameraFaceRectView.drawRealtimeFaceInfo(rgbDrawInfoList)
}
if (facePreviewInfoList.isEmpty() || (lastFaceTrackId != facePreviewInfoList[0]!!.trackId)) {
if (lastFaceTrackId != -1) {
// mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
// Timber.tag(TAG).i("$lastFaceTrackId 用户离开")
// lastFaceTrackId = -1
// postUserData()
// if (mealPickupMode == 0) {
// step1FoodRecognizing()
// } else {
// step2FaceRecognizing(currentFood!!)
// }
resumeCamera()
}
}
}
private var lastFaceTrackId: Int = -1 // 上一次的人脸信息
override fun onDisplayRemoved() {
super.onDisplayRemoved()
onDismissListener()
}
private fun faceRecSuccess(userId: String, isMember: Boolean) {
activity.runOnUiThread {
activity.showWaitingDialog("加载中,请稍后……")
}
activity.getMemberInfoById(userId) { memberInfo ->
if (memberInfo == null) {
activity.hideWaitingDialog()
//ToastUtils.showToast("查询会员信息失败,请稍后重试")
return@getMemberInfoById
}
memberInfo.member = isMember
activity.bindOrder(memberInfo.faceUserId ?: "") { bindResult ->
if (bindResult.not()) {
activity.hideWaitingDialog()
//ToastUtils.showToast("订单绑定失败")
return@bindOrder
}
binding.root.postDelayed({
activity.hideWaitingDialog()
// TODO: 测试 memberInfo.member是否返回正常数据---------------
activity.showPayInfo(type = 1, isVip = memberInfo.member, memberInfo = memberInfo)
activity.hidePayTab()
// binding.root.postDelayed({
// dismiss()
// }, 500)
}, 1000)
}
}
}
// private fun bindOrder(userId: String, block: () -> Unit) {
// val pickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0)
// val mode = if (pickupMode == 1) 1 else 2
// userViewModel.bindOrder(userId, activity.foodOrderId, mode = mode) { bindResult ->
// activity.runOnUiThread {
// if (bindResult.not()) {
// activity.hideWaitingDialog()
// //ToastUtils.showToast("订单绑定失败")
// return@runOnUiThread
// }
// block()
// }
// }
// }
// private fun getMemberInfo(userId: String, block:(MemberInfo)-> Unit) {
// userViewModel.getMemberInfoById(memberId = userId) { memberInfo ->
// activity.runOnUiThread {
// if (memberInfo == null) {
// activity.hideWaitingDialog()
// //ToastUtils.showToast("查询会员信息失败,请稍后重试")
// return@runOnUiThread
// }
// block(memberInfo)
// }
// }
// }
var payQrCodePic: Any? = null
//总价格
var totalPrice = 0.0
//余额
private var balance = 0.0
//实际支付金额
private var realPayPrice = 0.0
//扣除余额
private var expensesBalance = 0.0
@SuppressLint("SetTextI18n")
private fun initQrCodeView() {
when (type) {
0 -> {
binding.layoutVip.gone()
binding.layoutPaySuccess.gone()
binding.layoutScanQrCode.visible()
binding.tvPayTip.text = "请扫码支付或出示付款码"
binding.tvFoodName3.text = foodName
binding.tvRealAmount3.text = SpannedUtils.getAmountText(
listOf(
TextBean(text = "¥", textSize = 32),
TextBean(text = totalPrice.format2String(2), textSize = 48),
)
)
binding.ivPayQrCode.load(payQrCodePic)
}
1 -> {
val memberInfo = activity.memberInfo
binding.layoutVip.run {
if (memberInfo != null && memberInfo.member) visible() else gone()
}
binding.layoutPaySuccess.gone()
binding.layoutScanQrCode.visible()
balance = (memberInfo?.topUpBalance ?: 0.0) + (memberInfo?.rewardBalance ?: 0.0)
realPayPrice = if (balance >= totalPrice) 0.0 else totalPrice - balance
expensesBalance = if (balance >= totalPrice) totalPrice else balance
memberInfo?.let { loadUserInfo(it) }
binding.tvUserBalance.text = SpannedUtils.getAmountText(
listOf(
TextBean(text = "¥", textSize = 20),
TextBean(text = balance.format2String(2), textSize = 32),
)
)
binding.tvFoodName3.text = foodName
binding.tvRealAmount3.text = SpannedUtils.getAmountText(
listOf(
TextBean(text = "¥", textSize = 32),
TextBean(text = totalPrice.format2String(2), textSize = 48),
)
)
binding.ivPayQrCode.run {
// load(payQrCodePic)
if (balance >= totalPrice) invisible() else visible()
}
if (memberInfo !=null && memberInfo!!.member) {
binding.tvPayTip.text = SpannedUtils.getAmountText(getAmountList())
} else {
binding.tvPayTip.text = SpannedUtils.getAmountText(getAmountListNotMember())
}
}
2 -> {
val memberInfo = activity.memberInfo
binding.layoutVip.run {
if (memberInfo != null && memberInfo.member) visible() else gone()
}
binding.layoutPaySuccess.visible()
binding.layoutScanQrCode.gone()
balance = if (memberInfo != null && memberInfo.member) (memberInfo.topUpBalance
?: 0.0) + (memberInfo.rewardBalance ?: 0.0) else 0.0
realPayPrice = if (balance >= totalPrice) 0.0 else totalPrice - balance
expensesBalance = if (balance >= totalPrice) totalPrice else balance
memberInfo?.let { loadUserInfo(it) }
val remainBalance = if (balance >= totalPrice) balance - totalPrice else 0.0
binding.tvUserBalance.text = SpannedUtils.getAmountText(
listOf(
TextBean(text = "¥", textSize = 24),
TextBean(text = remainBalance.format2String(2), textSize = 36),
)
)
val showBalance = expensesBalance.format2String(2)
val showTotal = totalPrice.format2String(2)
binding.tvPayInfo.text = "余额扣除 $showBalance 元,在线支付 $showTotal"
}
}
}
fun loadQrCodeImage(qrCodeUrl: String?) {
binding.ivPayQrCode.load(qrCodeUrl)
}
private fun loadUserInfo(item: MemberInfo) {
binding.ivHeadPic.load(
if (item.faceUrl.isNullOrBlank()) R.drawable.ic_avatar_default
else item.faceUrl
)
binding.tvUserName.text = item.name
val phone = item.phone ?: ""
binding.tvUserPhone.text =
if (phone.length == 11) phone.replace(phone.substring(3, 7), "****")
else phone
}
private fun getAmountListNotMember(): List<TextBean> {
val list: MutableList<TextBean> = mutableListOf()
list.add(TextBean(text = "应付金额 ", textSize = 30, textColor = "#FF889AC2"))
list.add(
TextBean(
text = "-${totalPrice.format2String(2)}",
textSize = 30,
textColor = "#FF0A1428",
isBold = true
)
)
list.add(TextBean(text = "", textSize = 30, textColor = "#FF889AC2"))
return list
}
private fun getAmountList(): List<TextBean> {
val list: MutableList<TextBean> = mutableListOf()
list.add(TextBean(text = "余额扣除 ", textSize = 30, textColor = "#FF5E7585"))
list.add(
TextBean(
text = "-${expensesBalance.format2String(2)}",
textSize = 30,
textColor = "#FF0A1428",
isBold = true
)
)
if (balance >= totalPrice) {
//余额大于等于总价格,使用余额支付
list.add(TextBean(text = " 元,扣除后可用余额 ", textSize = 30, textColor = "#FF5E7585"))
val remainingBalance = balance - totalPrice
list.add(
TextBean(
text = remainingBalance.format2String(2),
textSize = 30,
textColor = "#FF0A1428",
isBold = true
)
)
} else {
//余额小于总价格,使用余额+扫码支付
//实际支付金额
list.add(TextBean(text = " 元,还需支付 ", textSize = 30, textColor = "#FF5E7585"))
list.add(
TextBean(
text = realPayPrice.format2String(2),
textSize = 30,
textColor = "#FF0A1428",
isBold = true
)
)
}
list.add(TextBean(text = "", textSize = 30, textColor = "#FF5E7585"))
return list
}
}
@@ -0,0 +1,202 @@
//package com.sw.dualscreen.presentation.pay
//
//import android.annotation.SuppressLint
//import android.app.Presentation
//import android.os.Bundle
//import android.view.Display
//import com.sw.dualscreen.R
//import com.sw.dualscreen.activity.PayActivity
//import com.sw.dualscreen.databinding.PresentationScanQrcodePayBinding
//import com.sw.dualscreen.ext.format2String
//import com.sw.dualscreen.ext.gone
//import com.sw.dualscreen.ext.invisible
//import com.sw.dualscreen.ext.load
//import com.sw.dualscreen.ext.visible
//import com.sw.dualscreen.model.response.MemberInfo
//import com.sw.dualscreen.model.response.TextBean
//import com.sw.dualscreen.utils.SpannedUtils
//
//class ScanQrCodePayPresentation(
// val activity: PayActivity,
// display: Display,
// val type: Int,
// private val onDismissListener: () -> Unit = {}
//) : Presentation(activity, display) {
//
// //type = 0,扫码支付默认显示二维码
// //type = 1,会员结算显示二维码,是会员则显示名字、头像、手机号、可用余额
// //type = 2,支付成功,是会员则显示名字、头像、手机号、可用余额
//
// private lateinit var binding: PresentationScanQrcodePayBinding
//
// override fun onCreate(savedInstanceState: Bundle?) {
// super.onCreate(savedInstanceState)
// binding = PresentationScanQrcodePayBinding.inflate(layoutInflater)
// setContentView(binding.root)
// window?.setBackgroundDrawableResource(android.R.color.transparent)
// initView()
// }
//
// var foodName: String? = null
// var payQrCodePic: Any? = null
//
// //总价格
// var totalPrice = 0.0
//
// //余额
// private var balance = 0.0
//
// //实际支付金额
// private var realPayPrice = 0.0
//
// //扣除余额
// private var expensesBalance = 0.0
//
// @SuppressLint("SetTextI18n")
// private fun initView() {
// when (type) {
// 0 -> {
// binding.layoutVip.gone()
// binding.layoutPaySuccess.gone()
// binding.layoutScanQrCode.visible()
//
// binding.tvPayTip.text = "请扫码支付或出示付款码"
// binding.tvFoodName.text = foodName
// binding.tvRealAmount.text = SpannedUtils.getAmountText(
// listOf(
// TextBean(text = "¥", textSize = 32),
// TextBean(text = totalPrice.format2String(2), textSize = 48),
// )
// )
// binding.ivPayQrCode.load(payQrCodePic)
// }
//
// 1 -> {
// val memberInfo = activity.memberInfo
// binding.layoutVip.run {
// if (memberInfo != null) visible() else gone()
// }
// binding.layoutPaySuccess.gone()
// binding.layoutScanQrCode.visible()
//
// balance = (memberInfo?.topUpBalance ?: 0.0) + (memberInfo?.rewardBalance ?: 0.0)
// realPayPrice = if (balance >= totalPrice) 0.0 else totalPrice - balance
// expensesBalance = if (balance >= totalPrice) totalPrice else balance
//
// memberInfo?.let { loadUserInfo(it) }
// binding.tvUserBalance.text = SpannedUtils.getAmountText(
// listOf(
// TextBean(text = "¥", textSize = 20),
// TextBean(text = balance.format2String(2), textSize = 32),
// )
// )
//
// binding.tvFoodName.text = foodName
// binding.tvRealAmount.text = SpannedUtils.getAmountText(
// listOf(
// TextBean(text = "¥", textSize = 32),
// TextBean(text = totalPrice.format2String(2), textSize = 48),
// )
// )
// binding.ivPayQrCode.run {
//// load(payQrCodePic)
// if (balance >= totalPrice) invisible() else visible()
// }
//
// binding.tvPayTip.text = SpannedUtils.getAmountText(getAmountList())
// }
//
// 2 -> {
// val memberInfo = activity.memberInfo
// binding.layoutVip.run {
// if (memberInfo != null) visible() else gone()
// }
// binding.layoutPaySuccess.visible()
// binding.layoutScanQrCode.gone()
//
// balance = if (memberInfo != null) (memberInfo.topUpBalance
// ?: 0.0) + (memberInfo.rewardBalance ?: 0.0) else 0.0
// realPayPrice = if (balance >= totalPrice) 0.0 else totalPrice - balance
// expensesBalance = if (balance >= totalPrice) totalPrice else balance
//
// memberInfo?.let { loadUserInfo(it) }
// val remainBalance = if (balance >= totalPrice) balance - totalPrice else 0.0
// binding.tvUserBalance.text = SpannedUtils.getAmountText(
// listOf(
// TextBean(text = "¥", textSize = 24),
// TextBean(text = remainBalance.format2String(2), textSize = 36),
// )
// )
//
// val showBalance = expensesBalance.format2String(2)
// val showTotal = totalPrice.format2String(2)
// binding.tvPayInfo.text = "余额扣除 $showBalance 元,在线支付 $showTotal 元"
// }
// }
// }
//
// fun loadQrCodeImage(qrCodeUrl: String?) {
// binding.ivPayQrCode.load(qrCodeUrl)
// }
//
// private fun loadUserInfo(item: MemberInfo) {
// binding.ivHeadPic.load(
// if(item.faceUrl.isNullOrBlank()) R.drawable.ic_avatar_default
// else item.faceUrl
// )
// binding.tvUserName.text = item.name
//
// val phone = item.phone ?: ""
// binding.tvUserPhone.text =
// if (phone.length == 11) phone.replace(phone.substring(3, 7), "****")
// else phone
// }
//
// private fun getAmountList(): List<TextBean> {
// val list: MutableList<TextBean> = mutableListOf()
//
// list.add(TextBean(text = "余额扣除 ", textSize = 30, textColor = "#FF5E7585"))
// list.add(
// TextBean(
// text = "-${expensesBalance.format2String(2)}",
// textSize = 30,
// textColor = "#FF0A1428",
// isBold = true
// )
// )
//
// if (balance >= totalPrice) {
// //余额大于等于总价格,使用余额支付
// list.add(TextBean(text = " 元,扣除后可用余额 ", textSize = 30, textColor = "#FF5E7585"))
// val remainingBalance = balance - totalPrice
// list.add(
// TextBean(
// text = remainingBalance.format2String(2),
// textSize = 30,
// textColor = "#FF0A1428",
// isBold = true
// )
// )
// } else {
// //余额小于总价格,使用余额+扫码支付
// //实际支付金额
// list.add(TextBean(text = " 元,还需支付 ", textSize = 30, textColor = "#FF5E7585"))
// list.add(
// TextBean(
// text = realPayPrice.format2String(2),
// textSize = 30,
// textColor = "#FF0A1428",
// isBold = true
// )
// )
// }
// list.add(TextBean(text = " 元", textSize = 30, textColor = "#FF5E7585"))
//
// return list
// }
//
// override fun onDisplayRemoved() {
// super.onDisplayRemoved()
// onDismissListener()
// }
//}
@@ -0,0 +1,12 @@
package com.sw.dualscreen.presentation.pay
import android.view.TextureView
import androidx.camera.lifecycle.ProcessCameraProvider
import java.util.concurrent.ExecutorService
class TransmitScreen {
}
@@ -2,6 +2,8 @@ package com.sw.dualscreen.repository
import com.google.gson.JsonParseException
import com.sw.dualscreen.model.response.ApiResponse
import com.sw.dualscreen.model.response.RespCodeMsg
import com.sw.dualscreen.utils.GsonUtils
import retrofit2.HttpException
import timber.log.Timber
import java.io.IOException
@@ -18,31 +20,37 @@ abstract class BaseRepository {
when (e) {
is HttpException -> {
ApiResponse(code = e.code(), message = e.message())
val respData = e.response()?.errorBody()?.string()
val respCodeMsg = GsonUtils.fromJson(respData, RespCodeMsg::class.java)
if (respCodeMsg?.msg.isNullOrBlank()) {
ApiResponse(code = "${e.code()}", msg = e.message())
} else {
ApiResponse(code = respCodeMsg.code ?:"-10", msg = respCodeMsg.msg)
}
}
is SocketTimeoutException -> {
ApiResponse(code = -2, message = "请求超时: ${e.message}")
ApiResponse(code = "-2", msg = "请求超时: ${e.message}")
}
is ConnectException -> {
ApiResponse(code = -3, message = "连接失败: ${e.message}")
ApiResponse(code = "-3", msg = "连接失败: ${e.message}")
}
is SSLHandshakeException -> {
ApiResponse(code = -4, message = "SSL握手失败: ${e.message}")
ApiResponse(code = "-4", msg = "SSL握手失败: ${e.message}")
}
is JsonParseException -> {
ApiResponse(code = -5, message = "JSON解析错误: ${e.message}")
ApiResponse(code = "-5", msg = "JSON解析错误: ${e.message}")
}
is IOException -> {
ApiResponse(code = -6, message = "网络IO错误: ${e.message}")
ApiResponse(code = "-6", msg = "网络IO错误: ${e.message}")
}
else -> {
ApiResponse(code = -1, message = "未知错误: ${e.message ?: "无错误信息"}")
ApiResponse(code = "-1", msg = "未知错误: ${e.message ?: "无错误信息"}")
}
}
}
@@ -1,19 +1,27 @@
package com.sw.dualscreen.repository
import android.content.Context
import android.net.Uri
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.model.request.UserNutritionParam
import com.sw.dualscreen.model.response.ApiResponse
import com.sw.dualscreen.model.response.DinnerTypeInfo
import com.sw.dualscreen.model.response.EquipmentInfo
import com.sw.dualscreen.model.response.DeviceConfig
import com.sw.dualscreen.model.response.DinnerType
import com.sw.dualscreen.model.response.FaceData
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.UserFaceInfo
import com.sw.dualscreen.model.response.UserNutritionData
import com.sw.dualscreen.model.response.FoodSearchReq
import com.sw.dualscreen.model.response.FoodOrder
import com.sw.dualscreen.model.response.FoodOrderModel
import com.sw.dualscreen.model.response.FoodVector
import com.sw.dualscreen.model.response.MemberInfo
import com.sw.dualscreen.model.response.UserFaceModel
import com.sw.dualscreen.model.response.UserNutrition
import com.sw.dualscreen.network.api.ApiService
import com.sw.dualscreen.utils.ImageUtil
import com.sw.dualscreen.objbox.CollectedFoodInfo
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
import kotlin.Int
/**
* 远程数据处理
@@ -24,43 +32,99 @@ class RemoteRepository constructor(
/**
* 生成token
*/
suspend fun getDeviceToken(qrcodeId: String): ApiResponse<String> {
return safeApiCall {
apiService.getDeviceToken(
qrcodeId
)
}
}
// suspend fun getDeviceToken(qrcodeId: String): ApiResponse<String> {
// return safeApiCall {
// apiService.getDeviceToken(
// qrcodeId
// )
// }
// }
/**
* 获取设备信息
*/
suspend fun getDeviceInfo(equipmentCode: String, token: String): ApiResponse<EquipmentInfo> {
return safeApiCall {
apiService.getDeviceInfo(
equipmentCode,
token = token
)
}
}
// suspend fun getDeviceInfo(equipmentCode: String, token: String): ApiResponse<EquipmentInfo> {
// return safeApiCall {
// apiService.getDeviceInfo(
// equipmentCode,
// token = token
// )
// }
// }
/**
* 获取业务服务器token
*/
suspend fun getEquipmentToken(qrcodeId: String): ApiResponse<String> {
// suspend fun getEquipmentToken(qrcodeId: String): ApiResponse<String> {
// return safeApiCall {
// apiService.getEquipmentToken(qrcodeId = qrcodeId)
// }
// }
/**
* 获取人脸数据
*/
suspend fun getUserFaceCache(
pageNum: Int,
pageSize: Int = 100,
): ApiResponse<List<UserFaceModel>?> {
return safeApiCall {
apiService.getEquipmentToken(qrcodeId = qrcodeId)
apiService.getUserFaceCache(
param = mapOf(
"pageNum" to pageNum,
"pageSize" to pageSize
)
)
}
}
/**
* 获取人脸数据
*/
suspend fun getUserFaceCache(
pageIndex: Int
): ApiResponse<UserFaceInfo> {
suspend fun getFaceIncrementList(
pageNum: Long,
pageSize: Long = 100L,
timestamp: Long
): ApiResponse<List<UserFaceModel>?> {
return safeApiCall {
apiService.getUserFaceCache(pageIndex = pageIndex)
apiService.getFaceIncrementList(
param = mapOf(
"pageNum" to pageNum,
"pageSize" to pageSize,
"timestamp" to timestamp
)
)
}
}
suspend fun getUserFaceCache2(
pageNum: Int,
pageSize: Int = 160,
): ApiResponse<FaceData> {
return safeApiCall {
apiService.getUserFaceCache2(
pageNum = pageNum,
pageSize = pageSize
)
}
}
/**
* 获取人脸数据
*/
suspend fun getCollectedFoodList(
pageNum: Int,
pageSize: Int = 100,
foodName: String,
): ApiResponse<List<CollectedFoodInfo>> {
return safeApiCall {
apiService.getCollectedFoodList(
param = mapOf(
"pageNum" to "$pageNum",
"pageSize" to "$pageSize",
"foodName" to foodName,
)
)
}
}
@@ -70,33 +134,87 @@ class RemoteRepository constructor(
* @param type 0全部 1餐次
*/
suspend fun getRestInfoFoodsByType(
restId: String = GlobalData.restId,
type: Int = 0,
foodName: String,
pageNum: Int = 1,
pageSize: Int = 50
): ApiResponse<List<FoodInfo>> {
return safeApiCall {
apiService.getRestInfoFoodsByType(restId = restId, type = type, foodName = foodName)
apiService.getRestInfoFoodsByType(
param = hashMapOf(
"name" to foodName,
"pageNum" to "$pageNum",
"pageSize" to "$pageSize",
//"订单来源权举:1-营养秤,2-档口
"deviceType" to "2"
)
)
}
}
// /**
// * 通过用户信息获取就餐数据
// */
// suspend fun getUserNutritionData(
// restId: String = GlobalData.restId,
// userId: String,
// foodId: String,
// ): ApiResponse<UserNutritionData> {
// return safeApiCall {
// apiService.getUserNutritionData(restId = restId, userId = userId, foodId = foodId)
// }
// }
/**
* 通过用户信息获取就餐数据
*/
suspend fun getUserNutritionData(
restId: String = GlobalData.restId,
userId: String,
foodId: String,
): ApiResponse<UserNutritionData> {
suspend fun getUserNutritionData(userId: String): ApiResponse<UserNutrition> {
return safeApiCall {
apiService.getUserNutritionData(restId = restId, userId = userId, foodId = foodId)
apiService.getUserNutritionData(userId = userId)
}
}
/**
* 获取支付二维码
*/
suspend fun getQrCodeImg(
orderNo: String,
memberId: String? = null,
totalFee: String? = null
): ApiResponse<String?> {
return safeApiCall {
apiService.getQrCodeImg(
orderNo = orderNo,
memberId = memberId,
totalFee = totalFee
)
}
}
/**
* 二维码支付
*/
suspend fun qrCodePay(
authCode: String,
orderNo: String,
memberId: String?
): ApiResponse<String?> {
return safeApiCall {
apiService.qrCodePay(
param = hashMapOf(
"authCode" to authCode,
"outTradeNo" to orderNo,
"memberId" to memberId,
"orderType" to "0"
)
)
}
}
suspend fun getDinnerType(
restId: String = GlobalData.restId,
): ApiResponse<DinnerTypeInfo> {
// restId: String = GlobalData.restId,
): ApiResponse<DinnerType> {
return safeApiCall {
apiService.getDinnerType(restId = restId)
apiService.getDinnerType()
}
}
@@ -114,33 +232,137 @@ class RemoteRepository constructor(
* @param restId 从device服务获取的canteenId字段
* @param foodName 菜品名称,多个使用逗号拼接
*/
suspend fun getFoodInfo(
restId: String = GlobalData.restId,
foodName: String,
): ApiResponse<List<FoodInfo>> {
return safeApiCall { apiService.getFoodInfo(restId = restId, foodName = foodName) }
suspend fun getFoodInfo(foodName: String): ApiResponse<List<FoodInfo>> {
return safeApiCall {
// val map = mutableMapOf<String, List<String>>()
// map["nameList"] = foodName.split(",")
apiService.getFoodInfo(req = FoodSearchReq(nameList = foodName.split(",")))
}
}
/**
* 获取菜品信息
* @param restId 从device服务获取的canteenId字段
* @param foodName 菜品名称,多个使用逗号拼接
* 创建订单
*/
suspend fun postImageData(
context: Context,
restId: String = GlobalData.restId,
foodId: String,
foodName: String,
foodVector: String,
uri: Uri,
): ApiResponse<String> {
suspend fun createOrder(order: FoodOrder): ApiResponse<Any?> {
return safeApiCall {
apiService.createOrder(order = order)
}
}
val params = HashMap<String, RequestBody>()
params["placeId"] = restId.toRequestBody()
params["foodId"] = foodId.toRequestBody()
params["foodName"] = foodName.toRequestBody()
params["foodVector"] = foodVector.toRequestBody()
val imagePart = ImageUtil.uriToMultipart(context, uri, "foodPic")
/**
* 现金支付
*/
suspend fun cashPay(param: HashMap<String, String>): ApiResponse<Boolean?> {
return safeApiCall {
apiService.cashPay(param = param)
}
}
return safeApiCall { apiService.postImageData(params = params, image = imagePart) }
/**
* 查询订单状态
* @param orderNo 订单号
*/
suspend fun queryOrderState(orderNo: String): ApiResponse<Any?> {
return safeApiCall {
apiService.queryOrderState(orderNo = orderNo)
}
}
/**
* 会员支付
*/
suspend fun memberPay(param: HashMap<String, String?>): ApiResponse<Any?> {
return safeApiCall {
apiService.memberPay(param = param)
}
}
/**
* 绑定订单
*/
suspend fun bindOrder(userId: String, orderId: String, mode: Int): ApiResponse<Any?> {
return safeApiCall {
apiService.bindOrder(userId = userId, orderId = orderId, mode = mode)
}
}
/**
* 根据id查询会员信息
*/
suspend fun getMemberInfoById(memberId: String): ApiResponse<MemberInfo?> {
return safeApiCall {
apiService.getMemberInfoById(memberId = memberId)
}
}
/**
* 根据id查询会员信息
*/
suspend fun getMemberInfoByPhone(phone: String, key: String): ApiResponse<MemberInfo?> {
return safeApiCall {
apiService.getMemberInfoByPhone(
param = hashMapOf(
"phone" to phone,
"password" to key
)
)
}
}
/**
* 上传采集菜品信息
*/
suspend fun uploadCollectFoodPics(
fileList: List<File>,
params: HashMap<String, RequestBody>
): ApiResponse<List<String>?> {
// 准备文件参数
val fileParts = mutableListOf<MultipartBody.Part>()
fileList.forEachIndexed { index, file ->
val requestFile = file
.asRequestBody("multipart/form-data".toMediaTypeOrNull())
val filePart = MultipartBody.Part.createFormData(
"foodPics",
file.name,
requestFile
)
fileParts.add(filePart)
}
return safeApiCall {
apiService.uploadCollectFoodPics(params = params, foodPics = fileParts)
}
}
suspend fun getDeviceConfig(): ApiResponse<DeviceConfig?> {
return safeApiCall { apiService.getDeviceConfig() }
}
suspend fun getCollectedFoodVector(param: MutableMap<String, String>): ApiResponse<List<FoodVector>?> {
return safeApiCall {
apiService.getCollectedFoodVector(param = param)
}
}
suspend fun deleteCollectFood(foodId: String?, version: String?): ApiResponse<Any?> {
return safeApiCall {
apiService.deleteCollectFood(
param = mutableMapOf(
"foodId" to foodId,
"version" to version
)
)
}
}
suspend fun getFoodOrderList(userId: String): ApiResponse<FoodOrderModel?> {
return safeApiCall {
apiService.getFoodOrderList(userId = userId)
}
}
suspend fun getMemberDiscount(userId: String): ApiResponse<Double?> {
return safeApiCall {
apiService.getMemberDiscount(userId = userId)
}
}
}
@@ -0,0 +1,237 @@
package com.sw.dualscreen.repository.v2
import com.sw.dualscreen.model.request.v2.BindUserOrderRequest
import com.sw.dualscreen.model.request.v2.PlaceOrderRequest
import com.sw.dualscreen.model.response.ApiResponse
import com.sw.dualscreen.model.response.DeviceConfig
import com.sw.dualscreen.model.response.FoodSearchReq
import com.sw.dualscreen.model.response.UserNutrition
import com.sw.dualscreen.model.response.v2.CollectedFoodV2
import com.sw.dualscreen.model.response.v2.FaceVO
import com.sw.dualscreen.model.response.v2.NewFoodInfo
import com.sw.dualscreen.model.response.v2.NewMemberInfo
import com.sw.dualscreen.model.response.v2.SettlementOrder
import com.sw.dualscreen.network.api.ApiServiceV2
import com.sw.dualscreen.repository.BaseRepository
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
class RemoteRepositoryV2 constructor(
private val apiService: ApiServiceV2
) : BaseRepository() {
suspend fun getDeviceConfig(): ApiResponse<DeviceConfig?> {
return safeApiCall {
apiService.getDeviceConfig()
}
}
suspend fun getFacePage(
pageNum: Long = 1L,
pageSize: Long = 100L
): ApiResponse<List<FaceVO>?> {
return safeApiCall {
apiService.getFacePage(
request = mapOf(
"pageNum" to pageNum,
"pageSize" to pageSize
)
)
}
}
suspend fun getFaceIncrement(
pageNum: Long = 1L,
pageSize: Long = 100L,
timestamp: Long
): ApiResponse<List<FaceVO>?> {
return safeApiCall {
apiService.getFaceIncrement(
request = mapOf(
"pageNum" to pageNum,
"pageSize" to pageSize,
"timestamp" to timestamp
)
)
}
}
suspend fun getFoodByNames(
nameList: List<String>,
deviceType: Int = 2
): ApiResponse<List<NewFoodInfo>?> {
return safeApiCall {
apiService.getFoodByNames(
request = FoodSearchReq(nameList, deviceType)
)
}
}
suspend fun searchFood(
name: String,
pageNum: Long = 1L,
pageSize: Long = 100L
): ApiResponse<List<NewFoodInfo>?> {
return safeApiCall {
apiService.searchFood(
request = mapOf(
"name" to name,
"pageNum" to pageNum,
"pageSize" to pageSize
)
)
}
}
suspend fun getUserCurrentFood(
userId: Long
): ApiResponse<UserNutrition?> {
return safeApiCall {
apiService.getUserCurrentFood(
request = mapOf("id" to userId)
)
}
}
suspend fun placeOrder(
orderRequest: PlaceOrderRequest
): ApiResponse<String?> {
return safeApiCall {
apiService.placeOrder(request = orderRequest)
}
}
suspend fun getSettlementOrders(
userId: Long
): ApiResponse<SettlementOrder?> {
return safeApiCall {
apiService.getSettlementOrders(
request = mapOf("id" to userId)
)
}
}
suspend fun getMemberInfo(
userId: Long
): ApiResponse<NewMemberInfo?> {
return safeApiCall {
apiService.getMemberInfo(
request = mapOf("id" to userId)
)
}
}
suspend fun getMemberInfoByPhone(
phone: String,
password: String = ""
): ApiResponse<NewMemberInfo?> {
return safeApiCall {
apiService.getMemberInfoByPhone(
request = mapOf(
"phone" to phone,
"password" to password
)
)
}
}
suspend fun getMemberDiscount(
userId: Long
): ApiResponse<String?> {
return safeApiCall {
apiService.getMemberDiscount(
request = mapOf("id" to userId)
)
}
}
suspend fun bindUserOrder(
userId: Long,
orderNo: String,
mode: Int? = null
): ApiResponse<Any?> {
return safeApiCall {
apiService.bindUserOrder(
request = BindUserOrderRequest(userId, orderNo, mode)
)
}
}
suspend fun getCollectPage(
pageNum: Long = 1L,
pageSize: Long = 100L,
foodName: String? = null
): ApiResponse<List<CollectedFoodV2>?> {
return safeApiCall {
val request = mutableMapOf<String, Any>(
"pageNum" to pageNum,
"pageSize" to pageSize
)
if (!foodName.isNullOrEmpty()) {
request["foodName"] = foodName
}
apiService.getCollectPage(request = request)
}
}
suspend fun getCollectVectorPage(
pageNum: Long = 1L,
pageSize: Long = 100L
): ApiResponse<List<CollectedFoodV2>?> {
return safeApiCall {
apiService.getCollectVectorPage(
request = mutableMapOf(
"pageNum" to pageNum,
"pageSize" to pageSize
)
)
}
}
suspend fun uploadCollect(
foodId: Long,
foodName: String,
version: String,
foodVector: String,
fileList: List<File>
): ApiResponse<List<String>?> {
val params = hashMapOf<String, RequestBody>()
params["foodId"] = foodId.toString().toRequestBody("text/plain".toMediaTypeOrNull())
params["foodName"] = foodName.toRequestBody("text/plain".toMediaTypeOrNull())
params["version"] = version.toRequestBody("text/plain".toMediaTypeOrNull())
params["foodVector"] = foodVector.toRequestBody("text/plain".toMediaTypeOrNull())
val fileParts = mutableListOf<MultipartBody.Part>()
fileList.forEach { file ->
val requestFile = file.asRequestBody("multipart/form-data".toMediaTypeOrNull())
val filePart = MultipartBody.Part.createFormData(
"foodPics",
file.name,
requestFile
)
fileParts.add(filePart)
}
return safeApiCall {
apiService.uploadCollect(params = params, foodPics = fileParts)
}
}
suspend fun deleteCollect(
foodId: Long,
version: String
): ApiResponse<Any?> {
return safeApiCall {
apiService.deleteCollect(
request = mapOf<String, Any>(
"foodId" to foodId,
"version" to version
)
)
}
}
}
@@ -4,8 +4,10 @@ import com.sw.plate.utils.ToastUtils
import com.wabon.wbintelligenthardwaresdk.api.SensorScale
import com.wabon.wbintelligenthardwaresdk.api.SensorScale.OnScaleResult
import timber.log.Timber
import kotlin.math.roundToInt
typealias Callback = (Double) -> Unit
typealias WeightCallback = (Int) -> Unit
private const val TAG = "SensorScaleUtils"
@@ -22,6 +24,7 @@ object SensorScaleUtils {
var isZero = false
private var callback: Callback? = {}
private var lastWeight: Double? = null
private var lastCallbackWeight: Double? = null
private fun init() {
mSensorScale = SensorScale(object : OnScaleResult {
@@ -29,17 +32,28 @@ object SensorScaleUtils {
* 读取重量
*/
override fun readWeight(state: Int, value: Double) {
val stateStr = when (state) {
SensorScale.STATE_STABLE -> "稳定"
SensorScale.STATE_UNSTABLE -> "不稳定"
SensorScale.STATE_OVER_WEIGHT -> "量程溢出"
else -> "未知"
}
// val stateStr = when (state) {
// SensorScale.STATE_STABLE -> "稳定"
// SensorScale.STATE_UNSTABLE -> "不稳定"
// SensorScale.STATE_OVER_WEIGHT -> "量程溢出"
// else -> "未知"
// }
// Log.d(TAG, "readWeight state = ${stateStr}, weight = $value")
// 只使用稳定值
if (state == SensorScale.STATE_STABLE && lastWeight != value) {
lastWeight = value
callback?.invoke(value)
if (state == SensorScale.STATE_STABLE) {
if (lastWeight != value) {
lastWeight = value
callback?.invoke(value)
}
// 去重:仅在重量变化时回调 weightCallbackList,避免同一重量反复触发
if (lastCallbackWeight != value) {
lastCallbackWeight = value
weightCallbackList.forEach {
val currentWeight = value * 1000
it.invoke(currentWeight.roundToInt())
}
}
}
}
@@ -60,7 +74,7 @@ object SensorScaleUtils {
* 开启称重
* @param autoScale 是否开启自动读取
*/
fun startScale(autoScale: Boolean = true, callback: Callback?) {
fun startScale(autoScale: Boolean = true, callback: Callback? = null) {
Timber.d("startScale isOpened = $isOpened")
if (isOpened) {
startContinuousRead(callback = callback)
@@ -79,8 +93,9 @@ object SensorScaleUtils {
// 打开后需要等待后才能调用,否则会 1001 SDK未初始化
mSensorScale?.startContinuousRead()
// Thread.sleep(1000)
zero()
// TODO: 暂时不标定---------
zero()
}.start()
}
}
@@ -112,16 +127,25 @@ object SensorScaleUtils {
mSensorScale?.readWeight()
}
private var weightCallbackList: MutableList<WeightCallback> = mutableListOf()
fun addWeightListener(weightCallback: WeightCallback) {
if (weightCallbackList.contains(weightCallback).not()) {
weightCallbackList.add(weightCallback)
}
}
/**
* 零位标定
*/
fun zero() {
fun zero(isShowToastRemind: Boolean = false) {
Timber.d("zero isOpened = $isOpened, mSensorScale = $mSensorScale")
if (!isOpened) return
mSensorScale?.zero {
isZero = true
Timber.d("zero 零位标定操作成功")
ToastUtils.showToast("零位标定操作成功")
if (isShowToastRemind) {
ToastUtils.showToast("零位标定操作成功")
}
}
}
@@ -0,0 +1,300 @@
package com.sw.dualscreen.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,21 @@
package com.sw.dualscreen.socket;
public class LanServer {
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;
}
}
@@ -0,0 +1,21 @@
package com.sw.dualscreen.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.dualscreen.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,32 @@
package com.sw.dualscreen.socket
import android.util.Log
import org.json.JSONObject
import java.lang.Exception
open class TcpClientListenerImpl : TcpClient.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.dualscreen.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.dualscreen.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,57 @@
package com.sw.dualscreen.utils
import android.app.Activity
import android.os.Process
import java.util.Stack
object ActivityManager {
private val activityStack = Stack<Activity>()
// 添加Activity到栈
fun addActivity(activity: Activity) {
activityStack.add(activity)
}
// 移除指定Activity
fun removeActivity(activity: Activity) {
activityStack.remove(activity)
}
// 获取当前Activity
fun currentActivity(): Activity? {
return if (activityStack.isEmpty()) null else activityStack.lastElement()
}
// 结束指定Activity
fun finishActivity(activity: Activity) {
if (!activity.isFinishing) {
activity.finish()
}
}
// 结束所有Activity
fun finishAllActivity() {
activityStack.forEach {
if (!it.isFinishing) {
it.finish()
}
}
activityStack.clear()
}
// 退出应用程序
fun exitApp() {
finishAllActivity()
Process.killProcess(Process.myPid())
}
fun getActivityStack(): Stack<Activity>{
return activityStack
}
fun isEmpty() : Boolean {
return activityStack.isEmpty()
}
}
@@ -1,6 +1,7 @@
package com.sw.dualscreen.utils
import android.graphics.Bitmap
object BitmapCropper {
/**
* 裁剪Bitmap中心区域为指定尺寸
@@ -9,30 +10,33 @@ object BitmapCropper {
* @param targetHeight 目标高度
* @return 裁剪后的Bitmap
*/
fun cropCenter(original: Bitmap, targetWidth: Int, targetHeight: Int): Bitmap {
fun cropCenter(original: Bitmap, targetWidth: Int, targetHeight: Int, offsetX:Int = 0, offsetY:Int = 0): Bitmap {
val originalWidth = original.width
val originalHeight = original.height
// 计算中心点坐标
var startX = (originalWidth - targetWidth) / 2
var startY = (originalHeight - targetHeight) / 2
var startX = (originalWidth - targetWidth) / 2 + offsetX
var startY = (originalHeight - targetHeight) / 2 + offsetY
// 边界检查
startX = startX.coerceAtLeast(0)
startY = startY.coerceAtLeast(0)
val actualWidth = minOf(targetWidth, originalWidth - startX)
val actualHeight = minOf(targetHeight, originalHeight - startY)
return Bitmap.createBitmap(original, startX, startY, actualWidth, actualHeight)
return Bitmap.createBitmap(original, startX, startY, actualWidth, actualHeight).also {
// it.setConfig(Bitmap.Config.RGB_565)
// it.density = DisplayMetrics.DENSITY_LOW
}
}
}
// 使用示例
fun main() {
// 假设这是从资源加载的Bitmap
val originalBitmap = Bitmap.createBitmap(1000, 1000, Bitmap.Config.ARGB_8888)
// 裁剪中心500x500区域
val croppedBitmap = BitmapCropper.cropCenter(originalBitmap, 500, 500)
println("裁剪后尺寸:${croppedBitmap.width}x${croppedBitmap.height}")
}
//fun main() {
// // 假设这是从资源加载的Bitmap
// val originalBitmap = Bitmap.createBitmap(1000, 1000, Bitmap.Config.ARGB_8888)
// // 裁剪中心500x500区域
// val croppedBitmap = BitmapCropper.cropCenter(originalBitmap, 500, 500)
// println("裁剪后尺寸:${croppedBitmap.width}x${croppedBitmap.height}")
//}
@@ -11,16 +11,18 @@ class CameraUtils(private var activity: ComponentActivity) {
private var cameraController: LifecycleCameraController? = null
private var photoCaptureHelper: PhotoCaptureHelper? = null
private var failCallback: ((msg: String) -> Unit)? = null
// private var isCameraReady = false
fun takePhoto(callback: (Uri) -> Unit) {
fun takePhoto(succCallback: (Uri) -> Unit, failCallback: (msg: String) -> Unit = { }) {
this.failCallback = failCallback
cameraController?.let {
if (photoCaptureHelper == null) {
initCaptureHelper()
}
}
photoCaptureHelper?.let {
it.addSuccessCallback(callback)
it.addSuccessCallback(succCallback)
it.bindCameraCallback {
bind()
}
@@ -32,9 +34,9 @@ class CameraUtils(private var activity: ComponentActivity) {
photoCaptureHelper = PhotoCaptureHelper(
context = activity,
cameraController = cameraController!!,
onSuccess = {},
onError = { msg ->
//toast(msg)
failCallback?.invoke(msg)
}
)
}
@@ -0,0 +1,32 @@
package com.sw.dualscreen.utils
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Date
object DateTimeUtil {
const val YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss"
fun formatDateTime(dateTime: LocalDateTime, pattern: String = YYYY_MM_DD_HH_MM_SS): String {
val formatter = DateTimeFormatter.ofPattern(pattern)
return dateTime.format(formatter)
}
fun convert(dateStr: String, pattern: String = YYYY_MM_DD_HH_MM_SS): Date {
val formatter = DateTimeFormatter.ofPattern(pattern)
val ldt = LocalDateTime.parse(dateStr, formatter)
val zdt = ldt.atZone(ZoneId.systemDefault())
return Date.from(zdt.toInstant())
}
fun main() {
val now = LocalDateTime.now()
println("默认格式: ${formatDateTime(now)}")
println("自定义格式: ${formatDateTime(now, "yyyy年MM月dd日 HH时mm分ss秒")}")
}
}
@@ -0,0 +1,168 @@
package com.sw.dualscreen.utils;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Handler;
import android.os.Looper;
import com.sw.plate.utils.arcface.facedb.FaceDatabase;
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
/**
* 从 assets 目录下的测试人脸数据库导入数据到 Room 数据库
*
* <p>以分批方式读取,每批 {@link #BATCH_SIZE} 条,边读边写,
* 避免 4350 条 + 17MB 特征数据一次性加载到内存。</p>
*/
public class FaceDbImporter {
private static final String TAG = "FaceDbImporter";
/** 每批写入条数 */
private static final int BATCH_SIZE = 200;
/** assets 中测试数据库的相对路径 */
private static final String ASSETS_DB_PATH = "db/faceDB.db";
/**
* 导入回调
*/
public interface ImportCallback {
/** 进度更新,已切到主线程 */
void onProgress(int current, int total);
/** 导入失败,已切到主线程 */
void onError(String message);
/** 导入完成,已切到主线程 */
void onComplete();
}
private FaceDbImporter() {
}
/**
* 从 assets 导入测试人脸数据到 Room 数据库
*
* @param context 上下文
* @param callback 回调(所有回调均已在主线程)
*/
public static void importFromAssets(Context context, ImportCallback callback) {
Context appContext = context.getApplicationContext();
new Thread(() -> {
File tempFile = null;
SQLiteDatabase testDb = null;
Cursor cursor = null;
try {
// 1. 从 assets 复制到缓存目录(SQLiteDatabase 需要文件路径)
tempFile = new File(appContext.getCacheDir(), "faceDB_import_temp.db");
copyAssetToFile(appContext, ASSETS_DB_PATH, tempFile);
// 2. 以只读方式打开测试数据库
testDb = SQLiteDatabase.openDatabase(
tempFile.getAbsolutePath(),
null,
SQLiteDatabase.OPEN_READONLY
);
// 3. 查询总记录数
int totalCount = 0;
Cursor countCursor = testDb.rawQuery("SELECT COUNT(*) FROM face", null);
if (countCursor.moveToFirst()) {
totalCount = countCursor.getInt(0);
}
countCursor.close();
if (totalCount == 0) {
postToMain(() -> {
if (callback != null) callback.onComplete();
});
return;
}
// 4. 分批读取 + 批量写入
cursor = testDb.rawQuery(
"SELECT user_name, feature_data, register_time FROM face ORDER BY faceId",
null
);
List<FaceEntity> batch = new ArrayList<>(BATCH_SIZE);
int processedCount = 0;
while (cursor.moveToNext()) {
String userName = cursor.getString(0);
byte[] featureData = cursor.getBlob(1);
long registerTime = cursor.getLong(2);
FaceEntity entity = new FaceEntity(userName, null, featureData);
entity.setUserType("2");
entity.setRegisterTime(registerTime);
batch.add(entity);
if (batch.size() >= BATCH_SIZE) {
FaceDatabase.getInstance(appContext).faceDao().insert(batch);
processedCount += batch.size();
notifyProgress(callback, processedCount, totalCount);
batch.clear();
}
}
// 5. 写入剩余不足一批的数据
if (!batch.isEmpty()) {
FaceDatabase.getInstance(appContext).faceDao().insert(batch);
processedCount += batch.size();
notifyProgress(callback, processedCount, totalCount);
}
L.d(TAG, "导入完成,共 " + processedCount + " 条记录");
postToMain(() -> {
if (callback != null) callback.onComplete();
});
} catch (Exception e) {
L.e(TAG, "导入失败: " + e.getMessage());
postToMain(() -> {
if (callback != null) callback.onError(e.getMessage());
});
} finally {
if (cursor != null) cursor.close();
if (testDb != null && testDb.isOpen()) testDb.close();
if (tempFile != null && tempFile.exists()) tempFile.delete();
}
}).start();
}
/**
* 从 assets 复制文件到指定路径
*/
private static void copyAssetToFile(Context context, String assetPath, File destFile)
throws Exception {
try (InputStream is = context.getAssets().open(assetPath);
FileOutputStream fos = new FileOutputStream(destFile)) {
byte[] buffer = new byte[8192];
int length;
while ((length = is.read(buffer)) > 0) {
fos.write(buffer, 0, length);
}
fos.flush();
}
}
private static void notifyProgress(ImportCallback callback, int current, int total) {
postToMain(() -> {
if (callback != null) {
callback.onProgress(current, total);
}
});
}
private static void postToMain(Runnable runnable) {
new Handler(Looper.getMainLooper()).post(runnable);
}
}
@@ -0,0 +1,699 @@
package com.sw.dualscreen.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 FileUtil {
private static boolean isSaveLog = true;
// public static final String FILE_STR_PATH = App.getContext().getExternalCacheDir().getAbsolutePath();
// 使用内部存储,不会被系统缓存清理策略影响
public static final String FILE_STR_PATH = new File(App.getContext().getFilesDir(), "logs").getAbsolutePath();
// 单线程执行器,保证日志串行写入,避免多线程并发竞争
// 单线程执行器,保证日志串行写入,避免多线程并发竞争
private static final ExecutorService LOG_EXECUTOR = Executors.newSingleThreadExecutor();
public static final int SIZETYPE_B = 1;//获取文件大小单位为B的double值
public static final int SIZETYPE_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(log)
.append("\n");
}
String fileName = "log" + AppUtil.formatDateGetDay(System.currentTimeMillis()) + ".txt";
FileUtil.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,32 @@
package com.sw.dualscreen.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
// 倒计时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,80 @@
package com.sw.dualscreen.utils
import androidx.lifecycle.LifecycleCoroutineScope
import com.sw.dualscreen.model.response.v2.CollectedFoodV2
import com.sw.dualscreen.objbox.Food
import com.sw.dualscreen.objbox.ObjectBox
import com.sw.dualscreen.viewmodel.NetViewModelV2
import kotlinx.coroutines.launch
object FoodVectorTool {
/** 每页拉取条数 */
private const val PAGE_SIZE = 100
/**
* 统一入口:分页拉取所有食物向量数据并保存到本地 ObjectBox
*/
fun loadAndSaveFoodVector(
userViewModel: NetViewModelV2,
lifecycleScope: LifecycleCoroutineScope,
successBlock: () -> Unit,
failureBlock: (String) -> Unit
) {
fetchPage(
pageNum = 1,
userViewModel = userViewModel,
lifecycleScope = lifecycleScope,
successBlock = successBlock,
failureBlock = failureBlock
)
}
private fun fetchPage(
pageNum: Int,
userViewModel: NetViewModelV2,
lifecycleScope: LifecycleCoroutineScope,
successBlock: () -> Unit,
failureBlock: (String) -> Unit
) {
userViewModel.getCollectVectorPage(
pageNum = pageNum.toLong(),
pageSize = PAGE_SIZE.toLong(),
onSuccess = { items ->
if (pageNum == 1 && items.isEmpty()) {
successBlock()
return@getCollectVectorPage
}
if (items.isNotEmpty()) {
saveFoodVector(lifecycleScope, items)
}
if (items.size >= PAGE_SIZE) {
fetchPage(pageNum + 1, userViewModel, lifecycleScope, successBlock, failureBlock)
} else {
successBlock()
}
},
onFailure = { failureBlock(it) }
)
}
/**
* 将 CollectedFoodV2 列表保存到 ObjectBox 本地数据库
*/
private fun saveFoodVector(lifecycleScope: LifecycleCoroutineScope, list: List<CollectedFoodV2>) {
lifecycleScope.launch {
val vectorList = list.map { item ->
val foodVector = item.foodVector?.removeSurrounding("[", "]")
?.split(",")?.map { it.toFloatOrNull() ?: 0.0f }?.toFloatArray()
Food(
collectId = item.foodId,
foodId = item.foodId,
foodName = item.foodName,
version = item.version,
foodVector = foodVector
)
}
ObjectBox.putAll(vectorList)
}
}
}

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