Author SHA1 Message Date
mazengfei 86c3c07e42 隐藏测试代码 2026-03-12 11:10:26 +08:00
lvmengandClaude Sonnet 4.6 aa96455f66 refactor(face): 优化人脸特征存储与增量同步逻辑
- FaceDao 新增 queryAllByUserName 查询用户全部特征记录
- 改为查询所有记录后做去重判断,支持同一用户保存多条特征数据
- 将 Thread 替换为 lifecycleScope.launch(Dispatchers.IO) 协程
- 提取 getNewFaceEntity 方法消除重复代码
- 新增特征比对调试日志

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

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 09:07:48 +08:00
lvmeng 513f4dbaea 增加查询用户数量 2026-03-04 09:03:32 +08:00
lvmengandClaude Sonnet 4.6 037816c303 fix(activity): 修复协程冗余 start() 调用并收敛方法可见性
- 移除 lifecycleScope.launch 后的 .start(),launch 已自动启动协程
- createBlackNV21Frame 和 emptyFrame 加 private 修饰符,收敛对外可见性

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 09:19:49 +08:00
mazengfei 4ea95092f5 优化 2026-02-28 16:54:57 +08:00
lvmeng cc88dccaa6 识别成功未提示及采集时间长问题优化 2026-02-28 10:03:45 +08:00
mazengfei e979322697 优化 2026-02-05 18:16:38 +08:00
lvmeng 1e6ea1517c 人脸识别+采集分开处理 2026-02-05 15:05:45 +08:00
lvmeng 2ee4d2d320 人脸识别+采集分开处理 2026-02-05 13:32:45 +08:00
lvmeng 67f5d07f25 优化 2026-02-04 18:01:15 +08:00
lvmeng 79e089363d 优化重复注册 2026-02-04 10:20:34 +08:00
lvmeng 16c9e5f61a 优化重复注册 2026-02-04 09:23:19 +08:00
lvmeng a8bbf68b9b 优化 2026-02-03 18:51:13 +08:00
mazengfei 0024ea2775 优化 2026-02-03 16:19:42 +08:00
lvmeng 9fea23303c 打开app不再清除本地数据,不再发送数据到结算终端,改为新增人脸采集接口,全量增量接口优化 2026-02-03 14:59:22 +08:00
mazengfei c5325f6ad6 优化流程,测试不再验证活体 2026-02-02 16:36:24 +08:00
mazengfei e1e754be9f 调整提示音 2026-01-29 14:31:23 +08:00
mazengfei 1d0e6d4bca 隐藏toast 2026-01-26 17:32:50 +08:00
lvmeng 8f788529d2 增加日志 2026-01-26 15:16:35 +08:00
mazengfei 5bc244dde5 增加无餐盘提醒 2026-01-22 15:49:36 +08:00
lvmeng d4f9ace67b 新增识别提示 2026-01-22 15:10:12 +08:00
lvmeng 855a84684c 人脸识别小于阈值时增加重试次数,降低识别错误情况 2026-01-22 10:30:57 +08:00
mazengfei f2bedb7777 优化 2026-01-21 17:58:55 +08:00
mazengfei aad4f299aa 优化 2026-01-21 15:53:53 +08:00
lvmeng 176c555860 采集优化 2026-01-21 14:27:16 +08:00
mazengfei de4e5d57e2 优化 2026-01-21 13:00:58 +08:00
mazengfei 8326ff0a03 优化 2026-01-20 19:04:17 +08:00
mazengfei b7408dc6c4 优化 2026-01-20 14:00:28 +08:00
mazengfei ec12017fd4 优化 2026-01-20 12:23:38 +08:00
lvmeng 662e2c63e1 优化--- 2026-01-20 08:32:14 +08:00
mazengfei f473d1f174 优化 2026-01-19 17:55:16 +08:00
lvmeng 3b51e6f3f2 日志按钮优化 2026-01-19 14:35:00 +08:00
lvmeng 40d41d1afb 增加实时日志查询 2026-01-19 14:32:48 +08:00
mazengfei b23045194a 增加日志 2026-01-19 13:08:28 +08:00
mazengfei 1b4762916a 餐盘柜优化-不绑定用户可多次取盘 2026-01-16 16:45:13 +08:00
mazengfei cf2d2db42f 餐盘柜优化-绑定临时用户 2026-01-16 16:14:38 +08:00
mazengfei 8337b3fabb 餐盘柜优化 2026-01-16 11:53:44 +08:00
mazengfei ab58651733 取盘优化 2026-01-15 11:39:13 +08:00
mazengfei aadba7efd5 增加人脸采集功能;
识别人脸成功后打开有餐盘的第一个柜子;
扫码打开已绑定的柜子,无则打开第一个空柜子并放入餐盘;
设置页面问题优化;
2026-01-14 19:00:20 +08:00
mazengfei 11a0a41e3c Rename .java to .kt 2026-01-14 19:00:20 +08:00
103 changed files with 6188 additions and 2750 deletions
+2 -2
View File
@@ -4,10 +4,10 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-11-06T09:21:30.965681400Z"> <DropdownSelection timestamp="2026-02-05T08:42:12.462597100Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="Default" identifier="serial=?;connection=3c39678a" /> <DeviceId pluginId="Default" identifier="serial=192.168.3.114:5555;connection=a41f05a9" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>
+1 -1
View File
@@ -6,7 +6,7 @@
<GradleProjectSettings> <GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" /> <option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="jbr-21" /> <option name="gradleJvm" value="azul-17" />
<option name="modules"> <option name="modules">
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />
+1 -2
View File
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="zulu-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">
-55
View File
@@ -1,55 +0,0 @@
# Repository Guidelines
本项目 **SmartPlateCabinet / SmartTakePlate(发盘机)** 是基于 Android 的智能餐盘柜控制系统,集成人脸识别(ArcFace SDK)与 PLC 硬件控制。本文档为贡献者提供统一规范。
## 项目结构与模块组织
多模块 Gradle 工程,模块定义于 `settings.gradle.kts`
- `app/` — 主应用模块,命名空间 `com.sw.platecabinet`applicationId `com.sw.take.plate`)。源码位于 `app/src/main/java/com/sw/platecabinet/`,按职责分包:`activity``fragment``viewmodel``repository``network``api`/`interceptor`/`task`)、`model``request`/`response`)、`socket``utils`PLC 实现在 `utils/mego`)、`adapter``dialog``view``receiver``ext`
- `lib_face/` — 人脸识别库模块(`com.sw.plate.utils.arcface`),封装 ArcFace SDK,含 `camera``face``facedb`Room 持久化)、`faceserver``viewmodel` 等。
- 依赖版本统一管理于 `gradle/libs.versions.toml`;本地 AAR 依赖置于 `app/libs/`(如 PLC SDK `plc_aar_104.aar`)。
- 测试目录:`app/src/test`(单元测试)、`app/src/androidTest`(仪器测试)。
## 构建、测试与开发命令
```bash
./gradlew assembleDebug # 编译 Debug APK
./gradlew installDebug # 安装到已连接设备
./gradlew clean # 清理构建产物
./gradlew :app:test # 运行单元测试
./gradlew :app:connectedAndroidTest # 运行仪器测试(需连接设备)
```
修改 Kotlin/Java 或资源后,须运行 `./gradlew assembleDebug` 验证编译通过。
## 编码规范与命名约定
- 语言:Kotlin 优先,Java 仅限历史代码维护;JVM target 11。
- 架构:MVVM。Activity 继承 `BaseActivity<VB>`Fragment 继承 `BaseFragment<VB>`ViewModel 继承 `BaseViewModel`Repository 继承 `BaseRepository`
- 命名:`XxxActivity` / `XxxFragment` / `XxxViewModel` / `XxxRepository`;工具类 `XxxUtil``XxxHelper`
- 异步:协程(`viewModelScope.launch`);PLC 操作走回调 `PlcCallback<T>`;网络请求在 ViewModel 内用 `launchRequest { }`
- 资源:布局 `activity_*` / `fragment_*`,字符串禁止硬编码到布局;依赖版本禁止硬编码于 `build.gradle.kts`,统一使用版本目录。
- 注释统一使用简体中文;标识符保持英文。
## 测试指南
框架:JUnit 4 + AndroidX TestEspresso)。业务逻辑优先在 `src/test` 编写单元测试;涉及 UI 或硬件交互的改动,须说明是否需仪器测试。测试类置于对应包路径下。
## 提交与分支约定
提交信息采用 **Conventional Commits(中文描述)**`<type>(<scope>): <中文描述>`。常见 type`feat``fix``refactor`scope 如 `login``plc``face``ui``build``api``base`。示例:
```
feat(plc): 添加餐盘平台复位功能和无餐盘提示界面
fix(login): 修复人脸识别登录界面返回时串口连接超时问题
```
分支:`main` 为基础分支;功能分支命名形如 `main_<功能>_<YYMMDD>`(如 `main_吐盘机_260701_5.0`)。合并请求需描述变更内容、关联问题;改动 UI 或硬件交互时附截图或验证说明。
## 配置与安全提示
- 签名:Debug 使用 `app/swkey.jks`(见 `app/build.gradle.kts`)。
- 明文 HTTP 流量已通过 `network_security_config.xml` 放行,请勿在受控环境外泄露配置。
- `app` 依赖 `lib_face` 时已排除其中的 `android-serialport`,由 app 自行引入,避免重复依赖。
- ABI 过滤:`armeabi-v7a``arm64-v8a`
-198
View File
@@ -1,198 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目概述
SmartTakePlate(发盘机)是一个基于 Android 的智能餐盘柜控制系统,集成了人脸识别和 PLC 硬件控制功能。
**应用ID**: com.sw.platecabinet.member(会员版;命名空间 com.sw.platecabinet
**最低SDK**: 24 (Android 7.0)
**目标SDK**: 35
## 构建与运行
### 构建项目
```bash
./gradlew assembleDebug
```
### 安装到设备
```bash
./gradlew installDebug
```
### 清理构建
```bash
./gradlew clean
```
**注意**: 构建需要 Java 17 环境(compileOptions 为 Java 11,但 Gradle 需 JDK 17)。直接使用系统默认 JDK 11 会报错,需使用 Android Studio 自带的 JBR 设置 `JAVA_HOME` 后再执行 gradlew。
## 项目架构
### 模块结构
- `app/` - 主应用模块(PLC控制、业务逻辑)
- `lib_face/` - 人脸识别库模块(ArcFace SDK封装)
### 架构模式
采用 MVVM 架构:
- **View**: Activity/Fragment(使用 ViewBinding
- **ViewModel**: 继承自 `BaseViewModel`,处理业务逻辑和状态管理
- **Repository**: `BaseRepository``RemoteRepository` 处理数据层
### 关键技术栈
- Kotlin 2.0.21 + Coroutines
- Retrofit 3.0.0 + OkHttp 4.12.0(网络请求)
- Room 2.2.5(人脸数据本地存储)
- CameraX 1.3.0(摄像头)
- EventBus 3.3.1(事件总线)
- PLC SDK (plc_aar_104.aar)(硬件控制)
- ArcSoft 人脸识别 SDK
## 核心功能模块
### 1. 串口硬件控制(柜锁)
**核心包**: `lib_face/com.sw.plate.utils.comn/`
关键类:
- `SerialApi` - 对外 API 门面(静态方法),串口路径 `/dev/ttyS2`、波特率 19200
- `init()` - 打开串口连接
- `openPlate(boxNumber, callback)` - 开柜(命令由 `CabinetLockCommand.generateOpenCommand` 生成)
- `close()` - 关闭串口
- `SerialPortManager` - 串口管理(发送命令、接收线程 `SerialReadThread`
- `CabinetLockCommand` - 柜锁命令生成
**调用方**: `BaseActivity``BindPlateFragment``SettingListFragment` 直接调用 `SerialApi`
**注意**: 所有串口操作都是异步的,通过 `SerialPortManager.SendCallback` 回调返回结果。
### 2. 人脸识别
**核心包**: `lib_face/com.sw.plate.utils.arcface/`
关键类:
- `FaceApi` - 人脸操作 API
- `RecognizeViewModel` - 人脸识别 ViewModel
- `FaceDatabase` - Room 数据库(存储人脸特征)
- `CameraHelper` / `DualCameraHelper` - 摄像头管理
人脸数据流程:
1. 从服务器获取用户人脸数据(`UserViewModel.getUserFaceList()`
2. 存储到本地 Room 数据库
3. 实时识别时与本地数据库对比
### 3. 网络层
**API 配置**: `GlobalData.appBaseUrl`(当前指向 `https://dev.yixiong-tech.com:8081`,切换环境直接修改该字段)
**API 响应格式**: `ApiResponse<T>`
- 成功: `code == "00000"`
- 失败: 检查 `msg` 字段
**网络客户端**: `ApiClient`Retrofit 单例)
**API 接口**: `ApiService`
### 4. 全局配置
**GlobalData** 包含关键配置:
- `arrayCross` = 2(横排数量)
- `arrayVertical` = 11(竖排数量)
- `arrayMode` = 0(0=竖直排列, 1=水平排列)
- ArcSoft SDK 配置:`appId`, `sdkKey`, `activeKey`
**SharedPreferences 键**: 见 `GlobalKey`
## 应用启动流程
1. `MyApp.onCreate()` - 初始化日志、崩溃处理、全局数据
2. `DeviceInitActivity` - 加载人脸缓存、获取设备配置
3. `LoginByFaceActivity` - 人脸识别登录
4. `MainActivity` - 主界面(Fragment 容器)
## 代码规范
### 命名约定
- Activity: `XxxActivity`
- Fragment: `XxxFragment`
- ViewModel: `XxxViewModel`
- Repository: `XxxRepository`
- 工具类: `XxxUtil``XxxHelper`
### 基类使用
- Activity 继承 `BaseActivity<VB>`VB 为 ViewBinding 类型)
- Fragment 继承 `BaseFragment<VB>`
- ViewModel 继承 `BaseViewModel`
- Repository 继承 `BaseRepository`
### 异步操作
- 使用 Kotlin Coroutines`viewModelScope.launch`
- PLC 操作使用回调(`PlcCallback<T>`
- 网络请求在 ViewModel 中调用 `launchRequest { }`
## 重要注意事项
### NDK 架构
项目仅支持 **armeabi-v7a**32位 ARM),不支持 arm64-v8a。这是因为 PLC SDK 和 ArcSoft SDK 的限制。
### 硬件依赖
- 需要串口通信支持(android-serialport
- 需要摄像头权限(人脸识别)
- 需要网络权限(API 调用)
### 调试配置
- 签名密钥: `swkey.jks`(密码: 123456
- 允许明文 HTTP 流量(`network_security_config.xml`
- ProGuard 混淆已禁用
### lib_face 依赖
app 模块依赖 lib_face 时,**排除了 android-serialport 依赖**,因为 app 模块自己引入了该库。
## 常用工具类
- `SpTool` - SharedPreferences 操作
- `Debouncer` - 防抖
- `QRCodeUtil` - 二维码生成
- `PermissionHelper` - 权限管理
- `FragmentHelper` - Fragment 管理
## Git 分支策略
- 主分支: `main`
- 功能分支命名形如 `main_<功能>_<YYMMDD>`(如 `main_吐盘机_260327`
## 最近开发重点
根据最近的提交记录,项目正在进行以下工作:
1. 会员版(已修改包名)
2. 绑定/解绑餐盘功能
3. 人脸识别登录(含手机号密码登录)
## API 调用示例
```kotlin
// 在 ViewModel 中
launchRequest(
request = { repository.someApiCall(params) },
onSuccess = { data ->
// 处理成功响应
},
onError = { code, msg ->
// 处理错误
}
)
```
## 串口开柜示例
```java
// 串口操作为异步回调(SerialPortManager.SendCallback
SerialApi.openPlate(boxNumber, new SerialPortManager.SendCallback() {
@Override
public void onSuccess() {
// 开柜命令发送成功
}
@Override
public void onFail(Exception e) {
// 开柜失败
}
});
```
+2 -2
View File
@@ -14,11 +14,11 @@ android {
keyPassword = "123456" keyPassword = "123456"
} }
} }
namespace = "com.sw.platecabinet.member" namespace = "com.sw.platecabinet"
compileSdk = 35 compileSdk = 35
defaultConfig { defaultConfig {
applicationId = "com.sw.platecabinet.member" applicationId = "com.sw.platecabinet"
minSdk = 24 minSdk = 24
targetSdk = 35 targetSdk = 35
versionCode = 1 versionCode = 1
Binary file not shown.
Binary file not shown.
-37
View File
@@ -1,37 +0,0 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.sw.take.plate",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "1.1",
"outputFile": "zncpg_1.1-release.apk"
}
],
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/zncpg_1.1-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/zncpg_1.1-release.dm"
]
}
],
"minSdkVersionForDexing": 24
}
+9 -8
View File
@@ -17,7 +17,7 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application <application
android:name="com.sw.platecabinet.MyApp" android:name=".MyApp"
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
@@ -31,7 +31,7 @@
tools:targetApi="31"> tools:targetApi="31">
<!-- 注册BootReceiver,监听开机完成广播 --> <!-- 注册BootReceiver,监听开机完成广播 -->
<receiver <receiver
android:name="com.sw.platecabinet.receiver.BootReceiver" android:name=".receiver.BootReceiver"
android:enabled="true" android:enabled="true"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
@@ -39,24 +39,24 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<activity <activity
android:name="com.sw.platecabinet.activity.UnBindDialogActivity" android:name=".activity.UnBindDialogActivity"
android:exported="false" android:exported="false"
android:theme="@style/DialogActivity" /> android:theme="@style/DialogActivity" />
<activity <activity
android:name="com.sw.platecabinet.activity.InitActivity" android:name=".activity.InitActivity"
android:exported="false" android:exported="false"
android:launchMode="singleTask"> android:launchMode="singleTask">
</activity> </activity>
<activity <activity
android:name="com.sw.platecabinet.activity.LoginByPwdActivity" android:name=".activity.LoginByPwdActivity"
android:exported="false" android:exported="false"
android:launchMode="singleTask"/> android:launchMode="singleTask"/>
<activity <activity
android:name="com.sw.platecabinet.activity.LoginByFaceActivity" android:name=".activity.LoginByFaceActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTask"/> android:launchMode="singleTask"/>
<activity <activity
android:name="com.sw.platecabinet.activity.DeviceInitActivity" android:name=".activity.DeviceInitActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTask"> android:launchMode="singleTask">
<intent-filter> <intent-filter>
@@ -66,9 +66,10 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name="com.sw.platecabinet.activity.MainActivity" android:name=".activity.MainActivity"
android:exported="false"> android:exported="false">
</activity> </activity>
<activity android:name="com.sw.platecabinet.activity.FunActivity" />
</application> </application>
</manifest> </manifest>
@@ -33,14 +33,7 @@ object GlobalData {
/** /**
* 具体业务baseurl * 具体业务baseurl
*/ */
var appBaseUrl: String = "" var appBaseUrl: String = "https://dev.yixiong-tech.com:8081"
/**
* 具体业务 BaseUrl
*/
const val LOCAL_BASE_URL: String = "http://192.168.10.101:24801"
const val TEST_BASE_URL: String = "https://dev.yixiong-tech.com:8081"
const val PROD_BASE_URL: String = "https://platform-api.uat.shuziweidao.com"
/** /**
* 设备id * 设备id
@@ -68,5 +61,4 @@ object GlobalKey {
const val KEY_FIRST_RUN = "firstRun" const val KEY_FIRST_RUN = "firstRun"
const val KEY_USER_INFO = "userInfoKey" const val KEY_USER_INFO = "userInfoKey"
const val PARAM_EQUIPMENT_INFO = "equipmentUserInfo" const val PARAM_EQUIPMENT_INFO = "equipmentUserInfo"
const val KEY_BASE_URL = "baseUrlKey"
} }
+10 -1
View File
@@ -3,16 +3,20 @@ package com.sw.platecabinet
import android.util.Log import android.util.Log
import com.sw.plate.App import com.sw.plate.App
import com.sw.plate.utils.AppUtil import com.sw.plate.utils.AppUtil
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.utils.CrashHandler import com.sw.platecabinet.utils.CrashHandler
import timber.log.Timber import timber.log.Timber
class MyApp : App() { class MyApp : App() {
companion object { companion object {
const val DEBUG: Boolean = true const val DEBUG: Boolean = true
var instance: MyApp? = null
var plateList: List<EquipmentUserInfo>? = null
} }
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
instance = this
Timber.plant(Timber.DebugTree()) Timber.plant(Timber.DebugTree())
Timber.d("初始化") Timber.d("初始化")
initGlobalData() initGlobalData()
@@ -26,10 +30,15 @@ class MyApp : App() {
*/ */
private fun initGlobalData() { private fun initGlobalData() {
var deviceId = AppUtil.getUDID(this) var deviceId = AppUtil.getUDID(this)
deviceId = "be154831-3466-3ba2-a2ea-57652c919fed" // deviceId = "be154831-3466-3ba2-a2ea-57652c919fed"
Log.d("MyApp", "initialize: deviceId=$deviceId") Log.d("MyApp", "initialize: deviceId=$deviceId")
GlobalData.deviceId = deviceId GlobalData.deviceId = deviceId
// TODO: 临时测试----------------
// if ("2987f0c5-5754-33e9-b00a-251db5e2e55f" == deviceId) {
// GlobalData.deviceId = "79814f20-f253-397c-9da7-8430ffa5ef00"
// }
GlobalData.appVersion = AppUtil.getAppVersionCode(this).toString() GlobalData.appVersion = AppUtil.getAppVersionCode(this).toString()
GlobalData.globalEquipmentCode = "202501171634" GlobalData.globalEquipmentCode = "202501171634"
@@ -1,10 +1,10 @@
package com.sw.platecabinet.activity package com.sw.platecabinet.activity
import android.app.Dialog
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import android.view.View import android.view.View
import android.view.WindowInsetsController import android.view.WindowInsetsController
@@ -16,30 +16,31 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.google.gson.Gson
import com.sw.inbound.utils.DateTimeUtils import com.sw.inbound.utils.DateTimeUtils
import com.sw.plate.App
import com.sw.plate.utils.Base64
import com.sw.plate.utils.ScanGunKeyEventHelper import com.sw.plate.utils.ScanGunKeyEventHelper
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.facedb.FaceDatabase import com.sw.plate.utils.arcface.facedb.FaceDatabase
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import com.sw.plate.utils.comn.SerialApi import com.sw.platecabinet.MyApp
import com.sw.plate.utils.comn.SerialPortManager import com.sw.platecabinet.R
import com.sw.platecabinet.member.R import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.member.databinding.ItemTitleTimeBinding import com.sw.platecabinet.ext.clickWithCoroutines
import com.sw.platecabinet.dialog.BalanceNotEnoughDialog
import com.sw.platecabinet.ext.setClickListeners
import com.sw.platecabinet.model.response.EquipmentUserInfo import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.utils.IntervalExecutor import com.sw.platecabinet.utils.IntervalExecutor
import com.sw.platecabinet.utils.PermissionHelper import com.sw.platecabinet.utils.PermissionHelper
import com.sw.platecabinet.utils.PlateUtils
import com.sw.platecabinet.utils.SpTool import com.sw.platecabinet.utils.SpTool
import com.sw.platecabinet.view.CustomLoadingDialog import com.sw.platecabinet.view.CustomLoadingDialog
import com.sw.platecabinet.viewmodel.NetViewModelV2
import com.sw.platecabinet.viewmodel.SettingViewModel import com.sw.platecabinet.viewmodel.SettingViewModel
import com.sw.platecabinet.viewmodel.UserViewModel import com.sw.platecabinet.viewmodel.UserViewModel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
/** /**
@@ -55,13 +56,10 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
protected var keyEventHelper: ScanGunKeyEventHelper? = null protected var keyEventHelper: ScanGunKeyEventHelper? = null
// 管理员对应的viewmodel // 管理员对应的viewmodel
private val settingViewModel by viewModels<SettingViewModel>() val settingViewModel by viewModels<SettingViewModel>()
// 用户对应的viewModel // 用户对应的viewModel
protected val viewModel by viewModels<UserViewModel>() protected val userViewModel by viewModels<UserViewModel>()
// V2 网络请求 viewModel(人脸缓存/增量同步/设备配置)
val netViewModelV2 by viewModels<NetViewModelV2>()
private var startTime: Long = 0 private var startTime: Long = 0
@@ -106,7 +104,28 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
* 处理扫描枪数据 * 处理扫描枪数据
*/ */
protected open fun handleScanKeyInfo(scanInfo: String) { protected open fun handleScanKeyInfo(scanInfo: String) {
settingViewModel.findByPlateNumber(plateNumber = scanInfo) // settingViewModel.findByPlateNumber(plateNumber = scanInfo)
var plateNumber = scanInfo
if (scanInfo.length > 6) {
plateNumber = scanInfo.take(6)
}
Log.d("TAG", "handleScanKeyInfo: " + Gson().toJson(MyApp.plateList))
if (MyApp.plateList == null) {
initPlateList()
}
//柜子未关联该餐盘,直接找空位置放入盘子
val item = MyApp.plateList?.firstOrNull { it.plateNumber.isNullOrBlank() }
if (item == null) {
//柜子已满
MainActivity.start(context = context, pageType = PageType.PLATE_CABINET_FULL)
return
}
//找到位置,放入餐盘,清除已有id和时间
item.plateNumber = plateNumber
item.faceId = null
item.updateTime = null
PlateUtils.open(this, item.equipmentBoxCode, true)
SpTool.savePlateData(MyApp.plateList!!)
} }
override fun dispatchKeyEvent(event: KeyEvent): Boolean { override fun dispatchKeyEvent(event: KeyEvent): Boolean {
@@ -121,12 +140,12 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
fun updateTime() { fun updateTime() {
headerBinding?.let { headerBinding?.let {
it.tvLeftDate.setClickListeners( it.tvLeftDate.clickWithCoroutines(
onDoubleClick = { onDoubleClick = {
onLeftDoubleClick() onLeftDoubleClick()
} }
) )
it.tvRightTime.setClickListeners( it.tvRightTime.clickWithCoroutines(
onDoubleClick = { onDoubleClick = {
onRightDoubleClick() onRightDoubleClick()
} }
@@ -237,22 +256,27 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
/** /**
* 显示等待提示框 * 显示等待提示框
*/ */
fun showWaitingDialog(tip: String?): Dialog? { fun showWaitingDialog(tip: String?) {
hideWaitingDialog() runOnUiThread {
val view = View.inflate(this, R.layout.dialog_waiting, null) hideWaitingDialog()
if (!TextUtils.isEmpty(tip)) (view.findViewById<View?>(R.id.tvTip) as TextView).setText(tip) val view = View.inflate(this, R.layout.dialog_waiting, null)
mDialogWaiting = CustomLoadingDialog(this, view, R.style.MyDialog) if (!TextUtils.isEmpty(tip)) (view.findViewById<View?>(R.id.tvTip) as TextView).setText(
mDialogWaiting!!.show() tip
mDialogWaiting!!.setCancelable(true) )
return mDialogWaiting mDialogWaiting = CustomLoadingDialog(this, view, R.style.MyDialog)
mDialogWaiting!!.show()
mDialogWaiting!!.setCancelable(true)
}
} }
/** /**
* 隐藏等待提示框 * 隐藏等待提示框
*/ */
fun hideWaitingDialog() { fun hideWaitingDialog() {
mDialogWaiting?.dismiss() runOnUiThread {
mDialogWaiting = null mDialogWaiting?.dismiss()
mDialogWaiting = null
}
} }
protected abstract fun inflateViewBinding(): VB protected abstract fun inflateViewBinding(): VB
@@ -266,40 +290,40 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
*/ */
protected open fun registerDataChange() { protected open fun registerDataChange() {
Timber.d("registerDataChange") Timber.d("registerDataChange")
lifecycleScope.launch { // lifecycleScope.launch {
viewModel.showLoading.collect { // viewModel.showLoading.collect {
Timber.d("registerDataChange 用户 showLoading = $it") // Timber.d("registerDataChange 用户 showLoading = $it")
if (it) { // if (it) {
showWaitingDialog("") // showWaitingDialog("")
} else { // } else {
hideWaitingDialog() // hideWaitingDialog()
} // }
} // }
} // }
lifecycleScope.launch { // lifecycleScope.launch {
viewModel.currentUserInfo.collect { // viewModel.currentUserInfo.collect {
if (it == null) return@collect // if (it == null) return@collect
it.showBalanceNotEnoughDialog = true //// it.showBalanceNotEnoughDialog = true
handleLoginSuccess(it, false) // handleLoginSuccess(it, false)
} // }
} // }
lifecycleScope.launch { // lifecycleScope.launch {
settingViewModel.showLoading.collect { // settingViewModel.showLoading.collect {
Timber.d("registerDataChange 管理员 showLoading = $it") // Timber.d("registerDataChange 管理员 showLoading = $it")
if (it) { // if (it) {
showWaitingDialog("") // showWaitingDialog("")
} else { // } else {
hideWaitingDialog() // hideWaitingDialog()
} // }
} // }
} // }
lifecycleScope.launch { // lifecycleScope.launch {
settingViewModel.searchUserInfo.collect { // settingViewModel.searchUserInfo.collect {
Timber.d("registerDataChange 管理员 currentUserInfo = $it") // Timber.d("registerDataChange 管理员 currentUserInfo = $it")
if (it == null) return@collect // if (it == null) return@collect
handleLoginSuccess(it, true) // handleLoginSuccess(it, true)
} // }
} // }
// lifecycleScope.launch { // lifecycleScope.launch {
// settingViewModel.equipmentList.collect { // settingViewModel.equipmentList.collect {
// loadEquipmentList(it) // loadEquipmentList(it)
@@ -307,79 +331,79 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
// } // }
} }
private fun loadEquipmentList(items: List<EquipmentUserInfo>) { // private fun loadEquipmentList(items: List<EquipmentUserInfo>) {
if (items.isEmpty()) return // if (items.isEmpty()) return
val unbindList = items.filter { !it.isBound() } // val unbindList = items.filter { !it.isBound() }
if (unbindList.isEmpty()) { // if (unbindList.isEmpty()) {
MainActivity.start(context = context, pageType = PageType.PLATE_CABINET_FULL) // MainActivity.start(context = context, pageType = PageType.PLATE_CABINET_FULL)
} else { // } else {
val firstInfo = unbindList[0] // val firstInfo = unbindList[0]
val currentUserInfo = settingViewModel.searchUserInfo.value // val currentUserInfo = settingViewModel.searchUserInfo.value
firstInfo.plateNumber = currentUserInfo?.plateNumber ?: "" // firstInfo.plateNumber = currentUserInfo?.plateNumber ?: ""
MainActivity.start( // MainActivity.start(
context = context, // context = context,
pageType = PageType.BIND_PLATE, // pageType = PageType.BIND_PLATE,
equipmentUserInfo = firstInfo // equipmentUserInfo = firstInfo
) // )
} // }
} // }
/** /**
* 处理登录成功操作 * 处理登录成功操作
* @param isAdmin true 管理员 执行绑盘此操作 false 用户,提示错误 * @param isAdmin true 管理员 执行绑盘此操作 false 用户,提示错误
*/ */
open fun handleLoginSuccess(equipmentUserInfo: EquipmentUserInfo, isAdmin: Boolean) { open fun handleLoginSuccess(equipmentUserInfo: EquipmentUserInfo, isAdmin: Boolean) {
Timber.d("handleLoginSuccess isAdmin = $isAdmin") // Timber.d("handleLoginSuccess isAdmin = $isAdmin")
equipmentUserInfo.isIntercept = false // equipmentUserInfo.isIntercept = false
val cardBalance = equipmentUserInfo.cardBalance ?: 0.toDouble() // val cardBalance = equipmentUserInfo.cardBalance ?: 0.toDouble()
val balanceIsNotEnough = cardBalance <= 0.toDouble() // val balanceIsNotEnough = cardBalance <= 0.toDouble()
if (balanceIsNotEnough && equipmentUserInfo.showBalanceNotEnoughDialog) { // if (balanceIsNotEnough && equipmentUserInfo.showBalanceNotEnoughDialog) {
//提示余额不足弹窗 // //提示余额不足弹窗
equipmentUserInfo.isIntercept = true // equipmentUserInfo.isIntercept = true
BalanceNotEnoughDialog(this).show() // BalanceNotEnoughDialog(this).show()
return
}
viewModel.resetUserInfo()
// if (equipmentUserInfo.isOtherEquipment()) {
// MainActivity.start(
// context,
// pageType = PageType.PLATE_TIP,
// equipmentUserInfo = equipmentUserInfo,
// isAdmin
// )
// return // return
// } // }
if (equipmentUserInfo.equipmentBoxCode?.isNotEmpty() == true) { // viewModel.resetUserInfo()
SerialApi.openPlate( //// if (equipmentUserInfo.isOtherEquipment()) {
equipmentUserInfo.equipmentBoxCode!!.toInt(), //// MainActivity.start(
object : SerialPortManager.SendCallback { //// context,
override fun onSuccess() { //// pageType = PageType.PLATE_TIP,
MainActivity.start( //// equipmentUserInfo = equipmentUserInfo,
context, //// isAdmin
pageType = PageType.PLATE_TIP, //// )
isAdmin = isAdmin //// return
) //// }
} // if (equipmentUserInfo.equipmentBoxCode?.isNotEmpty() == true) {
// SerialApi.openPlate(
override fun onFail(e: Exception?) { // equipmentUserInfo.equipmentBoxCode!!.toInt(),
ToastUtils.showToast("柜门打开失败") // object : SerialPortManager.SendCallback {
} // override fun onSuccess() {
}) // MainActivity.start(
} else { // context,
if (isAdmin) { // pageType = PageType.PLATE_TIP,
settingViewModel.getEquipmentList { // isAdmin = isAdmin
loadEquipmentList(it) // )
} // }
} else { //
Timber.d("无餐盘信息") // override fun onFail(e: Exception?) {
MainActivity.start( // ToastUtils.showToast("柜门打开失败")
context, // }
pageType = PageType.PLATE_TIP, // })
equipmentUserInfo = equipmentUserInfo, // } else {
isAdmin = isAdmin // if (isAdmin) {
) // settingViewModel.getEquipmentList {
} // loadEquipmentList(it)
} // }
// } else {
// Timber.d("无餐盘信息")
// MainActivity.start(
// context,
// pageType = PageType.PLATE_TIP,
// equipmentUserInfo = equipmentUserInfo,
// isAdmin = isAdmin
// )
// }
// }
} }
override fun onDestroy() { override fun onDestroy() {
@@ -394,45 +418,129 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
// private val initialDelay = 5 * 60 * 1000L // private val initialDelay = 5 * 60 * 1000L
// private val dealyMillis = 10 * 60 * 1000L // private val dealyMillis = 10 * 60 * 1000L
private val initialDelay = 5 * 60 * 1000L private val initialDelay = 1 * 1000L
private val dealyMillis = 5 * 60 * 1000L private val dealyMillis = 10 * 1000L
fun startFaceTask(block: (List<String?>) -> Unit) {
val recognizeViewModel by viewModels<RecognizeViewModel>()
fun startFaceTask() {
faceTaskJob = faceTaskJob =
intervalExecutor.startIntervalTaskWithInitialDelay(initialDelay, dealyMillis) { intervalExecutor.startIntervalTaskWithInitialDelay(initialDelay, dealyMillis) {
getFaceIncrementList() getFaceIncrementList(action = block)
} }
} }
/** private var faceTimestamp = 0L
* 人脸增量同步(V2,同步逻辑在 NetViewModelV2 内部处理)
*/ //private var taskPageNo = 1
private fun getFaceIncrementList(pageNo: Int = 1) { private fun getFaceIncrementList(pageNo: Int = 1, action:(List<String?>)-> Unit) {
val timestamp = SpTool.getLastFaceTimestamp() val timestamp = SpTool.lastFaceTimestamp
if (timestamp == 0L) { // if (timestamp == 0L) {
return // return
} // }
netViewModelV2.getFaceIncrementList( settingViewModel.getFaceIncrementList(
pageNo = pageNo, pageNo = pageNo,
timestamp = timestamp timestamp = timestamp
) { ) { list ->
recognizeViewModel.refreshFaceList() runOnUiThread {
if (pageNo == 1 && list.isEmpty()) {
//未查询到增量数据
// Thread{
// val faceDao = FaceDatabase.getInstance(this).faceDao()
// if (faceDao.getFaceCount() == 0) {
// FaceEntity("-1", null, byteArrayOf(-1)).also { it.userType = "-1" }
// }
// }.start()
return@runOnUiThread
}
updateFaceData(list)
val userIdList = list.map { it.userId }
action(userIdList)
//if (list.size >= settingViewModel.PAGE_SIZE) {
// faceTimestamp = list.last().faceUpdateTimestamp ?: 0
// taskPageNo++
// getFaceIncrementList(taskPageNo)
// return@runOnUiThread
//}
if (list.isNotEmpty()) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0
}
SpTool.lastFaceTimestamp = faceTimestamp
}
} }
} }
/** public val recognizeViewModel by viewModels<RecognizeViewModel>()
* 清空本地人脸库 private fun updateFaceData(list: List<UserFaceModel>) {
*/
fun clearAllFace(block: () -> Unit) {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val faceList = mutableListOf<FaceEntity>()
val faceDao = FaceDatabase.getInstance(this@BaseActivity).faceDao() val faceDao = FaceDatabase.getInstance(this@BaseActivity).faceDao()
try {
list.forEach { model ->
if (model.faceDeleted == true) {
//删除数据
faceDao.deleteFaceById(model.userId)
// if (faceDao.getFaceCount() == 0) {
// FaceEntity("-1", null, byteArrayOf(-1)).also { it.userType = "-1" }
// }
} else {
// 获取该用户所有已存特征,判断当前特征是否已存在
val existList = faceDao.queryAllByUserName(model.userId)
val featureMatches = existList.map { Base64.encode(it.featureData) == model.faceFeatureStr }
Timber.d("userId=${model.userId}, 已存记录数=${existList.size}, 各条特征是否与服务器一致=$featureMatches")
val alreadyExists = featureMatches.any { it }
if (!alreadyExists) {
//当前特征不存在,保存
faceList.add(getNewFaceEntity(model))
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
try {
if (faceList.isNotEmpty()) {
FaceDatabase.getInstance(this@BaseActivity).faceDao().insert(faceList)
}
recognizeViewModel.refreshFaceList()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private fun getNewFaceEntity(model: UserFaceModel) : FaceEntity {
return FaceEntity(
model.userId,
null,
Base64.decode(model.faceFeatureStr)
).also {
//1-会员、2-临时用户
it.userType = if (model.member) "1" else "2"
it.cardNo = model.cardNo
}
}
fun initPlateList() {
// SpTool.putString(SpTool.PLATE_BOX_DATA, "")
// var list = SpTool.getPlateData()
val list: MutableList<EquipmentUserInfo> = mutableListOf()
repeat(22) { index ->
list.add(
EquipmentUserInfo(equipmentBoxCode = "${index + 1}")
)
}
MyApp.plateList = list
}
fun clearAllFace(block: () -> Unit) {
Thread {
val faceDao = FaceDatabase.getInstance(App.getContext()).faceDao()
faceDao.deleteAll() faceDao.deleteAll()
faceDao.resetId() faceDao.resetId()
// faceDao.insert(
// FaceEntity("-1", null, byteArrayOf(-1)).also { it.userType = "-1" }
// )
recognizeViewModel.refreshFaceList() recognizeViewModel.refreshFaceList()
withContext(Dispatchers.Main) { block() } runOnUiThread { block() }
} }.start()
} }
} }
@@ -1,11 +1,16 @@
package com.sw.platecabinet.activity package com.sw.platecabinet.activity
import android.content.Intent import android.content.Intent
import androidx.lifecycle.lifecycleScope
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.FaceApi
import com.sw.platecabinet.GlobalData import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.member.databinding.ActivityDeviceInitBinding import com.sw.platecabinet.databinding.ActivityDeviceInitBinding
import com.sw.platecabinet.member.databinding.ItemTitleTimeBinding import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.utils.SpTool import com.sw.platecabinet.utils.SpTool
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/** /**
* 设备初始化界面 * 设备初始化界面
@@ -21,27 +26,50 @@ class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
} }
override fun initialize() { override fun initialize() {
// val isSuccess = deviceViewModel.checkEquipmentInfo()
// if (isSuccess) {
// goLoginActivity()
// return
// }
// binding.ivQrCode.setImageBitmap(
// QRCodeUtil.generateQRCode(
// content = GlobalData.deviceId,
// size = 200
// )
// )
// binding.btnInit.setOnClickListener {
// deviceViewModel.getDeviceToken()
// }
// binding.btnInit.gone()
// binding.ivQrCode.invisible()
// binding.root.postDelayed({
// goLoginActivity()
// }, 1000)
initPlateList()
showWaitingDialog("加载中……") showWaitingDialog("加载中……")
// 首次启动先全量拉取人脸缓存,后续走增量同步 lifecycleScope.launch {
if (SpTool.getFirstGetFace()) { withContext(Dispatchers.Default) {
netViewModelV2.getUserFaceCache { status, msg -> //val count = FaceApi().queryFaceCount()
SpTool.setFirstGetFace(false) //if (count == 0) {
getDeviceConfig() if (SpTool.firstGetFace) {
userViewModel.getUserFaceCache { status, msg ->
SpTool.firstGetFace = false
getDeviceConfig()
}
} else {
getDeviceConfig()
}
} }
} else {
getDeviceConfig()
} }
} }
/**
* 获取设备配置(V2),并初始化虹软 SDK 激活参数
*/
private fun getDeviceConfig() { private fun getDeviceConfig() {
netViewModelV2.getDeviceConfig(onSuccess = { deviceConfig -> userViewModel.getDeviceConfig { deviceConfig ->
runOnUiThread { runOnUiThread {
if (deviceConfig == null) { if (deviceConfig == null) {
hideWaitingDialog() hideWaitingDialog()
ToastUtils.showToast("获取设备配置数据失败") ToastUtils.showToast("获取设备配置数据失败")
hideWaitingDialog()
goLoginActivity() goLoginActivity()
return@runOnUiThread return@runOnUiThread
} }
@@ -53,13 +81,7 @@ class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
goLoginActivity() goLoginActivity()
}, 500) }, 500)
} }
}, onFailure = { errMsg -> }
runOnUiThread {
hideWaitingDialog()
ToastUtils.showToast(errMsg ?: "接口异常")
goLoginActivity()
}
})
} }
override fun registerDataChange() { override fun registerDataChange() {
@@ -0,0 +1,152 @@
package com.sw.platecabinet.activity
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import com.sw.plate.utils.Base64
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.facedb.FaceDatabase
import com.sw.platecabinet.MyApp
import com.sw.platecabinet.databinding.ActivityFunBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.dialog.CustomDialog
import com.sw.platecabinet.ext.clickWithDebounce
import com.sw.platecabinet.utils.PlateUtils
import com.sw.platecabinet.utils.SpTool
import com.sw.platecabinet.viewmodel.UserViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.getValue
class FunActivity : BaseActivity<ActivityFunBinding>() {
override fun inflateTitleBinding(): ItemTitleTimeBinding? {
return binding.includeHeader
}
override fun inflateViewBinding() = ActivityFunBinding.inflate(layoutInflater)
override fun initialize() {
binding.btnClearFace.clickWithDebounce { clearFace() }
binding.btnClearAndGetAll.clickWithDebounce { clearFaceAndGetAll() }
binding.btnOpenAllPlate.clickWithDebounce {
lifecycleScope.launch {
openAllPlate()
}
}
binding.btnPutPlate.clickWithDebounce { putPlate() }
queryUserCount()
binding.btnTestFeature.clickWithDebounce {
testFeature()
}
}
private fun testFeature() {
userViewModel.getUserFace(1, 1) {list ->
lifecycleScope.launch {
withContext(Dispatchers.IO) {
if (list.isEmpty()) return@withContext
val userId = list[0].userId
val base64Feature = list[0].faceFeatureStr
val faceDao = FaceDatabase.getInstance(this@FunActivity).faceDao()
val entityList = faceDao.queryAllByUserName(userId)
val resultList = entityList.map { base64Feature == Base64.encode(it.featureData) }
val isEqual = resultList.any { true }
ToastUtils.showToast("比较结果:$isEqual")
}
}
}
}
private fun clearFace() {
loadClearDialog("请确认是否清除本地所有数据?") {
showWaitingDialog("加载中……")
clearAllFace {
binding.root.postDelayed(
{
hideWaitingDialog()
ToastUtils.showToast("已清除")
queryUserCount()
}, 500
)
}
}
}
private fun clearFaceAndGetAll() {
loadClearDialog("请确认是否清除本地所有数据,并重新拉取全量人脸数据?") {
showWaitingDialog("加载中……")
clearAllFace {
userViewModel.getUserFaceCache { status, msg ->
hideWaitingDialog()
ToastUtils.showToast(
if (status) "请求成功,$msg" else "请求失败,$msg"
)
queryUserCount()
}
}
}
}
private suspend fun openAllPlate() {
MyApp.plateList?.let { plateList ->
val boxCodeList = plateList.filter { it.equipmentBoxCode.isNullOrBlank().not() }
.map { it.equipmentBoxCode }
boxCodeList.forEach { boxCode ->
withContext(Dispatchers.Default) {
PlateUtils.openDirect(boxCode)
delay(500)
}
}
}
}
private fun putPlate() {
showWaitingDialog("加载中……")
MyApp.plateList?.forEachIndexed { index, item ->
val num = index + 1
val realNum = if (num < 10) "0${num}" else "${num}"
item.plateNumber = realNum + realNum + realNum
item.faceId = null
item.updateTime = null
}
SpTool.savePlateData(MyApp.plateList!!)
binding.root.postDelayed({
hideWaitingDialog()
ToastUtils.showToast("已完成")
}, 1000)
}
/**
* 加载清除数据弹窗
*/
private fun loadClearDialog(msg: String, callback: () -> Unit) {
CustomDialog(
activity = this,
content = msg,
onConfirm = {
callback()
}).show()
}
override fun onLeftDoubleClick() {
finish()
}
override fun onRightDoubleClick() {
}
private fun queryUserCount() {
lifecycleScope.launch {
val faceDao = FaceDatabase.getInstance(this@FunActivity).faceDao()
val memberUserCount = withContext(Dispatchers.IO) { faceDao.getFaceCount("1") }
val tempUserCount = withContext(Dispatchers.IO) { faceDao.getFaceCount("2") }
binding.tvMemberUserCount.text = "会员用户:${memberUserCount}"
binding.tvTempUserCount.text = "临时用户:${tempUserCount}"
}
}
}
@@ -1,10 +1,11 @@
package com.sw.platecabinet.activity package com.sw.platecabinet.activity
import android.util.Log
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.sw.plate.utils.arcface.FaceApi import com.sw.plate.utils.arcface.FaceApi
import com.sw.plate.utils.arcface.faceserver.FaceServer import com.sw.plate.utils.arcface.faceserver.FaceServer
import com.sw.platecabinet.member.databinding.ActivityInitBinding import com.sw.platecabinet.databinding.ActivityInitBinding
import com.sw.platecabinet.member.databinding.ItemTitleTimeBinding import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -21,7 +22,8 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
} }
override fun initialize() { override fun initialize() {
binding.main.setOnClickListener { Log.d("InitActivity", "collectFace,initialize: ")
binding.takeButton.setOnClickListener {
finish() finish()
} }
// binding.takeButton.setOnClickListener { // binding.takeButton.setOnClickListener {
@@ -1,19 +1,25 @@
@file:Suppress("DEPRECATION")
package com.sw.platecabinet.activity package com.sw.platecabinet.activity
import android.Manifest import android.Manifest
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Point import android.graphics.Point
import android.hardware.Camera import android.hardware.Camera
import android.os.Build import android.os.Build
import android.os.CountDownTimer
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
import androidx.activity.viewModels
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.lifecycle.Observer import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.arcsoft.face.ErrorInfo import com.arcsoft.face.ErrorInfo
import com.bumptech.glide.Glide
import com.google.gson.Gson
import com.sw.inbound.utils.DateTimeUtils
import com.sw.plate.utils.Base64
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.ConfigUtil import com.sw.plate.utils.arcface.ConfigUtil
import com.sw.plate.utils.arcface.ErrorCodeUtil import com.sw.plate.utils.arcface.ErrorCodeUtil
@@ -24,30 +30,48 @@ import com.sw.plate.utils.arcface.PreviewConfig
import com.sw.plate.utils.arcface.camera.CameraListener import com.sw.plate.utils.arcface.camera.CameraListener
import com.sw.plate.utils.arcface.camera.DualCameraHelper import com.sw.plate.utils.arcface.camera.DualCameraHelper
import com.sw.plate.utils.arcface.face.constants.LivenessType import com.sw.plate.utils.arcface.face.constants.LivenessType
import com.sw.plate.utils.arcface.face.constants.RecognizeColor
import com.sw.plate.utils.arcface.face.model.CompareResult import com.sw.plate.utils.arcface.face.model.CompareResult
import com.sw.plate.utils.arcface.face.model.FacePreviewInfo import com.sw.plate.utils.arcface.face.model.FacePreviewInfo
import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration
import com.sw.plate.utils.arcface.faceserver.FaceServer import com.sw.plate.utils.arcface.facedb.FaceDatabase
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import com.sw.plate.utils.arcface.model.UserFaceInfo
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import com.sw.platecabinet.utils.Debouncer import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel.REGISTER_STATUS_READY
import com.sw.platecabinet.member.R import com.sw.platecabinet.MyApp
import com.sw.platecabinet.member.databinding.ActivityLoginFaceBinding import com.sw.platecabinet.R
import com.sw.platecabinet.member.databinding.ItemTitleTimeBinding import com.sw.platecabinet.databinding.ActivityLoginFaceBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.ext.clickWithDebounce
import com.sw.platecabinet.ext.gone
import com.sw.platecabinet.ext.visible
import com.sw.platecabinet.model.response.EquipmentUserInfo import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.network.task.TaskManager import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.utils.BitmapSaver
import com.sw.platecabinet.utils.CountDownUtil
import com.sw.platecabinet.utils.Debouncer
import com.sw.platecabinet.utils.LogFileUtil
import com.sw.platecabinet.utils.PermissionHelper import com.sw.platecabinet.utils.PermissionHelper
import com.sw.platecabinet.utils.PlateUtils
import com.sw.platecabinet.utils.RegisterCallbackHandler
import com.sw.platecabinet.utils.SoundPoolUtil
import com.sw.platecabinet.utils.SpTool
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import timber.log.Timber import timber.log.Timber
import java.io.File
import java.util.concurrent.atomic.AtomicBoolean
/** /**
* 人脸识别 * 人脸识别
*/ */
class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(), class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
ViewTreeObserver.OnGlobalLayoutListener { ViewTreeObserver.OnGlobalLayoutListener {
private var countDownTimer: CountDownTimer? = null
private val CAMERA_PERMISSION_REQUEST_CODE = 100 private val CAMERA_PERMISSION_REQUEST_CODE = 100
private val REQUIRED_PERMISSIONS: Array<String> = arrayOf( private val REQUIRED_PERMISSIONS: Array<String> = arrayOf(
@@ -65,44 +89,57 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
} }
companion object { companion object {
private var instance: LoginByFaceActivity? = null private const val TAG = "LoginByFaceActivity"
fun goInitActivity() { private const val COLLECT_SUCCESS = "collect_success"
instance?.goInitActivity() private const val RECOGNIZE_SUCCESS = "recognize_success"
} private const val NO_PLATE_REMIND = "no_plate_remind"
} }
override fun initialize() { override fun initialize() {
instance = this userViewModel.activeEngine()
// V2 引擎激活(appId/sdkKey/activeKey 来自 V2 设备配置)
netViewModelV2.activeEngine()
initCountTime()
initArcViewModel() initArcViewModel()
initArcView() initArcView()
openRectInfoDraw = true openRectInfoDraw = true
recognizeViewModel.setDrawRectInfoTextValue(true) recognizeViewModel.setDrawRectInfoTextValue(true)
//开启人脸增量数据定时任务(V2 EventBus.getDefault().register(this)
startFaceTask()
binding.llToPwd.setOnClickListener { //开启人脸增量数据定时任务
val intent = Intent(this, LoginByPwdActivity::class.java) startFaceTask {
startActivity(intent) loadLogInfo("collectFace增量接口数据用户id:${Gson().toJson(it)}")
} }
// TODO: 测试请求 val soundMap = hashMapOf<String, Int>()
TaskManager.startTask() soundMap[COLLECT_SUCCESS] = R.raw.collect_success
soundMap[RECOGNIZE_SUCCESS] = R.raw.recognize_success
soundMap[NO_PLATE_REMIND] = R.raw.no_plate_remind
SoundPoolUtil.getInstance().loadR(this, soundMap)
EventBus.getDefault().register(this) binding.btnCollect.clickWithDebounce {
//先重置,后采集
resetRecognizeState()
//采集数据按钮
retryFailCount = 0
isCollectFace = true
isCollecting.set(true)
collectFace()
}
binding.btnRetry.clickWithDebounce {
//重新识别按钮
resetRecognizeState()
}
} }
private fun checkCameraPermission() { private fun checkCameraPermission() {
Timber.i("checkCameraPermission") Timber.i("checkCameraPermission")
val permissionHelper = val permissionHelper =
PermissionHelper.with(this, REQUIRED_PERMISSIONS, CAMERA_PERMISSION_REQUEST_CODE) PermissionHelper.with(this, REQUIRED_PERMISSIONS, CAMERA_PERMISSION_REQUEST_CODE)
.onGranted { .onGranted {
Timber.d("checkCameraPermission 权限已允许") loadLogInfo("checkCameraPermission 权限已允许")
// 权限已授予 // 权限已授予
recognizeViewModel.init() recognizeViewModel.init()
initRgbCamera() initRgbCamera()
@@ -130,11 +167,51 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
private val livenessType = LivenessType.RGB private val livenessType = LivenessType.RGB
private var openRectInfoDraw = false private var openRectInfoDraw = false
private val leftDebouncer = Debouncer(30 * 1000)
/** 识别超时倒计时(15秒无操作跳回初始页) */
private val recognizeCountDownUtil = CountDownUtil()
/** 用户离开画面后的倒计时(llCollectButton 显示状态下无人脸时触发) */
private val leftCountDownUtil = CountDownUtil()
private var registerHandler: RegisterCallbackHandler? = null
private fun isFitData(userFaceInfo: UserFaceInfo?): Boolean {
if (userFaceInfo == null) return false
val headBmp = userFaceInfo.headBmp
if (headBmp == null) return false
// if (headBmp.width <= 200 || headBmp.height <= 200) {
// loadLogInfo("collectFace,isFitData:拿到人脸提取特征数据照片过小-----------")
// return false
// }
return true
}
private fun initArcViewModel() { private fun initArcViewModel() {
recognizeViewModel.setLiveType(livenessType) recognizeViewModel.setLiveType(livenessType)
recognizeViewModel.prepareRegister()
recognizeViewModel.ftInitCode.observe(this, Observer { ftInitCode: Int? -> recognizeViewModel.setOnRegisterFinishedCallback { facePreviewInfo, userFaceInfo ->
registerHandler?.setCallback {
if (!isCollectFace || !isCollecting.get()) {
return@setCallback
}
if (this.userFaceInfo == null && isFitData(userFaceInfo)) {
val headBmp = userFaceInfo.headBmp
if (headBmp.width <= 200 || headBmp.height <= 200) {
loadLogInfo("collectFace,isFitData:拿到人脸提取特征数据照片过小w:${headBmp.width},h:${headBmp.height}-----------")
binding.llCollectButton.visible()
binding.btnRetry.gone()
binding.btnCollect.text = "重新采集"
return@setCallback
}
recognizeViewModel.updateRegisterStatus(RecognizeViewModel.REGISTER_STATUS_DONE)
this.userFaceInfo = userFaceInfo
getFaceDataBlock()
}
}
}
recognizeViewModel.ftInitCode.observe(this) { ftInitCode: Int? ->
if (ftInitCode != ErrorInfo.MOK) { if (ftInitCode != ErrorInfo.MOK) {
val error: String? = getString( val error: String? = getString(
R.string.specific_engine_init_failed, "ftEngine", R.string.specific_engine_init_failed, "ftEngine",
@@ -143,8 +220,8 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
Timber.e("ftInitCode observe = $error") Timber.e("ftInitCode observe = $error")
ToastUtils.showToast(error) ToastUtils.showToast(error)
} }
}) }
recognizeViewModel.frInitCode.observe(this, Observer { frInitCode: Int? -> recognizeViewModel.frInitCode.observe(this) { frInitCode: Int? ->
if (frInitCode != ErrorInfo.MOK) { if (frInitCode != ErrorInfo.MOK) {
val error: String? = getString( val error: String? = getString(
R.string.specific_engine_init_failed, "frEngine", R.string.specific_engine_init_failed, "frEngine",
@@ -153,8 +230,8 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
Timber.e("frInitCode observe = $error") Timber.e("frInitCode observe = $error")
ToastUtils.showToast(error) ToastUtils.showToast(error)
} }
}) }
recognizeViewModel.flInitCode.observe(this, Observer { flInitCode: Int? -> recognizeViewModel.flInitCode.observe(this) { flInitCode: Int? ->
if (flInitCode != ErrorInfo.MOK) { if (flInitCode != ErrorInfo.MOK) {
val error: String? = getString( val error: String? = getString(
R.string.specific_engine_init_failed, "flEngine", R.string.specific_engine_init_failed, "flEngine",
@@ -163,83 +240,196 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
Timber.e("flInitCode observe = $error") Timber.e("flInitCode observe = $error")
ToastUtils.showToast(error) ToastUtils.showToast(error)
} }
}) }
recognizeViewModel.recognizeConfiguration recognizeViewModel.recognizeConfiguration
.observe(this, Observer { recognizeConfiguration: RecognizeConfiguration? -> .observe(this) { recognizeConfiguration: RecognizeConfiguration? ->
Timber.i("recognizeConfiguration: observe = ${recognizeConfiguration.toString()}") Timber.i("recognizeConfiguration: observe = ${recognizeConfiguration.toString()}")
}) }
recognizeViewModel.recognizeNotice.observe(this, Observer { notice: String? -> recognizeViewModel.recognizeNotice.observe(this) { notice: String? ->
Timber.i("recognizeNotice observe notice = $notice") Timber.i("recognizeNotice observe notice = $notice")
}) }
recognizeViewModel.recognizeUserId.observe(this, Observer { result: CompareResult? -> recognizeViewModel.recognizeUserId.observe(this) { result: CompareResult? ->
Timber.i( loadLogInfo("collectFace,recognizeUserId observe userId = ${result?.faceEntity?.userName}")
"recognizeUserId observe userId=${result?.faceEntity?.userName}," + loadFaceRecognizeResult3(result)
"similar=${result?.similar},userType=${result?.faceEntity?.userType}" }
)
loadFaceRecognizeResult(result)
})
recognizeViewModel.drawRectInfoText.observe(this, Observer { info -> recognizeViewModel.drawRectInfoText.observe(this) { info ->
Timber.i("drawRectInfoText observe info = $info") Timber.i("drawRectInfoText observe info = $info")
}) }
}
private fun initArcView() {
//在布局结束后才做初始化操作
binding.dualCameraTexturePreviewRgb.getViewTreeObserver().addOnGlobalLayoutListener(this)
recognizeViewModel.getCompareResultList().getValue()
} }
/** /**
* 识别阈值,默认值为0.8 * 识别失败重试次数
*/
private var retryFailCount = 0
/**
* 识别失败最大次数
*/
private val maxFailCount = 5
/**
* 配置阈值,默认值为0.8
*/ */
private val faceSuccessThreshold by lazy { ConfigUtil.getRecognizeThreshold(this) } private val faceSuccessThreshold by lazy { ConfigUtil.getRecognizeThreshold(this) }
/** private val openPlateDebouncer = Debouncer(5000)
* 识别成功后开柜(查询用户信息)防抖,防止重复触发 private val isCollecting = AtomicBoolean(false)
*/
private val loginDebouncer = Debouncer(3000)
/** private fun loadFaceRecognizeResult3(result: CompareResult?) {
* 处理识别结果:达到阈值走登录流程,未达阈值仅记录(本版本无采集模式)
*/
private fun loadFaceRecognizeResult(result: CompareResult?) {
val similar = result?.similar ?: return val similar = result?.similar ?: return
Timber.i( binding.tvSimilarValue.text = "$similar${retryFailCount}"
"loadFaceRecognizeResult 阈值=$faceSuccessThreshold, similar=$similar" loadLogInfo(
"collectFace,recognizeUserId 阈值 success=$faceSuccessThreshold, similar=$similar, retry=$retryFailCount"
) )
if (similar >= faceSuccessThreshold) { if (similar >= faceSuccessThreshold) {
val userId = result.faceEntity?.userName val userId = result.faceEntity?.userName
recognizeSuccess(userId) recognizeSuccess(userId)
return return
} }
// 未达阈值:暂不处理(无采集模式),等待下一次识别
if (isCollectFace || isCollecting.get()) {
loadLogInfo("collectFace,recognizeUserId 正在采集中,忽略失败结果 similar=$similar")
return
}
if (retryFailCount >= maxFailCount) {
loadLogInfo(
"collectFace,recognizeUserId 识别失败,开始采集 similar=$similar, " +
"retry=$retryFailCount, time=${DateTimeUtils.getDateTimeString()}"
)
if (!isCollecting.compareAndSet(false, true)) {
loadLogInfo("collectFace 已在采集中,忽略重复触发")
return
}
recognizeFail()
return
}
retryFailCount++
loadLogInfo("collectFace,recognizeUserId 中间态重试,retryFailCount=$retryFailCount")
}
private fun recognizeSuccess(userId: String?) {
if (userId.isNullOrBlank()) {
loadLogInfo("collectFace,recognizeUserId 识别成功但 userId 为空,忽略")
return
}
loadLogInfo(
"collectFace,recognizeUserId 识别成功 userId=$userId, time=${DateTimeUtils.getDateTimeString()}"
)
// 成功后必须重置状态
retryFailCount = 0
isCollecting.set(false)
isCollectFace = false
currentUserId = userId
// 防抖,防止重复开闸
openPlateDebouncer.debounce {
openPlate(userId)
}
}
private fun recognizeFail() {
recognizeCountDownUtil.cancelCountDown()
binding.tvToInit.text = ""
binding.tvSimilarValue.text = ""
binding.llCollectButton.visible()
binding.btnRetry.visible()
binding.btnCollect.run {
visible()
text = "新用户采集"
}
binding.ivCollectPhoto.visible()
}
private fun newUserCollect() {
recognizeCountDownUtil.cancelCountDown()
binding.tvToInit.text = ""
binding.tvSimilarValue.text = ""
binding.llCollectButton.visible()
binding.btnRetry.gone()
binding.btnCollect.run {
visible()
text = "新用户采集"
}
binding.ivCollectPhoto.gone()
resumeCamera()
} }
/** /**
* 识别成功:防抖后查询用户信息,走开柜门业务流程 * 查询餐盘数据
*/ */
private fun recognizeSuccess(userId: String?) { private fun getPlateData(block: (List<EquipmentUserInfo>) -> Unit) {
if (userId.isNullOrBlank()) { MyApp.plateList?.let {
Timber.i("recognizeSuccess 识别成功但 userId 为空,忽略") block(it)
return
}
Timber.i("recognizeSuccess userId=$userId")
loginDebouncer.debounce {
viewModel.getUserInfoById(memberId = userId)
} }
} }
override fun onDestroy() { private var currentPlateInfo: EquipmentUserInfo? = null
Timber.d("onDestroy") private var plateEnable = false
instance = null
if (rgbCameraHelper != null) { /**
rgbCameraHelper!!.release() * 打开餐盘柜
rgbCameraHelper = null */
private fun openPlate(userId: String?) {
if (plateEnable.not()) return
loadLogInfo("collectFace,openPlate: userId=$userId")
getPlateData { items ->
loadLogInfo("collectFace,openPlate: items:${Gson().toJson(items)}")
// 未查询到当前用户,查询第一个未绑定的餐盘
val item = items.firstOrNull { it.plateNumber.isNullOrEmpty().not() }
loadLogInfo("collectFace,openPlate: item2:${item}")
if (item == null) {
//暂无餐盘
loadLogInfo("collectFace,openPlate: 暂无餐盘,请联系管理人员,isCollectFace=$isCollectFace")
//ToastUtils.showToast("暂无餐盘,请联系管理人员")
SoundPoolUtil.getInstance().play(NO_PLATE_REMIND, 0)
goInitActivity()
return@getPlateData
}
// if (tempUserId != currentUserId) {
SoundPoolUtil.getInstance().play(RECOGNIZE_SUCCESS, 0)
// } else {
// SoundPoolUtil.getInstance().play(COLLECT_SUCCESS, 0)
// }
loadLogInfo("collectFace,openPlate: item3:${item}")
item.faceId = userId
item.updateTime = DateTimeUtils.getDateTimeString()
item.plateNumber = null
plateEnable = false
openDebouncer.debounce {
//打开柜子
PlateUtils.open(this, item.equipmentBoxCode, false) {
goInitActivity()
}
recognizeCountDownUtil.cancelCountDown()
}
currentPlateInfo = item
MyApp.plateList = items
SpTool.savePlateData(items)
} }
}
private val openDebouncer by lazy { Debouncer(5000) }
private fun initArcView() {
//在布局结束后才做初始化操作
binding.dualCameraTexturePreviewRgb.viewTreeObserver.addOnGlobalLayoutListener(this)
val list = recognizeViewModel.getCompareResultList().getValue()
loadLogInfo("initArcView:list.size=${list?.size}")
}
override fun onDestroy() {
loadLogInfo("onDestroy")
rgbCameraHelper?.release()
rgbCameraHelper = null
recognizeViewModel.destroy() recognizeViewModel.destroy()
@@ -282,7 +472,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
layoutParams.width = (measuredHeight * ratio).toInt() layoutParams.width = (measuredHeight * ratio).toInt()
} }
if (scale < 1f) { if (scale < 1f) {
val rgbParam = rgbPreview.getLayoutParams() val rgbParam = rgbPreview.layoutParams
layoutParams.width = (rgbParam.width * scale).toInt() layoutParams.width = (rgbParam.width * scale).toInt()
layoutParams.height = (rgbParam.height * scale).toInt() layoutParams.height = (rgbParam.height * scale).toInt()
} else { } else {
@@ -317,17 +507,18 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
displayOrientation: Int, displayOrientation: Int,
isMirror: Boolean isMirror: Boolean
) { ) {
Timber.d("initRgbCamera Rgb onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation") loadLogInfo("initRgbCamera Rgb onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation")
runOnUiThread({ runOnUiThread {
val previewSizeRgb = camera.getParameters().getPreviewSize() val previewSizeRgb = camera.parameters.previewSize
val layoutParams = adjustPreviewViewSize( val layoutParams = adjustPreviewViewSize(
binding.dualCameraTexturePreviewRgb, binding.dualCameraTexturePreviewRgb,
binding.dualCameraTexturePreviewRgb, binding.dualCameraFaceRectView, binding.dualCameraTexturePreviewRgb, binding.dualCameraFaceRectView,
previewSizeRgb, displayOrientation, 0.7f previewSizeRgb, displayOrientation, 0.7f
//0.6f
) )
Timber.d("initRgbCamera previewSizeRgb = ${previewSizeRgb.width}-${previewSizeRgb.height}") loadLogInfo("initRgbCamera previewSizeRgb = ${previewSizeRgb.width}-${previewSizeRgb.height}")
Timber.d("initRgbCamera layoutParams = ${layoutParams.width}-${layoutParams.height}") loadLogInfo("initRgbCamera layoutParams = ${layoutParams.width}-${layoutParams.height}")
Timber.d( loadLogInfo(
"initRgbCamera isMirror = ${isMirror}, isDrawRgbRectHorizontalMirror = ${ "initRgbCamera isMirror = ${isMirror}, isDrawRgbRectHorizontalMirror = ${
ConfigUtil.isDrawRgbRectHorizontalMirror( ConfigUtil.isDrawRgbRectHorizontalMirror(
context context
@@ -352,7 +543,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
recognizeViewModel.onRgbCameraOpened(camera) recognizeViewModel.onRgbCameraOpened(camera)
recognizeViewModel.setRgbFaceRectTransformer(rgbFaceRectTransformer) recognizeViewModel.setRgbFaceRectTransformer(rgbFaceRectTransformer)
}) }
} }
@RequiresApi(api = Build.VERSION_CODES.Q) @RequiresApi(api = Build.VERSION_CODES.Q)
@@ -361,11 +552,22 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
return return
} }
binding.dualCameraFaceRectView.clearFaceInfo() binding.dualCameraFaceRectView.clearFaceInfo()
val facePreviewInfoList: MutableList<FacePreviewInfo?>? = facePreviewInfoList = recognizeViewModel.onPreviewFrame(nv21, true)
recognizeViewModel.onPreviewFrame(nv21, true)
if (facePreviewInfoList != null && rgbFaceRectTransformer != null) { if (facePreviewInfoList != null && rgbFaceRectTransformer != null) {
drawPreviewInfo(facePreviewInfoList) drawPreviewInfo(facePreviewInfoList)
} }
if (facePreviewInfoList.isNullOrEmpty()) {
currentPlateInfo = null
currentUserId = null
userFaceInfo = null
binding.btnCollect.tag = ""
binding.tvSimilarValue.text = ""
retryFailCount = 0
if (binding.llCollectButton.isVisible) {
leftDebouncer.debounce { leftCountDown() }
}
}
recognizeViewModel.clearLeftFace(facePreviewInfoList) recognizeViewModel.clearLeftFace(facePreviewInfoList)
} }
@@ -403,8 +605,8 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
.previewOn(binding.dualCameraTexturePreviewRgb) .previewOn(binding.dualCameraTexturePreviewRgb)
.cameraListener(cameraListener) .cameraListener(cameraListener)
.build() .build()
rgbCameraHelper!!.init() rgbCameraHelper?.init()
rgbCameraHelper!!.start() rgbCameraHelper?.start()
} }
/** /**
@@ -412,103 +614,170 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
* *
* @param facePreviewInfoList RGB画面的实时人脸信息 * @param facePreviewInfoList RGB画面的实时人脸信息
*/ */
private fun drawPreviewInfo(facePreviewInfoList: MutableList<FacePreviewInfo?>) { private fun drawPreviewInfo(facePreviewInfoList: MutableList<FacePreviewInfo>?) {
if (rgbFaceRectTransformer != null) { if (rgbFaceRectTransformer != null) {
val rgbDrawInfoList: MutableList<DrawInfo?>? = recognizeViewModel.getDrawInfo( val rgbDrawInfoList: MutableList<DrawInfo>? = recognizeViewModel.getDrawInfo(
facePreviewInfoList, facePreviewInfoList, LivenessType.RGB, openRectInfoDraw
LivenessType.RGB,
openRectInfoDraw
) )
// 识别成功 // 识别成功
binding.dualCameraFaceRectView.drawRealtimeFaceInfo(rgbDrawInfoList) binding.dualCameraFaceRectView.drawRealtimeFaceInfo(rgbDrawInfoList)
} }
} }
fun openRectInfoDraw(view: View?) { // fun openRectInfoDraw(view: View?) {
openRectInfoDraw = !openRectInfoDraw // openRectInfoDraw = !openRectInfoDraw
recognizeViewModel.setDrawRectInfoTextValue(openRectInfoDraw) // recognizeViewModel.setDrawRectInfoTextValue(openRectInfoDraw)
} // }
override fun onGlobalLayout() { override fun onGlobalLayout() {
Timber.d("onGlobalLayout") loadLogInfo("onGlobalLayout")
binding.dualCameraTexturePreviewRgb.getViewTreeObserver().removeOnGlobalLayoutListener(this) binding.dualCameraTexturePreviewRgb.viewTreeObserver.removeOnGlobalLayoutListener(this)
// 请求权限 // 请求权限
checkCameraPermission() checkCameraPermission()
} }
private fun resumeCamera() { private fun resumeCamera() {
val helper = rgbCameraHelper // isRecognition = true
if (helper != null && helper.isStopped) { if (rgbCameraHelper != null && rgbCameraHelper!!.isStopped) {
loadLogInfo("collectFace,resumeCamera: ")
isRecognition = true isRecognition = true
helper.start() rgbCameraHelper!!.start()
} else { } else {
// 相机未停止时,发送黑帧清除 ViewModel 内部识别缓存,防止恢复后使用旧数据 //recognizeViewModel.onPreviewFrame(ByteArray(1382400), true)
recognizeViewModel.onPreviewFrame(emptyFrame, true) recognizeViewModel.onPreviewFrame(emptyFrame, true)
binding.dualCameraFaceRectView.clearFaceInfo() binding.dualCameraFaceRectView.clearFaceInfo()
recognizeViewModel.clearLeftFace(facePreviewInfoList)
binding.root.postDelayed({ binding.root.postDelayed({
isRecognition = true isRecognition = true
}, 500) }, 500)
} }
} }
override fun onResume() {
super.onResume()
// 清空上次识别结果和 FaceHelper 内部状态,防止短时间内再次识别无法触发
recognizeViewModel.resetFaceState()
resumeCamera()
viewModel.resetUserInfo()
countDownTimer?.let {
it.cancel()
it.start()
}
}
protected override fun onPause() {
pauseCamera()
super.onPause()
countDownTimer?.cancel()
}
private fun pauseCamera() {
isRecognition = false
recognizeViewModel.onPreviewFrame(emptyFrame, true)
}
/** /**
* 构造符合分辨率的 NV21 格式全黑帧 * 构造符合分辨率的 NV21 格式全黑帧
* @param width 图像宽度 * @param width 图像宽度(如 1920
* @param height 图像高度 * @param height 图像高度(如 1080
* @return 合法的全黑帧字节数组 * @return 合法的全黑帧字节数组
*/ */
private fun createBlackNV21Frame(width: Int, height: Int): ByteArray { private fun createBlackNV21Frame(width: Int, height: Int): ByteArray {
val frameSize = width * height val frameSize = width * height
val nv21Data = ByteArray(frameSize * 3 / 2) // NV21 格式长度 = 宽×高×1.5 val nv21Data = ByteArray(frameSize * 3 / 2) // NV21 格式长度 = 宽×高×1.5
// Y 分量(亮度)设为 0(全黑),UV 分量设为 128(默认值) // Y 分量(亮度)设为 0(全黑),UV 分量设为 128(默认值)
// 1. 填充 Y 分量(前 frameSize 个字节)
// Arrays.fill(nv21Data, 0, frameSize, 0.toByte())
nv21Data.fill(0, 0, frameSize) nv21Data.fill(0, 0, frameSize)
// 2. 填充 UV 分量(后 frameSize/2 个字节)
// Arrays.fill(nv21Data, frameSize, nv21Data.size, 128.toByte())
nv21Data.fill(128.toByte(), frameSize, nv21Data.size) nv21Data.fill(128.toByte(), frameSize, nv21Data.size)
return nv21Data return nv21Data
} }
// 调用示例:构造 1920×1080 的全黑帧(长度 1382400
// private val emptyFrame = createBlackNV21Frame(1920, 1080)
private val emptyFrame = createBlackNV21Frame(1280, 720) private val emptyFrame = createBlackNV21Frame(1280, 720)
// 传递“空帧” recognizeViewModel.onPreviewFrame(emptyFrame, true)
override fun handleLoginSuccess(equipmentUserInfo: EquipmentUserInfo, isAdmin: Boolean) { // /**
super.handleLoginSuccess(equipmentUserInfo, isAdmin) // * 临时用户id
// goInitActivity() // */
// private var tempUserId: String = ""
override fun onResume() {
super.onResume()
try {
if (isFirstOpen) {
isFirstOpen = false
goInitActivity()
return
}
lifecycleScope.launch {
try {
val faceDao = FaceDatabase.getInstance(this@LoginByFaceActivity).faceDao()
val faceCount = withContext(Dispatchers.IO) { faceDao.faceCount }
if (faceCount == 0) {
newUserCollect()
} else {
resetRecognizeState()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
} catch (e: Exception) {
e.printStackTrace()
}
} }
fun initCountTime() { /**
val totalTimeInMillis = 30L * 1000 * 重置识别状态
countDownTimer = object : CountDownTimer(totalTimeInMillis, 1000) { */
override fun onTick(millisUntilFinished: Long) { private fun resetRecognizeState() {
Timber.d("initCountTime onTick = ${(millisUntilFinished / 1000).toInt()}") binding.ivCollectPhoto.gone()
binding.tvToInit.text = "${(millisUntilFinished / 1000).toInt() + 1}" binding.ivCollectPhoto.setImageDrawable(null)
} binding.tvToInit.text = "15"
binding.llCollectButton.gone()
retryFailCount = 0
binding.tvSimilarValue.text = ""
isCollecting.set(false)
isCollectFace = false
plateEnable = true
collectCount = 0
// tempUserId = "${System.currentTimeMillis()}"
resumeCamera()
registerHandler = RegisterCallbackHandler()
override fun onFinish() { recognizeCountDownUtil.startCountDown(
goInitActivity() total = 15,
lifecycleScope = lifecycleScope,
onTick = { remaining ->
loadLogInfo("resetRecognizeState剩余时间:$remaining")
if (judgeRecognizeState()) return@startCountDown
loadLogInfo("initCountTime onTick = $remaining")
binding.tvToInit.text = "$remaining"
},
onFinish = {
loadLogInfo("resetRecognizeState倒计时结束")
if (judgeRecognizeState()) return@startCountDown
if (facePreviewInfoList.isNullOrEmpty()) {
goInitActivity()
} else {
loadLogInfo("recognizeFail---initCountTime")
recognizeFail()
}
} }
}.start() )
}
private var isFirstOpen = true
override fun onPause() {
pauseCamera()
super.onPause()
recognizeCountDownUtil.cancelCountDown()
}
private fun pauseCamera() {
isRecognition = false
// recognizeViewModel.onPreviewFrame(emptyFrame, true)
}
private fun judgeRecognizeState(): Boolean {
val rectColor = binding.dualCameraFaceRectView.rectColor
if (rectColor == RecognizeColor.COLOR_SUCCESS && currentUserId.isNullOrBlank()
.not() && facePreviewInfoList.isNullOrEmpty().not()
) {
//人未离开,再次识别
loadLogInfo("userId=$currentUserId")
openPlateDebouncer.debounce {
openPlate(currentUserId)
}
return true
}
return false
} }
fun goInitActivity() { fun goInitActivity() {
@@ -516,5 +785,151 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
startActivity(intent) startActivity(intent)
} }
/**
* 采集人脸
*/
private fun collectFace() {
loadLogInfo("collectFace: -----------1,开始采集人脸,开始时间:${DateTimeUtils.getDateTimeString()}----------")
if (isFirstOpen) return
loadLogInfo("collectFace: ----------2,isFirstOpen=false-----------")
loadLogInfo("collectFace: ----------4,检测到人脸信息-----------")
currentUserId = null
userFaceInfo = null
recognizeViewModel.updateRegisterStatus(REGISTER_STATUS_READY)
loadLogInfo("collectFace: ----------5,人脸采集中-----------")
}
private val uploadFaceDebouncer = Debouncer(10000)
/**
* 改为上传人脸数据,不需要发生人脸数据到收银端
*/
private fun sendFaceData(faceData: String, headBmp: Bitmap) {
loadLogInfo("collectFace,sendFaceData: ----------11,发送人脸数据-----------")
val imageFile = BitmapSaver.saveToAppFilesDir(
headBmp, this, "IMG_CROP_${System.currentTimeMillis()}.jpg"
)
loadLogInfo("collectFace,sendFaceData:${imageFile?.absolutePath}")
if (imageFile == null) {
ToastUtils.showToast("获取人脸照片失败")
return
}
uploadFaceDebouncer.debounce {
addUserFace(faceData, imageFile)
}
}
private fun addUserFace(faceData: String, imageFile: File) {
settingViewModel.addUserFace(faceData, imageFile) { status, faceModel ->
loadLogInfo("collectFace,saveFaceInfo: ----------12,人脸数据发送完成时间:${DateTimeUtils.getDateTimeString()},结果:$status-----------")
if (status.not()) {
ToastUtils.showToast("新增数据失败")
loadLogInfo("collectFace,sendFaceData: ----------12,发送人脸数据失败,faceData=${faceData}")
return@addUserFace
}
if (faceModel == null) {
ToastUtils.showToast("新增数据成功,但未拿到接口数据")
loadLogInfo("collectFace,sendFaceData: ----------12,发送人脸数据成功但接口返回为null,faceData=${faceData}")
return@addUserFace
}
lifecycleScope.launch(Dispatchers.IO) {
saveFaceInfo(faceModel) {
runOnUiThread {
Glide.with(this@LoginByFaceActivity).load(imageFile)
.into(binding.ivCollectPhoto)
}
}
uploadFaceDebouncer.reset()
}
}
}
private fun getFaceDataBlock() {
if (userFaceInfo?.faceFeature?.featureData == null) {
loadLogInfo("collectFace,getFaceData: ----------7,拿到人脸提取特征数据为null-----------")
return
}
val faceData: String? = Base64.encode(userFaceInfo?.faceFeature?.featureData)
if (faceData.isNullOrBlank()) {
loadLogInfo("collectFace,getFaceData: ----------7,拿到人脸提取特征数据Base64编码为空")
return
}
loadLogInfo("collectFace,getFaceData: ----------7,拿到人脸提取特征数据时间:${DateTimeUtils.getDateTimeString()}-----------")
val headBmp = userFaceInfo!!.headBmp
//block(faceData, headBmp)
isCollecting.set(false)
isCollectFace = false
sendFaceData(faceData, headBmp)
}
/**
* 加载日志
* @param msg 日志内容
*/
private fun loadLogInfo(msg: String) {
Timber.tag(TAG).d(msg)
LogFileUtil.saveLog(msg)
}
private var facePreviewInfoList: MutableList<FacePreviewInfo>? = null
private var userFaceInfo: UserFaceInfo? = null
private var currentUserId: String? = null
private fun saveFaceInfo(faceModel: UserFaceModel, block: () -> Unit = {}) {
try {
loadLogInfo("collectFace,saveFaceInfo: ----------8,保存人脸取特征数据-----------")
val faceDao = FaceDatabase.getInstance(this).faceDao()
val faceEntity = FaceEntity(
faceModel.userId,
null,
Base64.decode(faceModel.faceFeatureStr)
).also {
//1-会员、2-临时用户
it.userType = if (faceModel.member) "1" else "2"
}
val faceId = faceDao.insert(faceEntity)
faceEntity.faceId = faceId
loadLogInfo("collectFace,saveFaceInfo: ----------10,人脸数据保存完成-----------")
recognizeViewModel.refreshFaceList()
loadLogInfo("collectFace,saveFaceInfo: ----------11,刷新人脸数据-----------")
runOnUiThread {
//采集完成,不再识别,直接提示成功
isRecognition = false
recognizeSuccess("${System.currentTimeMillis()}")
}
loadLogInfo("---------------------------------")
collectCount = 0
block()
} catch (e: Exception) {
e.printStackTrace()
loadLogInfo("collectFace,saveFaceInfo: ----------13,人脸采集异常:${e.message}-----------")
collectCount++
loadLogInfo("collectFace,saveFaceInfo: ----------14,${collectCount}次人脸采集异常:${e.message}-----------")
ToastUtils.showToast("人脸采集异常:${e.message}")
}
}
private var collectCount = 0
private var isCollectFace = false
private fun leftCountDown() {
leftCountDownUtil.startCountDown(total = 15, lifecycleScope = lifecycleScope, onStart = {
loadLogInfo("onPreview-----------leftCountDown-------------")
}, onTick = {
loadLogInfo("leftCountDown.onTick:$it")
if (facePreviewInfoList.isNullOrEmpty().not()) {
loadLogInfo("leftCountDown.onTick-cancel()")
leftCountDownUtil.cancelCountDown()
return@startCountDown
}
}, onFinish = {
if (facePreviewInfoList.isNullOrEmpty().not()) {
loadLogInfo("leftCountDown.onFinish-人脸未离开,不跳转")
return@startCountDown
}
goInitActivity()
})
}
} }
@@ -0,0 +1,946 @@
//package com.sw.platecabinet.activity
//
//import android.Manifest
//import android.content.Intent
//import android.graphics.Bitmap
//import android.graphics.Point
//import android.hardware.Camera
//import android.os.Build
//import android.os.CountDownTimer
//import android.util.DisplayMetrics
//import android.view.View
//import android.view.ViewGroup
//import android.view.ViewTreeObserver
//import androidx.annotation.RequiresApi
//import androidx.lifecycle.Observer
//import com.arcsoft.face.ErrorInfo
//import com.google.gson.Gson
//import com.sw.inbound.utils.DateTimeUtils
//import com.sw.plate.utils.Base64
//import com.sw.plate.utils.ToastUtils
//import com.sw.plate.utils.arcface.ConfigUtil
//import com.sw.plate.utils.arcface.ErrorCodeUtil
//import com.sw.plate.utils.arcface.FaceRectTransformer
//import com.sw.plate.utils.arcface.FaceRectView
//import com.sw.plate.utils.arcface.FaceRectView.DrawInfo
//import com.sw.plate.utils.arcface.PreviewConfig
//import com.sw.plate.utils.arcface.camera.CameraListener
//import com.sw.plate.utils.arcface.camera.DualCameraHelper
//import com.sw.plate.utils.arcface.face.constants.LivenessType
//import com.sw.plate.utils.arcface.face.model.CompareResult
//import com.sw.plate.utils.arcface.face.model.FacePreviewInfo
//import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration
//import com.sw.plate.utils.arcface.facedb.FaceDatabase
//import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
//import com.sw.plate.utils.arcface.model.UserFaceInfo
//import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
//import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel.REGISTER_STATUS_READY
//import com.sw.platecabinet.MyApp
//import com.sw.platecabinet.R
//import com.sw.platecabinet.databinding.ActivityLoginFaceBinding
//import com.sw.platecabinet.databinding.ItemTitleTimeBinding
//import com.sw.platecabinet.model.response.EquipmentUserInfo
//import com.sw.platecabinet.model.response.UserFaceModel
//import com.sw.platecabinet.utils.BitmapSaver
//import com.sw.platecabinet.utils.Debouncer
//import com.sw.platecabinet.utils.LogFileUtil
//import com.sw.platecabinet.utils.PermissionHelper
//import com.sw.platecabinet.utils.PlateUtils
//import com.sw.platecabinet.utils.RegisterCallbackHandler
//import com.sw.platecabinet.utils.SoundPoolUtil
//import com.sw.platecabinet.utils.SpTool
//import org.greenrobot.eventbus.EventBus
//import org.greenrobot.eventbus.Subscribe
//import org.greenrobot.eventbus.ThreadMode
//import timber.log.Timber
//import java.io.File
//import java.util.concurrent.atomic.AtomicBoolean
//
///**
// * 人脸识别
// */
//class LoginByFaceActivityBak : BaseActivity<ActivityLoginFaceBinding>(),
// ViewTreeObserver.OnGlobalLayoutListener {
// // private val recognizeViewModel by viewModels<RecognizeViewModel>()
// private var countDownTimer: CountDownTimer? = null
//
// private val CAMERA_PERMISSION_REQUEST_CODE = 100
// private val REQUIRED_PERMISSIONS: Array<String> = arrayOf(
// Manifest.permission.CAMERA,
// Manifest.permission.WRITE_EXTERNAL_STORAGE,
// Manifest.permission.READ_EXTERNAL_STORAGE
// )
//
// override fun inflateViewBinding(): ActivityLoginFaceBinding {
// return ActivityLoginFaceBinding.inflate(layoutInflater)
// }
//
// override fun inflateTitleBinding(): ItemTitleTimeBinding {
// return binding.includeHeader
// }
//
// companion object {
// private const val TAG = "LoginByFaceActivity"
// private var instance: LoginByFaceActivityBak? = null
//
// private const val COLLECT_SUCCESS = "collect_success"
// private const val RECOGNIZE_SUCCESS = "recognize_success"
// private const val NO_PLATE_REMIND = "no_plate_remind";
//
// fun goInitActivity() {
// instance?.goInitActivity()
// }
// }
//
//// private var logList: MutableList<String> = mutableListOf()
//// private val logAdapter by lazy { LogAdapter(logList) }
//
// override fun initialize() {
// instance = this
// userViewModel.activeEngine()
//// addSocketListener()
// initCountTime()
// initArcViewModel()
// initArcView()
// openRectInfoDraw = true
// recognizeViewModel.setDrawRectInfoTextValue(true)
// //viewModel.generateToken()
//
//// binding.llToPwd.setOnClickListener {
//// val intent = Intent(this, LoginByPwdActivity::class.java)
//// startActivity(intent)
//// }
//
//// TaskManager.startTask()
//
// EventBus.getDefault().register(this)
//
//// binding.btnCollectFace.setOnClickListener { collectFace() }
//
//
//// binding.rvLogInfo.let {
//// it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, true)
//// it.adapter = logAdapter
//// }
////
//// binding.btnLogState.let {
//// it.tag = false
//// it.setImageResource(R.drawable.ic_log_show)
//// it.setOnClickListener { v ->
//// binding.rvLogInfo.run {
//// val state = it.tag.toString().toBooleanStrictOrNull() ?: false
//// if (state) visible() else gone()
//// }
//// val state = it.tag.toString().toBooleanStrictOrNull() ?: false
//// it.tag = state
//// it.setImageResource(
//// if (state) R.drawable.ic_log_hide else R.drawable.ic_log_show
//// )
//// }
//// }
//
// //开启人脸增量数据定时任务
// startFaceTask {
// loadLogInfo("collectFace增量接口数据用户id:${Gson().toJson(it)}")
// }
//
// val soundMap = hashMapOf<String, Int>()
// soundMap[COLLECT_SUCCESS] = R.raw.collect_success
// soundMap[RECOGNIZE_SUCCESS] = R.raw.recognize_success
// soundMap[NO_PLATE_REMIND] = R.raw.no_plate_remind
// SoundPoolUtil.getInstance().loadR(this, soundMap);
// }
//
//
// private fun checkCameraPermission() {
// Timber.i("checkCameraPermission")
// val permissionHelper =
// PermissionHelper.with(this, REQUIRED_PERMISSIONS, CAMERA_PERMISSION_REQUEST_CODE)
// .onGranted {
// loadLogInfo("checkCameraPermission 权限已允许")
// // 权限已授予
// recognizeViewModel.init()
// initRgbCamera()
// resumeCamera()
// }
// .build()
// registerPermissionHelper(permissionHelper)
// permissionHelper.checkAndRequest()
// }
//
// @Subscribe(threadMode = ThreadMode.MAIN)
// fun getFaceEntity(insertEntity: FaceEntity) {
// Timber.tag("performSync").e("-time=%s", insertEntity.registerTime)
// recognizeViewModel.addFace(insertEntity)
// Timber.tag("performSync over").e("-time=%s", insertEntity.registerTime)
// }
//
// override fun onLeftDoubleClick() {
// finish()
// }
//
// private var isRecognition = false
// private var rgbCameraHelper: DualCameraHelper? = null
// private var rgbFaceRectTransformer: FaceRectTransformer? = null
// private val livenessType = LivenessType.RGB
// private var openRectInfoDraw = false
// private val regDebouncer = Debouncer(3000)
// private var registerHandler: RegisterCallbackHandler? = null
//
// private fun initArcViewModel() {
// recognizeViewModel.setLiveType(livenessType)
// recognizeViewModel.prepareRegister()
// recognizeViewModel.setOnRegisterFinishedCallback { facePreviewInfo, userFaceInfo ->
// registerHandler?.setCallback {
// if (!isCollectFace) {
// return@setCallback
// }
// if (this.userFaceInfo == null && userFaceInfo != null) {
// recognizeViewModel.updateRegisterStatus(RecognizeViewModel.REGISTER_STATUS_DONE)
// this.userFaceInfo = userFaceInfo
// getFaceDataBlock { faceData, headBmp ->
// isCollecting.set(false)
// isCollectFace = false
// sendFaceData(faceData, headBmp)
// }
// }
// }
// }
// recognizeViewModel.ftInitCode.observe(this, Observer { ftInitCode: Int? ->
// if (ftInitCode != ErrorInfo.MOK) {
// val error: String? = getString(
// R.string.specific_engine_init_failed, "ftEngine",
// ftInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(ftInitCode!!)
// )
// Timber.e("ftInitCode observe = $error")
// ToastUtils.showToast(error)
// }
// })
// recognizeViewModel.frInitCode.observe(this, Observer { frInitCode: Int? ->
// if (frInitCode != ErrorInfo.MOK) {
// val error: String? = getString(
// R.string.specific_engine_init_failed, "frEngine",
// frInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(frInitCode!!)
// )
// Timber.e("frInitCode observe = $error")
// ToastUtils.showToast(error)
// }
// })
// recognizeViewModel.flInitCode.observe(this, Observer { flInitCode: Int? ->
// if (flInitCode != ErrorInfo.MOK) {
// val error: String? = getString(
// R.string.specific_engine_init_failed, "flEngine",
// flInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(flInitCode!!)
// )
// Timber.e("flInitCode observe = $error")
// ToastUtils.showToast(error)
// }
// })
//
// recognizeViewModel.recognizeConfiguration
// .observe(this, Observer { recognizeConfiguration: RecognizeConfiguration? ->
// Timber.i("recognizeConfiguration: observe = ${recognizeConfiguration.toString()}")
// })
// recognizeViewModel.recognizeNotice.observe(this, Observer { notice: String? ->
// Timber.i("recognizeNotice observe notice = $notice")
// })
//
// recognizeViewModel.recognizeUserId.observe(this, Observer { result: CompareResult? ->
// loadLogInfo("collectFace,recognizeUserId observe userId = ${result?.faceEntity?.userName}")
// if (faceUseSwitch) {
// loadFaceRecognizeResult2(result)
// return@Observer
// }
// loadFaceRecognizeResult(result)
// })
//
// recognizeViewModel.drawRectInfoText.observe(this, Observer { info ->
// Timber.i("drawRectInfoText observe info = $info")
// })
// }
//
// /**
// * 人脸识别开关,true-loadFaceRecognizeResult2false-loadFaceRecognizeResult
// */
// private var faceUseSwitch = true
//
// /**
// * 识别失败重试次数
// */
// private var retryFailCount = 0
//
// /**
// * 识别失败最大次数
// */
// private val maxFailCount = 5
//
// /**
// * 人脸失败阈值
// */
// private val faceFailThreshold = 0.3
//
// /**
// * 配置阈值,默认值为0.8
// */
// private val faceSuccessThreshold by lazy { ConfigUtil.getRecognizeThreshold(this) }
//
//
// private val openPlateDebouncer = Debouncer(5000)
// private val isCollecting = AtomicBoolean(false)
//
// /**
// * 根据相似度similar>设定阈值0.8作为识别成功判断,
// * 如果similar<0.3作为识别失败判断,
// * 如果similar>=0.3&&similar<=0.8时,暂时不作为失败情况,再识别5次如果similar<0.8则作为识别失败处理
// */
// private fun loadFaceRecognizeResult2(result: CompareResult?) {
// // 无有效结果直接忽略
// val similar = result?.similar ?: return
//
// binding.tvSimilarValue.text = "$similar${retryFailCount}次"
// loadLogInfo(
// "collectFace,recognizeUserId 阈值 success=$faceSuccessThreshold, " +
// "fail=$faceFailThreshold, similar=$similar, retry=$retryFailCount"
// )
//
// if (similar >= faceSuccessThreshold) {
// val userId = result.faceEntity?.userName
// if (userId.isNullOrBlank()) {
// loadLogInfo("collectFace,recognizeUserId 识别成功但 userId 为空,忽略")
// return
// }
//
// loadLogInfo(
// "collectFace,recognizeUserId 识别成功 userId=$userId, time=${DateTimeUtils.getDateTimeString()}"
// )
//
// // 成功后必须重置状态
// retryFailCount = 0
// isCollecting.set(false)
// isCollectFace = false
// currentUserId = userId
//
// // 防抖,防止重复开闸
// openPlateDebouncer.debounce {
// openPlate(userId)
// }
// return
// }
//
// if (isCollectFace) {
// loadLogInfo("collectFace,recognizeUserId 正在采集中,忽略失败结果 similar=$similar")
// return
// }
//
// if (similar <= faceFailThreshold || retryFailCount >= maxFailCount) {
// loadLogInfo(
// "collectFace,recognizeUserId 识别失败,开始采集 similar=$similar, " +
// "retry=$retryFailCount, time=${DateTimeUtils.getDateTimeString()}"
// )
// if (!isCollecting.compareAndSet(false, true)) {
// loadLogInfo("collectFace 已在采集中,忽略重复触发")
// return
// }
//
// retryFailCount = 0
// isCollectFace = true
// collectFace()//采集人脸
// return
// }
//
// retryFailCount++
// loadLogInfo("collectFace,recognizeUserId 中间态重试,retryFailCount=$retryFailCount")
// }
//
//// private fun loadFaceRecognizeResult2(result: CompareResult?) {
////
//// if(isCollectFace){
//// return
//// }
////
//// val similar = result?.similar ?: 0f
//// binding.tvSimilarValue.text = "$similar${retryFailCount}次"
//// loadLogInfo("collectFace,recognizeUserId 人脸识别配置阈值为:${faceSuccessThreshold},失败阈值为:${faceFailThreshold},相似度为:${similar},失败次数为:${retryFailCount}")
//// if (similar > faceSuccessThreshold) {
//// loadLogInfo("collectFace,recognizeUserId 相似度为:${similar},识别成功时间:${DateTimeUtils.getDateTimeString()}")
//// //大 于识别阈值,作为识别成功判断
//// currentUserId = result?.faceEntity?.userName
//// Debouncer(5000).debounce { openPlate(currentUserId) }
//// return
//// }
//// loadLogInfo("collectFace,recognizeUserId 人脸识别相似度为:${similar},失败次数为:${retryFailCount}")
//// if (similar < faceFailThreshold || retryFailCount >= maxFailCount) {
//// //相似度低于识别错误阈值进行采集、识别失败次数大于等于最大失败次数,作为识别失败判断
//// if (isCollectFace.not()) {
//// //非采集中
//// loadLogInfo("collectFace,recognizeUserId 未识别到人脸信息,相似度为:${similar},开始采集时间:${DateTimeUtils.getDateTimeString()}")
//// isCollectFace = true
//// collectFace()
//// return
//// }
//// }
//// //满足similar >= faceFailThreshold && similar <= successThreshold时进行重试
//// retryFailCount++
//// loadLogInfo("collectFace,recognizeUserId 人脸识别失败次数为:${retryFailCount}")
//// }
//
// /**
// * 根据相似度similar>设定阈值0.8作为识别是否成功来加载人脸识别结果
// */
// private fun loadFaceRecognizeResult(result: CompareResult?) {
// val similar = result?.similar ?: 0f
// binding.tvSimilarValue.text = "$similar"
// val similarPass = result?.isSimilarPass ?: false
// if (similarPass.not()) {
// loadLogInfo("collectFace,recognizeUserId 未识别到人脸信息,开始采集")
// isCollectFace = true
// collectFace()
// return
// }
// currentUserId = result?.faceEntity?.userName
// if (tempUserId != currentUserId) {
// SoundPoolUtil.getInstance().play("success", 0)
// }
//// plateEnable = true
// Debouncer(5000).debounce { openPlate(currentUserId) }
// }
//
// /**
// * 查询餐盘数据
// */
// private fun getPlateData(block: (List<EquipmentUserInfo>) -> Unit) {
// MyApp.plateList?.let {
// block(it)
// }
// }
//
// private var currentPlateInfo: EquipmentUserInfo? = null
// private var plateEnable = false
//
// /**
// * 打开餐盘柜
// */
// private fun openPlate(userId: String?) {
// if (plateEnable.not()) return
// loadLogInfo("collectFace,openPlate: userId=$userId")
// getPlateData { items ->
// loadLogInfo("collectFace,openPlate: items:${Gson().toJson(items)}")
// // 未查询到当前用户,查询第一个未绑定的餐盘
// val item = items.firstOrNull { it.plateNumber.isNullOrEmpty().not() }
// loadLogInfo("collectFace,openPlate: item2:${item}")
// if (item == null) {
// //暂无餐盘
// loadLogInfo("collectFace,openPlate: 暂无餐盘,请联系管理人员,isCollectFace=$isCollectFace")
// //ToastUtils.showToast("暂无餐盘,请联系管理人员")
// SoundPoolUtil.getInstance().play(NO_PLATE_REMIND, 0)
// goInitActivity()
// return@getPlateData
// }
//// if (tempUserId != currentUserId) {
// SoundPoolUtil.getInstance().play(RECOGNIZE_SUCCESS, 0)
//// } else {
//// SoundPoolUtil.getInstance().play(COLLECT_SUCCESS, 0)
//// }
// loadLogInfo("collectFace,openPlate: item3:${item}")
// item.faceId = userId
// item.updateTime = DateTimeUtils.getDateTimeString()
// item.plateNumber = null
// plateEnable = false
// Debouncer(5000).debounce {
// //打开柜子
// PlateUtils.open(this, item.equipmentBoxCode, false)
// }
// currentPlateInfo = item
// MyApp.plateList = items
// SpTool.savePlateData(items)
// }
// }
//
//// private val debouncer by lazy { Debouncer(3000) }
//
// private fun initArcView() {
// //在布局结束后才做初始化操作
// binding.dualCameraTexturePreviewRgb.getViewTreeObserver().addOnGlobalLayoutListener(this)
// recognizeViewModel.getCompareResultList().getValue()
// }
//
// override fun onDestroy() {
// loadLogInfo("onDestroy")
// instance = null
// if (rgbCameraHelper != null) {
// rgbCameraHelper!!.release()
// rgbCameraHelper = null
// }
//
// recognizeViewModel.destroy()
//
// EventBus.getDefault().unregister(this)
// super.onDestroy()
// }
//
//
// /**
// * 调整View的宽高,使预览显示正常且采集框固定为380x380
// *
// * @param rgbPreview RGB预览View
// * @param previewView 显示预览数据的view
// * @param faceRectView 画框的view (380x380)
// * @param previewSize 预览大小
// * @param displayOrientation 相机旋转角度
// * @param scale 缩放比例
// * @return 调整后的LayoutParams
// */
// private fun adjustPreviewViewSize(
// rgbPreview: View,
// previewView: View,
// faceRectView: FaceRectView,
// previewSize: Camera.Size,
// displayOrientation: Int,
// scale: Float
// ): ViewGroup.LayoutParams {
// val layoutParams = previewView.layoutParams
// val measuredWidth = previewView.measuredWidth
// val measuredHeight = previewView.measuredHeight
// var ratio = (previewSize.height.toFloat()) / previewSize.width.toFloat()
// if (ratio > 1) {
// ratio = 1 / ratio
// }
// if (displayOrientation % 180 == 0) {
// layoutParams.width = measuredWidth
// layoutParams.height = (measuredWidth * ratio).toInt()
// } else {
// layoutParams.height = measuredHeight
// layoutParams.width = (measuredHeight * ratio).toInt()
// }
// if (scale < 1f) {
// val rgbParam = rgbPreview.getLayoutParams()
// layoutParams.width = (rgbParam.width * scale).toInt()
// layoutParams.height = (rgbParam.height * scale).toInt()
// } else {
// layoutParams.width = (layoutParams.width * scale).toInt()
// layoutParams.height = (layoutParams.height * scale).toInt()
// }
//
// val metrics = DisplayMetrics()
// windowManager.defaultDisplay.getMetrics(metrics)
//
// if (layoutParams.width >= metrics.widthPixels) {
// val viewRatio = layoutParams.width / (metrics.widthPixels.toFloat())
// layoutParams.width = (layoutParams.width / viewRatio).toInt()
// layoutParams.height = (layoutParams.height / viewRatio).toInt()
// }
// if (layoutParams.height >= metrics.heightPixels) {
// val viewRatio = layoutParams.height / (metrics.heightPixels.toFloat())
// layoutParams.width = (layoutParams.width / viewRatio).toInt()
// layoutParams.height = (layoutParams.height / viewRatio).toInt()
// }
//
// previewView.setLayoutParams(layoutParams)
// faceRectView.setLayoutParams(layoutParams)
// return layoutParams
// }
//
// private fun initRgbCamera() {
// val cameraListener: CameraListener = object : CameraListener {
// override fun onCameraOpened(
// camera: Camera,
// cameraId: Int,
// displayOrientation: Int,
// isMirror: Boolean
// ) {
// loadLogInfo("initRgbCamera Rgb onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation")
// runOnUiThread({
// val previewSizeRgb = camera.getParameters().getPreviewSize()
// val layoutParams = adjustPreviewViewSize(
// binding.dualCameraTexturePreviewRgb,
// binding.dualCameraTexturePreviewRgb, binding.dualCameraFaceRectView,
// previewSizeRgb, displayOrientation, 0.6f
// )
// loadLogInfo("initRgbCamera previewSizeRgb = ${previewSizeRgb.width}-${previewSizeRgb.height}")
// loadLogInfo("initRgbCamera layoutParams = ${layoutParams.width}-${layoutParams.height}")
// loadLogInfo(
// "initRgbCamera isMirror = ${isMirror}, isDrawRgbRectHorizontalMirror = ${
// ConfigUtil.isDrawRgbRectHorizontalMirror(
// context
// )
// }, isDrawRgbRectVerticalMirror = ${
// ConfigUtil.isDrawRgbRectVerticalMirror(
// context
// )
// }"
// )
// rgbFaceRectTransformer = FaceRectTransformer(
// previewSizeRgb.width,
// previewSizeRgb.height,
// layoutParams.width,
// layoutParams.height,
// 90,
// cameraId,
// isMirror,
// true,
// true
// )
//
// recognizeViewModel.onRgbCameraOpened(camera)
// recognizeViewModel.setRgbFaceRectTransformer(rgbFaceRectTransformer)
// })
// }
//
// @RequiresApi(api = Build.VERSION_CODES.Q)
// override fun onPreview(nv21: ByteArray?, camera: Camera?) {
// if (!isRecognition) {
// return
// }
// binding.dualCameraFaceRectView.clearFaceInfo()
// facePreviewInfoList = recognizeViewModel.onPreviewFrame(nv21, true)
// if (facePreviewInfoList != null && rgbFaceRectTransformer != null) {
// drawPreviewInfo(facePreviewInfoList)
// }
// if (facePreviewInfoList.isNullOrEmpty()) {
// currentPlateInfo = null
// currentUserId = null
// userFaceInfo = null
// }
// recognizeViewModel.clearLeftFace(facePreviewInfoList)
// }
//
// override fun onCameraClosed() {
// Timber.i("initRgbCamera onCameraClosed: ")
// }
//
// override fun onCameraError(e: java.lang.Exception) {
// Timber.i("initRgbCamera onCameraError: %s", e.message)
// e.printStackTrace()
// }
//
// override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) {
// Timber.i("initRgbCamera onCameraConfigurationChanged: threadName = ${Thread.currentThread().name}")
// if (rgbFaceRectTransformer != null) {
// rgbFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation
// }
// Timber.i("initRgbCamera onCameraConfigurationChanged: $cameraID $displayOrientation")
// }
// }
//
// val previewConfig: PreviewConfig = recognizeViewModel.previewConfig
// rgbCameraHelper = DualCameraHelper.Builder()
// .previewViewSize(
// Point(
// binding.dualCameraTexturePreviewRgb.measuredWidth,
// binding.dualCameraTexturePreviewRgb.measuredHeight
// )
// )
// .rotation(windowManager.defaultDisplay.rotation)
// .additionalRotation(0) // 角度
// .previewSize(recognizeViewModel.loadPreviewSize())
// .specificCameraId(previewConfig.rgbCameraId)
// .isMirror(true)
// .previewOn(binding.dualCameraTexturePreviewRgb)
// .cameraListener(cameraListener)
// .build()
// rgbCameraHelper!!.init()
// rgbCameraHelper!!.start()
// }
//
// /**
// * 绘制RGB、IR画面的实时人脸信息
// *
// * @param facePreviewInfoList RGB画面的实时人脸信息
// */
// private fun drawPreviewInfo(facePreviewInfoList: MutableList<FacePreviewInfo>?) {
// if (rgbFaceRectTransformer != null) {
// val rgbDrawInfoList: MutableList<DrawInfo>? = recognizeViewModel.getDrawInfo(
// facePreviewInfoList, LivenessType.RGB, openRectInfoDraw
// )
// // 识别成功
// binding.dualCameraFaceRectView.drawRealtimeFaceInfo(rgbDrawInfoList)
// }
// }
//
// fun openRectInfoDraw(view: View?) {
// openRectInfoDraw = !openRectInfoDraw
// recognizeViewModel.setDrawRectInfoTextValue(openRectInfoDraw)
// }
//
//
// override fun onGlobalLayout() {
// loadLogInfo("onGlobalLayout")
// binding.dualCameraTexturePreviewRgb.getViewTreeObserver().removeOnGlobalLayoutListener(this)
// // 请求权限
// checkCameraPermission()
// }
//
// private fun resumeCamera() {
// isRecognition = true
// if (rgbCameraHelper != null && rgbCameraHelper!!.isStopped) {
// loadLogInfo("collectFace,resumeCamera: ")
// rgbCameraHelper!!.start()
// } else {
// recognizeViewModel.onPreviewFrame(ByteArray(1382400), true)
// }
// }
//
// /**
// * 临时用户id
// */
// private var tempUserId: String = ""
// override fun onResume() {
// super.onResume()
// registerHandler = RegisterCallbackHandler()
// try {
// if (isFirstOpen) {
// goInitActivity()
// binding.root.postDelayed({
// isFirstOpen = false
// }, 1000)
// return
// }
// retryFailCount = 0
// binding.tvSimilarValue.text = ""
// isCollecting.set(false)
// isCollectFace = false
// plateEnable = true
// collectCount = 0
// tempUserId = "${System.currentTimeMillis()}"
// resumeCamera()
// userViewModel.resetUserInfo()
// countDownTimer?.let {
// it.cancel()
// it.start()
// }
//// initFaceScheduler()
//// checkFaceState()
// } catch (e: Exception) {
// e.printStackTrace()
// }
// }
//
// private var isFirstOpen = true
//
// protected override fun onPause() {
// pauseCamera()
// super.onPause()
// countDownTimer?.cancel()
// }
//
// private fun pauseCamera() {
// isRecognition = false
// recognizeViewModel.onPreviewFrame(ByteArray(1382400), true)
// }
//
// override fun handleLoginSuccess(equipmentUserInfo: EquipmentUserInfo, isAdmin: Boolean) {
// super.handleLoginSuccess(equipmentUserInfo, isAdmin)
//// goInitActivity()
// }
//
// private val totalTimeInMillis = 15 * 1000L
// fun initCountTime() {
// countDownTimer = object : CountDownTimer(totalTimeInMillis, 1000) {
// override fun onTick(millisUntilFinished: Long) {
// loadLogInfo("initCountTime onTick = ${(millisUntilFinished / 1000).toInt()}")
// binding.tvToInit.text = "${(millisUntilFinished / 1000).toInt() + 1}"
// }
//
// override fun onFinish() {
// goInitActivity()
// }
// }
// }
//
// fun goInitActivity() {
// val intent = Intent(context, InitActivity::class.java)
// startActivity(intent)
// }
//
// /**
// * 采集人脸
// */
// private fun collectFace() {
// loadLogInfo("collectFace: -----------1,开始采集人脸,开始时间:${DateTimeUtils.getDateTimeString()}----------")
// if (isFirstOpen) return
// loadLogInfo("collectFace: ----------2,isFirstOpen=false-----------")
// loadLogInfo("collectFace: ----------4,检测到人脸信息-----------")
// currentUserId = null
// userFaceInfo = null
// recognizeViewModel.updateRegisterStatus(REGISTER_STATUS_READY)
// loadLogInfo("collectFace: ----------5,人脸采集中-----------")
// }
//
// private val uploadFaceDebouncer = Debouncer(10000)
//
// /**
// * 改为上传人脸数据,不需要发生人脸数据到收银端
// */
// private fun sendFaceData(faceData: String, headBmp: Bitmap) {
// loadLogInfo("collectFace,sendFaceData: ----------11,发送人脸数据-----------")
//
// val imageFile = BitmapSaver.saveToAppFilesDir(
// headBmp, this, "IMG_CROP_${System.currentTimeMillis()}.jpg"
// )
// loadLogInfo("collectFace,sendFaceData:${imageFile?.absolutePath}")
// if (imageFile == null) {
// ToastUtils.showToast("获取人脸照片失败")
// return
// }
// uploadFaceDebouncer.debounce {
// addUserFace(faceData, imageFile)
// }
//// // 给某个客户端发送
//// val jsonObject = JSONObject().also {
//// it.put("type", "faceFeature")
//// it.put("content", faceData)
//// }
//// tcpClient?.send(jsonObject)
// }
//
// private fun addUserFace(faceData: String, imageFile: File) {
// settingViewModel.addUserFace(faceData, imageFile) { status, faceModel ->
// loadLogInfo("collectFace,saveFaceInfo: ----------12,人脸数据发送完成时间:${DateTimeUtils.getDateTimeString()},结果:$status-----------")
// if (status.not()) {
// ToastUtils.showToast("新增数据失败")
// loadLogInfo("collectFace,sendFaceData: ----------12,发送人脸数据失败,faceData=${faceData}")
// return@addUserFace
// }
// if (faceModel == null) {
// ToastUtils.showToast("新增数据成功,但未拿到接口数据")
// loadLogInfo("collectFace,sendFaceData: ----------12,发送人脸数据成功但接口返回为nullfaceData=${faceData}")
// return@addUserFace
// }
// Thread {
// saveFaceInfo(faceModel)
// uploadFaceDebouncer.reset()
// }.start()
// }
// }
//
// private fun getFaceDataBlock(block: (String, Bitmap) -> Unit) {
// if (userFaceInfo == null || userFaceInfo?.faceFeature?.featureData == null) {
// return
// }
// val faceData: String? = Base64.encode(userFaceInfo?.faceFeature?.featureData)
// if (faceData.isNullOrBlank()) {
// return
// }
// loadLogInfo("collectFace,getFaceData: ----------7,拿到人脸提取特征数据时间:${DateTimeUtils.getDateTimeString()}-----------")
// block(faceData, userFaceInfo!!.headBmp)
// }
//
// /**
// * 加载日志
// * @param msg 日志内容
// */
// private fun loadLogInfo(msg: String) {
// Timber.tag(TAG).d(msg)
// LogFileUtil.saveLog(msg)
// }
//
// private var facePreviewInfoList: MutableList<FacePreviewInfo>? = null
// private var userFaceInfo: UserFaceInfo? = null
//
// private var currentUserId: String? = null
//
// private fun saveFaceInfo(faceModel: UserFaceModel) {
// try {
// loadLogInfo("collectFace,saveFaceInfo: ----------8,保存人脸取特征数据-----------")
// val faceDao = FaceDatabase.getInstance(this).faceDao()
// val faceEntity = FaceEntity(
// faceModel.userId,
// null,
// Base64.decode(faceModel.faceFeatureStr)
// ).also {
// //1-会员、2-临时用户
// it.userType = if (faceModel.isMember) "1" else "2"
// }
// faceDao.insert(faceEntity)
// loadLogInfo("collectFace,saveFaceInfo: ----------10,人脸数据保存完成-----------")
// recognizeViewModel.refreshFaceList()
// loadLogInfo("collectFace,saveFaceInfo: ----------11,刷新人脸数据-----------")
// runOnUiThread { recognizeViewModel.onPreviewFrame(ByteArray(1382400), true) }
// loadLogInfo("---------------------------------")
// collectCount = 0
// } catch (e: Exception) {
// e.printStackTrace()
// loadLogInfo("collectFace,saveFaceInfo: ----------13,人脸采集异常:${e.message}-----------")
// collectCount++
// loadLogInfo("collectFace,saveFaceInfo: ----------14,${collectCount}次人脸采集异常:${e.message}-----------")
// ToastUtils.showToast("人脸采集异常:${e.message}")
// }
// }
//
// private var collectCount = 0
//
//// private var tcpClient: TcpClient? = null
////
//// private fun addSocketListener() {
////// val ipAddress = NetworkUtils.getIPAddress(true)
////// loadLogInfo( "addSocketListener,ipAddress: $ipAddress");
//// tcpClient = TcpClient(
//// "192.168.1.95",
//// 5000,
//// GlobalData.deviceId, // clientId
//// 5000, // connectTimeoutMs
//// 10 * 1000, // heartbeatIntervalMs
//// 30 * 1000 // heartbeatTimeoutMs
//// )
////
//// tcpClient?.setListener(object : TcpClientListenerImpl() {
//// override fun onSendSuccess(json: JSONObject?) {
//// super.onSendSuccess(json)
//// loadLogInfo("collectFace,TcpClientListener发送成功:$json")
////// toast("发送成功")
//// }
////
//// override fun onSendFailed(json: JSONObject?, e: java.lang.Exception?) {
//// super.onSendFailed(json, e)
//// ToastUtils.showToast("发送失败${e?.message}")
//// loadLogInfo("collectFace,TcpClientListener人脸数据发送失败:${e?.message}")
//// }
////
//// override fun onMessage(json: JSONObject?) {
//// super.onMessage(json)
//// loadLogInfo("collectFace,TcpClientListener收到消息:$json")
//// val type = json?.getInt("type")
//// // 根据服务端消息类型处理删除新增临时用户人脸数据---------------------
//// when (type) {
//// LanServer.TYPE_ADD_FACE_DATA -> {
//// //采集设备id自己生成,不接收服务端的,在本地只判断是否存在同一人脸数据
//// loadLogInfo("collectFace,TcpClientListener收到消息,新增用户:采集设备id自己生成,不接收服务端的,在本地只判断是否存在同一人脸数据")
//// }
////
//// LanServer.TYPE_CLEAR_FACE_DATA -> {
//// clearFaceData()
//// }
////
//// else -> {}
//// }
//// }
//// })
//// tcpClient?.start()
////
//// try {
//// LanServer.getInstance().let {
//// it.setListener(object : LanServerListenerImpl() {
//// override fun onMessageReceived(clientId: String?, message: JSONObject?) {
//// super.onMessageReceived(clientId, message)
//// ToastUtils.showToast("收到消息clientId=$clientId")
//// }
//// })
//// it.start()
//// }
//// } catch (e: Exception) {
//// e.printStackTrace()
//// }
//// }
//
// private fun clearFaceData() {
// Thread {
// val faceDao = FaceDatabase.getInstance(this).faceDao()
// faceDao.deleteTempUserFaceData()
// recognizeViewModel.refreshFaceList()
// loadLogInfo("collectFace,TcpClientListener收到消息,已删除并刷新人脸数据")
// }.start()
// }
//
// private var isCollectFace = false
//
//}
@@ -1,62 +1,62 @@
package com.sw.platecabinet.activity //package com.sw.platecabinet.activity
//
import android.content.Intent //import android.content.Intent
import com.sw.plate.utils.ToastUtils //import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.member.databinding.ActivityLoginByPwdBinding //import com.sw.platecabinet.databinding.ActivityLoginByPwdBinding
import com.sw.platecabinet.member.databinding.ItemTitleTimeBinding //import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.dialog.BalanceNotEnoughDialog //import com.sw.platecabinet.dialog.BalanceNotEnoughDialog
import com.sw.platecabinet.model.request.LoginParam //import com.sw.platecabinet.model.request.LoginParam
import com.sw.platecabinet.model.response.EquipmentUserInfo //import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.utils.KeyboardUtils //import com.sw.platecabinet.utils.KeyboardUtils
//
/** ///**
* 密码登录 // * 密码登录
*/ // */
class LoginByPwdActivity : BaseActivity<ActivityLoginByPwdBinding>() { //class LoginByPwdActivity : BaseActivity<ActivityLoginByPwdBinding>() {
//
override fun inflateViewBinding(): ActivityLoginByPwdBinding { // override fun inflateViewBinding(): ActivityLoginByPwdBinding {
return ActivityLoginByPwdBinding.inflate(layoutInflater) // return ActivityLoginByPwdBinding.inflate(layoutInflater)
} // }
//
override fun inflateTitleBinding(): ItemTitleTimeBinding { // override fun inflateTitleBinding(): ItemTitleTimeBinding {
return binding.includeHeader // return binding.includeHeader
} // }
//
override fun initialize() { // override fun initialize() {
binding.btnLogin.setOnClickListener { // binding.btnLogin.setOnClickListener {
val phone = binding.etPhone.text.toString() // val phone = binding.etPhone.text.toString()
val pwd = binding.etPwd.text.toString() // val pwd = binding.etPwd.text.toString()
if (phone.isEmpty() || pwd.isEmpty()) { // if (phone.isEmpty() || pwd.isEmpty()) {
ToastUtils.showToast("手机或校验码不能为空") // ToastUtils.showToast("手机或校验码不能为空")
return@setOnClickListener // return@setOnClickListener
} // }
val loginParam = LoginParam( // val loginParam = LoginParam(
//equipmentId = equipmentId, // //equipmentId = equipmentId,
phone = phone, // phone = phone,
password = pwd // password = pwd
) // )
viewModel.loginWithPwd(loginParam) // viewModel.loginWithPwd(loginParam)
KeyboardUtils.hideKeyboard(this) // KeyboardUtils.hideKeyboard(this)
} // }
binding.tvFaceRec.setOnClickListener { // binding.tvFaceRec.setOnClickListener {
val intent = Intent(this, LoginByFaceActivity::class.java) // val intent = Intent(this, LoginByFaceActivity::class.java)
startActivity(intent) // startActivity(intent)
} // }
} // }
//
override fun onResume() { // override fun onResume() {
super.onResume() // super.onResume()
viewModel.resetUserInfo() // viewModel.resetUserInfo()
} // }
//
override fun handleLoginSuccess(equipmentUserInfo: EquipmentUserInfo, isAdmin: Boolean) { // override fun handleLoginSuccess(equipmentUserInfo: EquipmentUserInfo, isAdmin: Boolean) {
super.handleLoginSuccess(equipmentUserInfo, isAdmin) // super.handleLoginSuccess(equipmentUserInfo, isAdmin)
// val cardBalance = equipmentUserInfo.cardBalance?:0.toDouble() //// val cardBalance = equipmentUserInfo.cardBalance?:0.toDouble()
// val balanceIsNotEnough = cardBalance <= 0.toDouble() //// val balanceIsNotEnough = cardBalance <= 0.toDouble()
if (equipmentUserInfo.isIntercept) { // if (equipmentUserInfo.isIntercept) {
return // return
} // }
LoginByFaceActivity.goInitActivity() // LoginByFaceActivity.goInitActivity()
finish() // finish()
} // }
} //}
@@ -2,15 +2,14 @@ package com.sw.platecabinet.activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.sw.platecabinet.member.R import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.member.databinding.ActivityMainBinding import com.sw.platecabinet.R
import com.sw.platecabinet.member.databinding.ItemTitleTimeBinding import com.sw.platecabinet.databinding.ActivityMainBinding
import com.sw.platecabinet.fragment.BindPlateFragment import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.dialog.CustomDialog
import com.sw.platecabinet.fragment.PlateCabinetFullFragment import com.sw.platecabinet.fragment.PlateCabinetFullFragment
import com.sw.platecabinet.fragment.PlateOpenFragment import com.sw.platecabinet.fragment.PlateOpenFragment
import com.sw.platecabinet.fragment.SettingListFragment import com.sw.platecabinet.fragment.SettingListFragment
import com.sw.platecabinet.fragment.UnBindPlateFragment
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.utils.FragmentHelper import com.sw.platecabinet.utils.FragmentHelper
typealias Callback = (String) -> Unit typealias Callback = (String) -> Unit
@@ -34,36 +33,49 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
// 静态启动方法 // 静态启动方法
companion object { companion object {
private const val PARAM_PAGE_TYPE = "pageType" private const val PARAM_PAGE_TYPE = "pageType"
private const val PARAM_EQUIPMENT_INFO = "equipmentUserInfo" // private const val PARAM_EQUIPMENT_INFO = "equipmentUserInfo"
private const val PARAM_IS_ADMIN = "isAdmin" // private const val PARAM_IS_ADMIN = "isAdmin"
fun start( // fun start(
context: Context, // context: Context,
pageType: PageType, // pageType: PageType,
equipmentUserInfo: EquipmentUserInfo? = null, // equipmentUserInfo: EquipmentUserInfo? = null,
isAdmin: Boolean = false // isAdmin: Boolean = false,
) { // block:()->Unit={}
// ) {
// val intent = Intent(context, MainActivity::class.java)
// intent.putExtra(PARAM_PAGE_TYPE, pageType.name)
// intent.putExtra(PARAM_EQUIPMENT_INFO, equipmentUserInfo)
// intent.putExtra(PARAM_IS_ADMIN, isAdmin)
// context.startActivity(intent)
// }
fun start(context: Context, pageType: PageType, isScanCode: Boolean = false) {
val intent = Intent(context, MainActivity::class.java) val intent = Intent(context, MainActivity::class.java)
intent.putExtra(PARAM_PAGE_TYPE, pageType.name) intent.putExtra(PARAM_PAGE_TYPE, pageType.name)
intent.putExtra(PARAM_EQUIPMENT_INFO, equipmentUserInfo) intent.putExtra(PlateOpenFragment.IS_SCAN_CODE, isScanCode)
intent.putExtra(PARAM_IS_ADMIN, isAdmin)
context.startActivity(intent) context.startActivity(intent)
} }
} }
override fun initialize() { override fun initialize() {
val param = intent.getStringExtra(PARAM_PAGE_TYPE) val param = intent.getStringExtra(PARAM_PAGE_TYPE)
val isAdmin = intent.getBooleanExtra(PARAM_IS_ADMIN, false) // val isAdmin = intent.getBooleanExtra(PARAM_IS_ADMIN, false)
val equipmentUserInfo = intent.getParcelableExtra<EquipmentUserInfo>(PARAM_EQUIPMENT_INFO) val isScanCode = intent.getBooleanExtra(PlateOpenFragment.IS_SCAN_CODE, false)
// val equipmentUserInfo = intent.getParcelableExtra<EquipmentUserInfo>(PARAM_EQUIPMENT_INFO)
pageType = param?.let { PageType.valueOf(param) } ?: PageType.SETTING_LIST pageType = param?.let { PageType.valueOf(param) } ?: PageType.SETTING_LIST
fragmentHelper = FragmentHelper(supportFragmentManager, R.id.fragment_container) fragmentHelper = FragmentHelper(supportFragmentManager, R.id.fragment_container)
val page = when (pageType) { val page = when (pageType) {
PageType.SETTING_LIST -> SettingListFragment() PageType.SETTING_LIST -> SettingListFragment()
PageType.BIND_PLATE -> BindPlateFragment.newInstance(equipmentUserInfo) // PageType.BIND_PLATE -> BindPlateFragment.newInstance(equipmentUserInfo)
PageType.PLATE_TIP -> PlateOpenFragment.newInstance(equipmentUserInfo, isAdmin) // PageType.PLATE_TIP -> PlateOpenFragment.newInstance(equipmentUserInfo, isAdmin)
PageType.UNBIND_PLATE -> UnBindPlateFragment.newInstance(equipmentUserInfo) PageType.PLATE_TIP -> PlateOpenFragment.newInstance(isScanCode)
// PageType.UNBIND_PLATE -> UnBindPlateFragment.newInstance(equipmentUserInfo)
PageType.PLATE_CABINET_FULL -> PlateCabinetFullFragment() PageType.PLATE_CABINET_FULL -> PlateCabinetFullFragment()
else -> null
}
page?.let {
fragmentHelper.addFragment(it)
} }
fragmentHelper.addFragment(page)
} }
override fun onLeftDoubleClick() { override fun onLeftDoubleClick() {
@@ -73,7 +85,9 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
} }
override fun onRightDoubleClick() { override fun onRightDoubleClick() {
if (pageType == PageType.SETTING_LIST) {
startActivity(Intent(this, FunActivity::class.java))
}
} }
override fun handleScanKeyInfo(scanInfo: String) { override fun handleScanKeyInfo(scanInfo: String) {
@@ -89,9 +103,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
override fun registerDataChange() { override fun registerDataChange() {
super.registerDataChange() super.registerDataChange()
} }
}
}
/** /**
* 界面类型 * 界面类型
*/ */
@@ -10,7 +10,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.sw.platecabinet.member.R import com.sw.platecabinet.R
import kotlin.math.ceil import kotlin.math.ceil
class TestActivity : AppCompatActivity() { class TestActivity : AppCompatActivity() {
@@ -1,57 +1,57 @@
package com.sw.platecabinet.activity //package com.sw.platecabinet.activity
//
import android.graphics.Color //import android.graphics.Color
import android.view.Gravity //import android.view.Gravity
import android.view.ViewGroup //import android.view.ViewGroup
import androidx.core.graphics.drawable.toDrawable //import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.WindowCompat //import androidx.core.view.WindowCompat
import com.sw.platecabinet.GlobalKey //import com.sw.platecabinet.GlobalKey
import com.sw.platecabinet.member.databinding.ActivityUnbindDialogBinding //import com.sw.platecabinet.databinding.ActivityUnbindDialogBinding
import com.sw.platecabinet.member.databinding.ItemTitleTimeBinding //import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.ext.dp //import com.sw.platecabinet.ext.dp
import com.sw.platecabinet.model.response.EquipmentUserInfo //import com.sw.platecabinet.model.response.EquipmentUserInfo
//
/** ///**
* 解绑确认弹窗界面 // * 解绑确认弹窗界面
*/ // */
class UnBindDialogActivity : BaseActivity<ActivityUnbindDialogBinding>() { //class UnBindDialogActivity : BaseActivity<ActivityUnbindDialogBinding>() {
private var equipmentUserInfo: EquipmentUserInfo? = null // private var equipmentUserInfo: EquipmentUserInfo? = null
//
override fun inflateViewBinding(): ActivityUnbindDialogBinding { // override fun inflateViewBinding(): ActivityUnbindDialogBinding {
return ActivityUnbindDialogBinding.inflate(layoutInflater) // return ActivityUnbindDialogBinding.inflate(layoutInflater)
} // }
//
override fun inflateTitleBinding(): ItemTitleTimeBinding? { // override fun inflateTitleBinding(): ItemTitleTimeBinding? {
return null // return null
} // }
//
override fun initialize() { // override fun initialize() {
window?.apply { // window?.apply {
WindowCompat.setDecorFitsSystemWindows(this, false) // WindowCompat.setDecorFitsSystemWindows(this, false)
setBackgroundDrawable(Color.TRANSPARENT.toDrawable()) // setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
//
attributes = attributes.apply { // attributes = attributes.apply {
y = 314.dp // y = 314.dp
width = (context.resources.displayMetrics.widthPixels * 0.8).toInt() // width = (context.resources.displayMetrics.widthPixels * 0.8).toInt()
height = ViewGroup.LayoutParams.MATCH_PARENT // height = ViewGroup.LayoutParams.MATCH_PARENT
gravity = Gravity.CENTER // gravity = Gravity.CENTER
} // }
} // }
equipmentUserInfo = // equipmentUserInfo =
intent.getParcelableExtra<EquipmentUserInfo>(GlobalKey.PARAM_EQUIPMENT_INFO) // intent.getParcelableExtra<EquipmentUserInfo>(GlobalKey.PARAM_EQUIPMENT_INFO)
if (equipmentUserInfo == null) return // if (equipmentUserInfo == null) return
binding.tvTipInfo.text = "即将解除【${equipmentUserInfo!!.name}】的餐盘" // binding.tvTipInfo.text = "即将解除【${equipmentUserInfo!!.name}】的餐盘"
binding.tvCancel.setOnClickListener { // binding.tvCancel.setOnClickListener {
finish() // finish()
} // }
binding.tvConfirm.setOnClickListener { // binding.tvConfirm.setOnClickListener {
setResult(RESULT_OK) // setResult(RESULT_OK)
finish() // finish()
} // }
} // }
//
override fun finish() { // override fun finish() {
super.finish() // super.finish()
overridePendingTransition(0, 0) // 确保退出也无动画 // overridePendingTransition(0, 0) // 确保退出也无动画
} // }
} //}
@@ -6,7 +6,7 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.sw.platecabinet.member.R import com.sw.platecabinet.R
/** /**
* 通用分页适配器(支持ViewBinding * 通用分页适配器(支持ViewBinding
@@ -0,0 +1,34 @@
package com.sw.platecabinet.adapter
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.sw.platecabinet.databinding.ListItemLogInfoBinding
class LogAdapter(var list: MutableList<String>) : RecyclerView.Adapter<LogAdapter.ViewHolder>() {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.binding.tvLogText.text = list[position]
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding =
ListItemLogInfoBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun getItemCount() = list.size
inner class ViewHolder(var binding: ListItemLogInfoBinding) :
RecyclerView.ViewHolder(binding.root)
fun addData(logInfo: String) {
Handler(Looper.getMainLooper()).post {
// list.add(logInfo)
// notifyDataSetChanged()
}
}
}
@@ -5,7 +5,7 @@ import android.graphics.drawable.ColorDrawable
import android.os.Bundle import android.os.Bundle
import android.view.Window import android.view.Window
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.sw.platecabinet.member.databinding.DialogBalanceNotEnoughBinding import com.sw.platecabinet.databinding.DialogBalanceNotEnoughBinding
import com.sw.platecabinet.ext.dp import com.sw.platecabinet.ext.dp
open class BalanceNotEnoughDialog( open class BalanceNotEnoughDialog(
@@ -0,0 +1,48 @@
package com.sw.platecabinet.dialog
import android.app.Dialog
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.Window
import androidx.fragment.app.FragmentActivity
import com.sw.platecabinet.databinding.DialogCustomBinding
import com.sw.platecabinet.databinding.DialogUserBindRemindBinding
import com.sw.platecabinet.ext.dp
open class CustomDialog(
var activity: FragmentActivity,
var content: String,
var onCancel: () -> Unit = {},
var onConfirm: () -> Unit = {},
) : Dialog(activity) {
private lateinit var binding: DialogCustomBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
binding = DialogCustomBinding.inflate(layoutInflater)
setContentView(binding.root)
window?.run {
attributes = attributes.apply {
width = 480.dp
}
setBackgroundDrawable(ColorDrawable())
setCancelable(false)
}
binding.tvContent.text = content
addClickListener()
}
private fun addClickListener() {
binding.btnCancel.setOnClickListener {
onCancel()
dismiss()
}
binding.btnConfirm.setOnClickListener {
onConfirm()
dismiss()
}
}
}
@@ -5,7 +5,7 @@ import android.graphics.drawable.ColorDrawable
import android.os.Bundle import android.os.Bundle
import android.view.Window import android.view.Window
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.sw.platecabinet.member.databinding.DialogUserBindRemindBinding import com.sw.platecabinet.databinding.DialogUserBindRemindBinding
import com.sw.platecabinet.ext.dp import com.sw.platecabinet.ext.dp
open class UserBindRemindDialog( open class UserBindRemindDialog(
@@ -4,6 +4,11 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/** /**
* 设置 TextView 的双击和单击事件监听器 * 设置 TextView 的双击和单击事件监听器
@@ -31,3 +36,69 @@ fun TextView.setClickListeners(
}, doubleClickInterval) }, doubleClickInterval)
} }
} }
fun View.clickWithCoroutines(
doubleClickInterval: Long = 300,
onDoubleClick: (View) -> Unit = {},
onSingleClick: (View) -> Unit = {}
) {
var pendingItem: View? = null
var lastClickTime: Long = 0
var clickJob: Job? = null
setOnClickListener { view ->
val currentTime = System.currentTimeMillis()
// 如果是同一控件且在双击间隔内,则判定为双击
if (pendingItem === view && currentTime - lastClickTime < doubleClickInterval) {
// 取消之前的单击任务
clickJob?.cancel()
onDoubleClick(view)
pendingItem = null
} else {
// 取消之前的单击任务
clickJob?.cancel()
// 启动新的单击任务
clickJob = CoroutineScope(Dispatchers.Main).launch {
delay(doubleClickInterval)
onSingleClick(view)
}
pendingItem = view
lastClickTime = currentTime
}
}
}
//fun View.clickWithDebounce(delay: Long = 500, action: () -> Unit) {
// var job: Job? = null
// setOnClickListener {
// job?.cancel()
// job = CoroutineScope(Dispatchers.Main).launch {
// delay(delay)
// action()
// }
// }
//}
/**
* 极简版防重复点击扩展函数
* @param delay 防抖时间(默认300ms
* @param action 点击执行逻辑
*/
fun View.clickWithDebounce(delay: Long = 500, action: () -> Unit) {
setOnClickListener {
// 用View的tag存储是否可点击的状态(默认可点击)
if (tag as? Boolean ?: true) {
tag = false // 标记为不可点击
action() // 立即执行点击逻辑
// 启动协程,延迟后恢复可点击状态
CoroutineScope(Dispatchers.Main).launch {
delay(delay)
tag = true // 恢复可点击
}
}
}
}
@@ -11,7 +11,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.sw.platecabinet.member.R import com.sw.platecabinet.R
import com.sw.platecabinet.view.CustomLoadingDialog import com.sw.platecabinet.view.CustomLoadingDialog
import com.sw.platecabinet.viewmodel.SettingViewModel import com.sw.platecabinet.viewmodel.SettingViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -1,317 +1,317 @@
package com.sw.platecabinet.fragment //package com.sw.platecabinet.fragment
//
import android.annotation.SuppressLint //import android.annotation.SuppressLint
import android.os.Bundle //import android.os.Bundle
import android.text.Editable //import android.text.Editable
import android.text.TextUtils //import android.text.TextUtils
import android.text.TextWatcher //import android.text.TextWatcher
import android.view.inputmethod.EditorInfo //import android.view.inputmethod.EditorInfo
import androidx.fragment.app.viewModels //import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope //import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager //import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager //import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView //import androidx.recyclerview.widget.RecyclerView
import com.sw.plate.utils.ToastUtils //import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.comn.SerialApi //import com.sw.plate.utils.comn.SerialApi
import com.sw.plate.utils.comn.SerialPortManager //import com.sw.plate.utils.comn.SerialPortManager
import com.sw.platecabinet.member.R //import com.sw.platecabinet.R
import com.sw.platecabinet.activity.MainActivity //import com.sw.platecabinet.activity.MainActivity
import com.sw.platecabinet.activity.PageType //import com.sw.platecabinet.activity.PageType
import com.sw.platecabinet.adapter.GenericItemAdapter //import com.sw.platecabinet.adapter.GenericItemAdapter
import com.sw.platecabinet.adapter.GridSpacingItemDecoration //import com.sw.platecabinet.adapter.GridSpacingItemDecoration
import com.sw.platecabinet.adapter.dpToPx //import com.sw.platecabinet.adapter.dpToPx
import com.sw.platecabinet.member.databinding.FragmentBindPlateBinding //import com.sw.platecabinet.databinding.FragmentBindPlateBinding
import com.sw.platecabinet.member.databinding.ItemSearchUserInfoBinding //import com.sw.platecabinet.databinding.ItemSearchUserInfoBinding
import com.sw.platecabinet.dialog.UserBindRemindDialog //import com.sw.platecabinet.dialog.UserBindRemindDialog
import com.sw.platecabinet.ext.maskName //import com.sw.platecabinet.ext.maskName
import com.sw.platecabinet.ext.maskPhone //import com.sw.platecabinet.ext.maskPhone
import com.sw.platecabinet.model.ErrorInfo //import com.sw.platecabinet.model.ErrorInfo
import com.sw.platecabinet.model.request.BindParam //import com.sw.platecabinet.model.request.BindParam
import com.sw.platecabinet.model.response.EquipmentUserInfo //import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.SearchResult //import com.sw.platecabinet.model.response.SearchResult
import com.sw.platecabinet.utils.Debouncer //import com.sw.platecabinet.utils.Debouncer
import com.sw.platecabinet.utils.KeyboardUtils //import com.sw.platecabinet.utils.KeyboardUtils
import com.sw.platecabinet.viewmodel.SettingViewModel //import com.sw.platecabinet.viewmodel.SettingViewModel
import com.sw.platecabinet.viewmodel.UserViewModel //import com.sw.platecabinet.viewmodel.UserViewModel
import kotlinx.coroutines.launch //import kotlinx.coroutines.launch
import timber.log.Timber //import timber.log.Timber
//
/** ///**
* 餐盘柜绑定 // * 餐盘柜绑定
*/ // */
class BindPlateFragment : BaseFragment<FragmentBindPlateBinding>( //class BindPlateFragment : BaseFragment<FragmentBindPlateBinding>(
FragmentBindPlateBinding::inflate // FragmentBindPlateBinding::inflate
) { //) {
private lateinit var adapter: GenericItemAdapter<SearchResult.Member, ItemSearchUserInfoBinding> // private lateinit var adapter: GenericItemAdapter<SearchResult.Member, ItemSearchUserInfoBinding>
internal val KEY_ACTION_TYPE = "actionType" // internal val KEY_ACTION_TYPE = "actionType"
private var info: EquipmentUserInfo? = null // private var info: EquipmentUserInfo? = null
private var checkedItem: SearchResult.Member? = null // private var checkedItem: SearchResult.Member? = null
private var lastText = "" // private var lastText = ""
private val debouncer = Debouncer(2000) // private val debouncer = Debouncer(2000)
//
private val userViewModel by viewModels<UserViewModel>() // private val userViewModel by viewModels<UserViewModel>()
//
companion object { // companion object {
@JvmStatic // @JvmStatic
fun newInstance(userInfo: EquipmentUserInfo?) = // fun newInstance(userInfo: EquipmentUserInfo?) =
BindPlateFragment().apply { // BindPlateFragment().apply {
arguments = Bundle().apply { // arguments = Bundle().apply {
putParcelable(KEY_ACTION_TYPE, userInfo) // putParcelable(KEY_ACTION_TYPE, userInfo)
} // }
} // }
} // }
//
@SuppressLint("SetTextI18n") // @SuppressLint("SetTextI18n")
override fun initialize() { // override fun initialize() {
arguments?.let { args -> // arguments?.let { args ->
info = args.getParcelable(KEY_ACTION_TYPE) // info = args.getParcelable(KEY_ACTION_TYPE)
info?.let { // info?.let {
binding.tvNum.text = "${it.equipmentName}-${it.equipmentBoxCode}" // binding.tvNum.text = "${it.equipmentName}-${it.equipmentBoxCode}"
if (!TextUtils.isEmpty(it.plateNumber)) { // if (!TextUtils.isEmpty(it.plateNumber)) {
// binding.etCode.text = Editable.Factory.getInstance().newEditable(it.plateNumber) //// binding.etCode.text = Editable.Factory.getInstance().newEditable(it.plateNumber)
binding.etCode.apply { // binding.etCode.apply {
setText(it.plateNumber) // setText(it.plateNumber)
setSelection(length()) // setSelection(length())
} // }
} // }
}
}
initView()
initListener()
}
private fun initView() {
adapter = createAdapter()
binding.recyclerview.layoutManager = GridLayoutManager(context, 2)
// 添加间距装饰(12dp)
binding.recyclerview.addItemDecoration(
GridSpacingItemDecoration(
spanCount = 2,
spacing = requireContext().dpToPx(6),
includeEdge = false // 包含边缘间距
)
)
binding.recyclerview.adapter = adapter
binding.recyclerview.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
val totalItems = layoutManager.itemCount
if (!viewModel.isLoading && viewModel.canLoadMore
&& lastVisibleItem >= totalItems - 3
) {
viewModel.getSearchMemberList(
param = lastText,
pageNum = viewModel.currentPage
)
}
}
})
(activity as MainActivity).registerKeyEventInfo {
// binding.etCode.text.clear()
// binding.etCode.postDelayed({
// binding.etCode.text.clear()
// binding.etCode.text = Editable.Factory.getInstance().newEditable(it)
activity?.runOnUiThread {
if (binding.etUserInfo.isFocused) {
return@runOnUiThread
}
binding.etCode.apply {
if (it.length > 6) {
setText(it.substring(0, 6))
} else {
setText(it)
}
if (length() > 0) {
setSelection(length())
}
}
binding.etUserInfo.requestFocus()
}
// }, 50)
}
}
private fun initListener() {
binding.etCode.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) {
Timber.d("beforeTextChanged s = ${s.toString()}, start = $start, count = $count, after = $after")
}
override fun onTextChanged(
s: CharSequence?,
start: Int,
before: Int,
count: Int
) {
Timber.d("onTextChanged s = ${s.toString()}, start = $start, count = $count, before = $before")
}
override fun afterTextChanged(s: Editable?) {
Timber.d("afterTextChanged s = ${s.toString()}")
}
})
binding.etUserInfo.setOnEditorActionListener { _, actionId, keyEvent ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
val currentText = binding.etUserInfo.text.toString()
val containsNewlines = currentText.contains('\n') || currentText.contains('\r')
if (containsNewlines) {
Timber.d("非用户主动搜索")
true
}
debouncer.debounce {
// 执行搜索操作
viewModel.getSearchMemberList(param = currentText, pageNum = 1)
// 隐藏键盘
KeyboardUtils.hideKeyboard(requireActivity())
}
true // 表示已处理该事件
} else {
false // 未处理其他动作
}
}
binding.ivSearch.setOnClickListener {
debouncer.debounce {
// 执行搜索操作
viewModel.getSearchMemberList(
param = binding.etUserInfo.text.toString(),
pageNum = 1
)
// 隐藏键盘
KeyboardUtils.hideKeyboard(requireActivity())
}
}
binding.llBind.setOnClickListener {
if (equipmentBoxCode.isNullOrBlank().not()) {
showBindDialog()
return@setOnClickListener
}
if (info == null || info!!.equipmentCode?.isEmpty() == true || info!!.equipmentBoxCode?.isEmpty() == true) {
ToastUtils.showToast("传入的设备信息异常")
return@setOnClickListener
}
val plateNumber = binding.etCode.text.toString()
if (plateNumber.isEmpty()) {
ToastUtils.showToast("请输入餐盘号")
return@setOnClickListener
}
if (checkedItem == null) {
ToastUtils.showToast("请选择要绑定的会员")
return@setOnClickListener
}
if (checkedItem!!.id == null) {
ToastUtils.showToast("选中的会员信息异常")
return@setOnClickListener
}
val bindParam = BindParam(
equipmentId = info?.equipmentId,
equipmentCode = info?.equipmentCode,
equipmentBoxCode = info?.equipmentBoxCode,
faceId = checkedItem?.faceId,
plateNumber = plateNumber
)
viewModel.bindPlate(bindParam) { pair ->
updateBindState(pair)
}
KeyboardUtils.hideKeyboard(requireActivity())
}
binding.tvBack.setOnClickListener { activity?.finish() }
}
/**
* 监听数据变化
*/
override fun registerDataChange() {
super.registerDataChange()
lifecycleScope.launch {
viewModel.searchMemberList.collect {
adapter.updateData(it)
}
}
// lifecycleScope.launch {
// viewModel.bindStateChange.collect {
// updateBindState(it)
// } // }
// } // }
} // initView()
// initListener()
private fun createAdapter(): GenericItemAdapter<SearchResult.Member, ItemSearchUserInfoBinding> { // }
return GenericItemAdapter( //
items = emptyList(), // private fun initView() {
bindingInflater = ItemSearchUserInfoBinding::inflate, // adapter = createAdapter()
bindCallback = { item, position -> // binding.recyclerview.layoutManager = GridLayoutManager(context, 2)
this.tvName.text = item.name.maskName() // // 添加间距装饰(12dp)
this.tvPhone.text = item.phone.maskPhone() // binding.recyclerview.addItemDecoration(
if (item.id != checkedItem?.id) { // GridSpacingItemDecoration(
this.llRoot.setBackgroundResource(R.drawable.grid_item_bind) // spanCount = 2,
} else { // spacing = requireContext().dpToPx(6),
this.llRoot.setBackgroundResource(R.drawable.grid_item_unbind) // includeEdge = false // 包含边缘间距
} // )
// )
this.llRoot.setOnClickListener { // binding.recyclerview.adapter = adapter
Timber.d("itemClick ${item.name}, position = $position") // binding.recyclerview.addOnScrollListener(object : RecyclerView.OnScrollListener() {
checkedItem = item // override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
adapter.notifyDataSetChanged() // super.onScrolled(recyclerView, dx, dy)
//TODO 调用接口查询用户是否已绑盘 // val layoutManager = recyclerView.layoutManager as LinearLayoutManager
this@BindPlateFragment.equipmentBoxCode = null // val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
(activity as? MainActivity)?.showWaitingDialog("查询中,请稍后……") // val totalItems = layoutManager.itemCount
userViewModel.getUserInfoById(memberId = item.faceId) { //
(activity as? MainActivity)?.hideWaitingDialog() // if (!viewModel.isLoading && viewModel.canLoadMore
this@BindPlateFragment.equipmentBoxCode = it?.equipmentBoxCode // && lastVisibleItem >= totalItems - 3
} // ) {
} // viewModel.getSearchMemberList(
} // param = lastText,
) // pageNum = viewModel.currentPage
} // )
// }
private var equipmentBoxCode:String? = null
private fun showBindDialog() {
UserBindRemindDialog(
activity = requireActivity(),
content = "您已绑定编号为${equipmentBoxCode}的柜子,请解绑后重试"
)
.show()
}
private fun updateBindState(pair: Pair<Boolean?, ErrorInfo>){
if (pair.first == null) return // 解绑状态过滤
val errorInfo = pair.second
if (errorInfo.isSuccess()) {
if (errorInfo.equipmentUserInfo == null) {
ToastUtils.showToast("数据异常")
return
}
// if (errorInfo.equipmentUserInfo.isOtherEquipment()) {
// MainActivity.start(
// requireContext(),
// pageType = PageType.PLATE_TIP,
// equipmentUserInfo = errorInfo.equipmentUserInfo,
// isAdmin = true
// )
// } else {
SerialApi.openPlate(
errorInfo.equipmentUserInfo.equipmentBoxCode!!.toInt(),
object : SerialPortManager.SendCallback {
override fun onSuccess() {
ToastUtils.showToast("绑定成功")
activity?.finish()
}
override fun onFail(e: Exception?) {
ToastUtils.showToast("绑定失败")
}
})
// } // }
} else { // })
ToastUtils.showToast(errorInfo.msg) // (activity as MainActivity).registerKeyEventInfo {
} //// binding.etCode.text.clear()
} //// binding.etCode.postDelayed({
} //// binding.etCode.text.clear()
//// binding.etCode.text = Editable.Factory.getInstance().newEditable(it)
// activity?.runOnUiThread {
// if (binding.etUserInfo.isFocused) {
// return@runOnUiThread
// }
// binding.etCode.apply {
// if (it.length > 6) {
// setText(it.substring(0, 6))
// } else {
// setText(it)
// }
// if (length() > 0) {
// setSelection(length())
// }
// }
// binding.etUserInfo.requestFocus()
// }
//// }, 50)
// }
// }
//
// private fun initListener() {
// binding.etCode.addTextChangedListener(object : TextWatcher {
// override fun beforeTextChanged(
// s: CharSequence?,
// start: Int,
// count: Int,
// after: Int
// ) {
// Timber.d("beforeTextChanged s = ${s.toString()}, start = $start, count = $count, after = $after")
// }
//
// override fun onTextChanged(
// s: CharSequence?,
// start: Int,
// before: Int,
// count: Int
// ) {
// Timber.d("onTextChanged s = ${s.toString()}, start = $start, count = $count, before = $before")
// }
//
// override fun afterTextChanged(s: Editable?) {
// Timber.d("afterTextChanged s = ${s.toString()}")
// }
// })
// binding.etUserInfo.setOnEditorActionListener { _, actionId, keyEvent ->
// if (actionId == EditorInfo.IME_ACTION_SEARCH) {
// val currentText = binding.etUserInfo.text.toString()
// val containsNewlines = currentText.contains('\n') || currentText.contains('\r')
// if (containsNewlines) {
// Timber.d("非用户主动搜索")
// true
// }
// debouncer.debounce {
// // 执行搜索操作
// viewModel.getSearchMemberList(param = currentText, pageNum = 1)
// // 隐藏键盘
// KeyboardUtils.hideKeyboard(requireActivity())
// }
// true // 表示已处理该事件
// } else {
// false // 未处理其他动作
// }
// }
// binding.ivSearch.setOnClickListener {
// debouncer.debounce {
// // 执行搜索操作
// viewModel.getSearchMemberList(
// param = binding.etUserInfo.text.toString(),
// pageNum = 1
// )
// // 隐藏键盘
// KeyboardUtils.hideKeyboard(requireActivity())
// }
// }
// binding.llBind.setOnClickListener {
// if (equipmentBoxCode.isNullOrBlank().not()) {
// showBindDialog()
// return@setOnClickListener
// }
// if (info == null || info!!.equipmentCode?.isEmpty() == true || info!!.equipmentBoxCode?.isEmpty() == true) {
// ToastUtils.showToast("传入的设备信息异常")
// return@setOnClickListener
// }
// val plateNumber = binding.etCode.text.toString()
// if (plateNumber.isEmpty()) {
// ToastUtils.showToast("请输入餐盘号")
// return@setOnClickListener
// }
// if (checkedItem == null) {
// ToastUtils.showToast("请选择要绑定的会员")
// return@setOnClickListener
// }
// if (checkedItem!!.id == null) {
// ToastUtils.showToast("选中的会员信息异常")
// return@setOnClickListener
// }
// val bindParam = BindParam(
// equipmentId = info?.equipmentId,
// equipmentCode = info?.equipmentCode,
// equipmentBoxCode = info?.equipmentBoxCode,
// faceId = checkedItem?.faceId,
// plateNumber = plateNumber
// )
// viewModel.bindPlate(bindParam) { pair ->
// updateBindState(pair)
// }
// KeyboardUtils.hideKeyboard(requireActivity())
// }
// binding.tvBack.setOnClickListener { activity?.finish() }
// }
//
// /**
// * 监听数据变化
// */
// override fun registerDataChange() {
// super.registerDataChange()
// lifecycleScope.launch {
// viewModel.searchMemberList.collect {
// adapter.updateData(it)
// }
// }
//// lifecycleScope.launch {
//// viewModel.bindStateChange.collect {
//// updateBindState(it)
//// }
//// }
// }
//
// private fun createAdapter(): GenericItemAdapter<SearchResult.Member, ItemSearchUserInfoBinding> {
// return GenericItemAdapter(
// items = emptyList(),
// bindingInflater = ItemSearchUserInfoBinding::inflate,
// bindCallback = { item, position ->
// this.tvName.text = item.name.maskName()
// this.tvPhone.text = item.phone.maskPhone()
// if (item.id != checkedItem?.id) {
// this.llRoot.setBackgroundResource(R.drawable.grid_item_bind)
// } else {
// this.llRoot.setBackgroundResource(R.drawable.grid_item_unbind)
// }
//
// this.llRoot.setOnClickListener {
// Timber.d("itemClick ${item.name}, position = $position")
// checkedItem = item
// adapter.notifyDataSetChanged()
// //TODO 调用接口查询用户是否已绑盘
// this@BindPlateFragment.equipmentBoxCode = null
// (activity as? MainActivity)?.showWaitingDialog("查询中,请稍后……")
// userViewModel.getUserInfoById(memberId = item.faceId) {
// (activity as? MainActivity)?.hideWaitingDialog()
// this@BindPlateFragment.equipmentBoxCode = it?.equipmentBoxCode
// }
// }
// }
// )
// }
//
// private var equipmentBoxCode:String? = null
//
// private fun showBindDialog() {
// UserBindRemindDialog(
// activity = requireActivity(),
// content = "您已绑定编号为${equipmentBoxCode}的柜子,请解绑后重试"
// )
// .show()
// }
//
// private fun updateBindState(pair: Pair<Boolean?, ErrorInfo>){
// if (pair.first == null) return // 解绑状态过滤
// val errorInfo = pair.second
// if (errorInfo.isSuccess()) {
// if (errorInfo.equipmentUserInfo == null) {
// ToastUtils.showToast("数据异常")
// return
// }
//// if (errorInfo.equipmentUserInfo.isOtherEquipment()) {
//// MainActivity.start(
//// requireContext(),
//// pageType = PageType.PLATE_TIP,
//// equipmentUserInfo = errorInfo.equipmentUserInfo,
//// isAdmin = true
//// )
//// } else {
// SerialApi.openPlate(
// errorInfo.equipmentUserInfo.equipmentBoxCode!!.toInt(),
// object : SerialPortManager.SendCallback {
// override fun onSuccess() {
// ToastUtils.showToast("绑定成功")
// activity?.finish()
// }
//
// override fun onFail(e: Exception?) {
// ToastUtils.showToast("绑定失败")
// }
// })
//// }
// } else {
// ToastUtils.showToast(errorInfo.msg)
// }
// }
//}
@@ -2,7 +2,7 @@ package com.sw.platecabinet.fragment
import android.os.CountDownTimer import android.os.CountDownTimer
import com.sw.platecabinet.Constants import com.sw.platecabinet.Constants
import com.sw.platecabinet.member.databinding.FragmentPlateCabinetFullBinding import com.sw.platecabinet.databinding.FragmentPlateCabinetFullBinding
import timber.log.Timber import timber.log.Timber
/** /**
@@ -4,9 +4,9 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.CountDownTimer import android.os.CountDownTimer
import com.sw.platecabinet.Constants import com.sw.platecabinet.Constants
import com.sw.platecabinet.member.R import com.sw.platecabinet.R
import com.sw.platecabinet.activity.InitActivity import com.sw.platecabinet.activity.InitActivity
import com.sw.platecabinet.member.databinding.FragmentPlateOpenBinding import com.sw.platecabinet.databinding.FragmentPlateOpenBinding
import com.sw.platecabinet.model.response.EquipmentUserInfo import com.sw.platecabinet.model.response.EquipmentUserInfo
/** /**
@@ -15,44 +15,61 @@ import com.sw.platecabinet.model.response.EquipmentUserInfo
class PlateOpenFragment : class PlateOpenFragment :
BaseFragment<FragmentPlateOpenBinding>(FragmentPlateOpenBinding::inflate) { BaseFragment<FragmentPlateOpenBinding>(FragmentPlateOpenBinding::inflate) {
private var totalTimeInMillis: Long = Constants.AUTO_CLOSE_TIME * 1000 private var totalTimeInMillis: Long = Constants.AUTO_CLOSE_TIME * 1000
internal val KEY_ACTION_TYPE = "actionType" // internal val KEY_ACTION_TYPE = "actionType"
internal val PARAM_IS_ADMIN = "isAdmin" // internal val PARAM_IS_ADMIN = "isAdmin"
private var info: EquipmentUserInfo? = null // private var info: EquipmentUserInfo? = null
private var isAdmin: Boolean = false // private var isAdmin: Boolean = false
private var isScanCode = false
companion object { companion object {
const val IS_SCAN_CODE = "isScanCode"
// @JvmStatic
// fun newInstance(userInfo: EquipmentUserInfo?, isAdmin: Boolean) =
// PlateOpenFragment().apply {
// arguments = Bundle().apply {
//// putParcelable(KEY_ACTION_TYPE, userInfo)
//// putBoolean(PARAM_IS_ADMIN, isAdmin)
// }
// }
@JvmStatic @JvmStatic
fun newInstance(userInfo: EquipmentUserInfo?, isAdmin: Boolean) = fun newInstance(isScanCode: Boolean) =
PlateOpenFragment().apply { PlateOpenFragment().apply {
arguments = Bundle().apply { arguments = Bundle().apply {
putParcelable(KEY_ACTION_TYPE, userInfo) putBoolean(IS_SCAN_CODE, isScanCode)
putBoolean(PARAM_IS_ADMIN, isAdmin)
} }
} }
} }
override fun initialize() { override fun initialize() {
arguments?.let { arguments?.let {
info = it.getParcelable(KEY_ACTION_TYPE) // info = it.getParcelable(KEY_ACTION_TYPE)
isAdmin = it.getBoolean(PARAM_IS_ADMIN) // isAdmin = it.getBoolean(PARAM_IS_ADMIN)
info?.let { // info?.let {
if (!it.hasEquipmentBox()) { // if (!it.hasEquipmentBox()) {
binding.ivType.setImageResource(R.drawable.ic_tip_warn) // binding.ivType.setImageResource(R.drawable.ic_tip_warn)
binding.tvTitle.text = "暂无餐盘信息" // binding.tvTitle.text = "暂无餐盘信息"
binding.tvSubTitle.text = "请联系管理员" // binding.tvSubTitle.text = "请联系管理员"
binding.tvTitle.setTextColor(requireContext().getColor(R.color.tip_title_fail)) // binding.tvTitle.setTextColor(requireContext().getColor(R.color.tip_title_fail))
} else if (it.isOtherEquipment()) { // }
binding.ivType.setImageResource(R.drawable.ic_tip_warn) //// else if (it.isOtherEquipment()) {
binding.tvTitle.text = "暂无餐盘信息" //// binding.ivType.setImageResource(R.drawable.ic_tip_warn)
binding.tvSubTitle.text = "您的餐盘绑定在${it.equipmentName ?: " "}号柜上" //// binding.tvTitle.text = "暂无餐盘信息"
binding.tvTitle.setTextColor(requireContext().getColor(R.color.tip_title_fail)) //// binding.tvSubTitle.text = "您的餐盘绑定在${it.equipmentName ?: " "}号柜上"
} else { //// binding.tvTitle.setTextColor(requireContext().getColor(R.color.tip_title_fail))
binding.ivType.setImageResource(R.drawable.ic_tip_success) //// }
binding.tvTitle.text = "柜门开启" // else {
binding.tvSubTitle.text = "餐盘取出后请关闭柜门" // binding.ivType.setImageResource(R.drawable.ic_tip_success)
binding.tvTitle.setTextColor(requireContext().getColor(R.color.tip_title_success)) // binding.tvTitle.text = "柜门开启"
} // binding.tvSubTitle.text = "餐盘取出后请关闭柜门"
} // binding.tvTitle.setTextColor(requireContext().getColor(R.color.tip_title_success))
// }
// }
isScanCode = it.getBoolean(IS_SCAN_CODE, false)
binding.ivType.setImageResource(R.drawable.ic_tip_success)
binding.tvTitle.text = "柜门开启"
binding.tvSubTitle.text = if (isScanCode) "餐盘放入后请关闭柜门" else "餐盘取出后请关闭柜门"
binding.tvTitle.setTextColor(requireContext().getColor(R.color.tip_title_success))
} }
initCountTime() initCountTime()
@@ -66,10 +83,11 @@ class PlateOpenFragment :
override fun onFinish() { override fun onFinish() {
activity?.finish() activity?.finish()
if (!isAdmin) { // if (!isAdmin) {
val intent = Intent(context, InitActivity::class.java) val intent = Intent(context, InitActivity::class.java)
startActivity(intent) startActivity(intent)
} // }
} }
}.start() }.start()
} }
@@ -11,19 +11,23 @@ import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.comn.SerialApi import com.sw.plate.utils.comn.SerialApi
import com.sw.plate.utils.comn.SerialPortManager import com.sw.plate.utils.comn.SerialPortManager
import com.sw.platecabinet.GlobalData import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.member.R import com.sw.platecabinet.MyApp
import com.sw.platecabinet.R
import com.sw.platecabinet.activity.BaseActivity
import com.sw.platecabinet.activity.MainActivity import com.sw.platecabinet.activity.MainActivity
import com.sw.platecabinet.activity.PageType import com.sw.platecabinet.activity.PageType
import com.sw.platecabinet.adapter.GenericItemAdapter import com.sw.platecabinet.adapter.GenericItemAdapter
import com.sw.platecabinet.adapter.GenericPageAdapter import com.sw.platecabinet.adapter.GenericPageAdapter
import com.sw.platecabinet.adapter.GridSpacingItemDecoration import com.sw.platecabinet.adapter.GridSpacingItemDecoration
import com.sw.platecabinet.adapter.dpToPx import com.sw.platecabinet.adapter.dpToPx
import com.sw.platecabinet.member.databinding.FragmentSettingListBinding import com.sw.platecabinet.databinding.FragmentSettingListBinding
import com.sw.platecabinet.member.databinding.ItemBindViewBinding import com.sw.platecabinet.databinding.ItemBindViewBinding
import com.sw.platecabinet.ext.formatNumber import com.sw.platecabinet.ext.formatNumber
import com.sw.platecabinet.ext.maskName import com.sw.platecabinet.ext.maskName
import com.sw.platecabinet.model.response.EquipmentUserInfo import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.utils.ListArrangementUtil import com.sw.platecabinet.utils.ListArrangementUtil
import com.sw.platecabinet.utils.PlateUtils
import com.sw.platecabinet.utils.SpTool
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -80,11 +84,23 @@ class SettingListFragment :
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
viewModel.getEquipmentList{ // viewModel.getEquipmentList{
// updateAdapter(it)
// }
MyApp.plateList?.let {
updateAdapter(it) updateAdapter(it)
} }
} }
// private fun getPlateData(block: (List<EquipmentUserInfo>) -> Unit) {
// MyApp.plateList?.let {
// block(it)
// }
//// viewModel.getEquipmentList { items ->
//// block(items)
//// }
// }
private fun createItemAdapter(): GenericItemAdapter<EquipmentUserInfo, ItemBindViewBinding> { private fun createItemAdapter(): GenericItemAdapter<EquipmentUserInfo, ItemBindViewBinding> {
return GenericItemAdapter( return GenericItemAdapter(
items = emptyList(), items = emptyList(),
@@ -110,31 +126,33 @@ class SettingListFragment :
item: EquipmentUserInfo, item: EquipmentUserInfo,
position: Int position: Int
) { ) {
if (item.isPlaceholder() == true) { // if (item.isPlaceholder()) {
this.llRoot.isVisible = false // this.llRoot.isVisible = false
return // return
} // }
this.llRoot.isVisible = true this.llRoot.isVisible = true
this.tvNum.text = formatNumber(item.equipmentBoxCode ?: "") this.tvNum.text = formatNumber(item.equipmentBoxCode ?: "")
if (item.isBound()) { if (item.plateNumber.isNullOrBlank().not()) {
val date = DateTimeUtils.parseDateTime(item.updateTime) // val date = DateTimeUtils.parseDateTime(item.updateTime)
val timeInMillis = date?.time ?: 0L // val timeInMillis = date?.time ?: 0L
val isOldTime = DateTimeUtils.isMoreThanHoursFromNow(timeInMillis) // val isOldTime = DateTimeUtils.isMoreThanHoursFromNow(timeInMillis)
this.llRoot.setBackgroundResource(R.drawable.grid_item_bind) this.llRoot.setBackgroundResource(R.drawable.grid_item_bind)
this.tvNum.setTextColor(resources.getColor(R.color.bind_4E535D)) this.tvNum.setTextColor(resources.getColor(R.color.bind_4E535D))
this.tvLastTime.setTextColor( // this.tvLastTime.setTextColor(
if (isOldTime) resources.getColor(R.color.bind_time_old) else resources.getColor( // if (isOldTime) resources.getColor(R.color.bind_time_old) else resources.getColor(
R.color.bind_585868 // R.color.bind_585868
) // )
) // )
this.llOpen.setBackgroundResource(R.drawable.grid_button_bind) this.llOpen.setBackgroundResource(R.drawable.grid_button_bind)
this.tvOpen.setTextColor(resources.getColor(R.color.bind_FFCC99)) this.tvOpen.setTextColor(resources.getColor(R.color.bind_FFCC99))
this.llBindInfo.visibility = View.VISIBLE this.llBindInfo.visibility = View.VISIBLE
this.tvUnbind.visibility = View.GONE this.tvUnbind.visibility = View.GONE
this.tvName.text = item.name.maskName() // this.tvName.text = item.name.maskName()
this.tvLastTime.text = DateTimeUtils.getTimeAgo(date) this.tvName.text = item.plateNumber
// this.tvLastTime.text = DateTimeUtils.getTimeAgo(date)
} else { } else {
this.llRoot.setBackgroundResource(R.drawable.grid_item_unbind) this.llRoot.setBackgroundResource(R.drawable.grid_item_unbind)
this.tvNum.setTextColor(resources.getColor(R.color.bind_FFCC99)) this.tvNum.setTextColor(resources.getColor(R.color.bind_FFCC99))
@@ -142,46 +160,32 @@ class SettingListFragment :
this.tvOpen.setTextColor(resources.getColor(R.color.unbind_32283C)) this.tvOpen.setTextColor(resources.getColor(R.color.unbind_32283C))
this.llBindInfo.visibility = View.GONE this.llBindInfo.visibility = View.GONE
this.tvUnbind.visibility = View.VISIBLE this.tvUnbind.visibility = View.VISIBLE
// this.tvUnbind.text = "未绑定"
} }
this.llRoot.setOnClickListener { // this.llRoot.setOnClickListener {
Timber.d("itemClick llRoot ${item.name}, position = $position") //// Timber.d("itemClick llRoot ${item.name}, position = $position")
if (item.isBound()) { // if (item.isBound()) {
MainActivity.start( // MainActivity.start(
requireContext(), // requireContext(),
pageType = PageType.UNBIND_PLATE, // pageType = PageType.UNBIND_PLATE,
equipmentUserInfo = item // equipmentUserInfo = item
) // )
} else { // } else {
MainActivity.start( // MainActivity.start(
requireContext(), // requireContext(),
pageType = PageType.BIND_PLATE, // pageType = PageType.BIND_PLATE,
equipmentUserInfo = item // equipmentUserInfo = item
) // )
} // }
} // }
this.llOpen.setOnClickListener { this.llOpen.setOnClickListener {
Timber.d("itemClick llOpen equipmentBoxCode = ${item.equipmentBoxCode}") // item.faceId = null
val equipmentBoxCode: Int = item.equipmentBoxCode?.toInt() ?: -1 PlateUtils.open(
if (equipmentBoxCode < 0) { fragmentActivity = requireActivity(),
ToastUtils.showToast("餐盘柜编号错误") code = item.equipmentBoxCode,
return@setOnClickListener isScanCode = false,
} isSetting = true
showWaitingDialog("请稍等") )
SerialApi.openPlate(equipmentBoxCode, object : SerialPortManager.SendCallback {
override fun onSuccess() {
hideWaitingDialog()
MainActivity.start(
requireContext(),
pageType = PageType.PLATE_TIP,
isAdmin = true
)
}
override fun onFail(e: Exception?) {
hideWaitingDialog()
ToastUtils.showToast("柜门打开失败")
}
})
} }
} }
@@ -1,95 +1,95 @@
package com.sw.platecabinet.fragment //package com.sw.platecabinet.fragment
//
import android.app.Activity //import android.app.Activity
import android.content.Intent //import android.content.Intent
import android.os.Bundle //import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts //import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.lifecycleScope //import androidx.lifecycle.lifecycleScope
import com.sw.inbound.utils.DateTimeUtils //import com.sw.inbound.utils.DateTimeUtils
import com.sw.plate.utils.ToastUtils //import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.GlobalKey //import com.sw.platecabinet.GlobalKey
import com.sw.platecabinet.activity.UnBindDialogActivity //import com.sw.platecabinet.activity.UnBindDialogActivity
import com.sw.platecabinet.member.databinding.FragmentUnbindPlateBinding //import com.sw.platecabinet.databinding.FragmentUnbindPlateBinding
import com.sw.platecabinet.ext.maskName //import com.sw.platecabinet.ext.maskName
import com.sw.platecabinet.ext.maskPhone //import com.sw.platecabinet.ext.maskPhone
import com.sw.platecabinet.model.ErrorInfo //import com.sw.platecabinet.model.ErrorInfo
import com.sw.platecabinet.model.response.EquipmentUserInfo //import com.sw.platecabinet.model.response.EquipmentUserInfo
import kotlinx.coroutines.launch //import kotlinx.coroutines.launch
//
/** ///**
* 餐盘柜绑定 // * 餐盘柜绑定
*/ // */
class UnBindPlateFragment private constructor() : BaseFragment<FragmentUnbindPlateBinding>( //class UnBindPlateFragment private constructor() : BaseFragment<FragmentUnbindPlateBinding>(
FragmentUnbindPlateBinding::inflate // FragmentUnbindPlateBinding::inflate
) { //) {
internal val ARG_PARAM1 = "param1" // internal val ARG_PARAM1 = "param1"
private var info: EquipmentUserInfo? = null // private var info: EquipmentUserInfo? = null
//
companion object { // companion object {
@JvmStatic // @JvmStatic
fun newInstance(param1: EquipmentUserInfo?) = // fun newInstance(param1: EquipmentUserInfo?) =
UnBindPlateFragment().apply { // UnBindPlateFragment().apply {
arguments = Bundle().apply { // arguments = Bundle().apply {
putParcelable(ARG_PARAM1, param1) // putParcelable(ARG_PARAM1, param1)
} // }
} // }
} // }
//
private val dialogLauncher = registerForActivityResult( // private val dialogLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult() // ActivityResultContracts.StartActivityForResult()
) { result -> // ) { result ->
if (result.resultCode == Activity.RESULT_OK) { // if (result.resultCode == Activity.RESULT_OK) {
viewModel.unbindPlate(info?.id) { pair -> // viewModel.unbindPlate(info?.id) { pair ->
updateBindState(pair) // updateBindState(pair)
}
}
}
override fun initialize() {
arguments?.let { args ->
info = args.getParcelable(ARG_PARAM1)
info?.let {
binding.tvNum.text = "${it.equipmentName}-${it.equipmentBoxCode}"
binding.tvPlateNumberValue.text = it.plateNumber
binding.tvNameValue.text = it.name.maskName()
binding.tvPhoneValue.text = it.phone.maskPhone()
val timeInfo = DateTimeUtils.parseDateTime(it.updateTime)
val timeAgo = DateTimeUtils.getTimeAgo(timeInfo)
binding.tvUpdateTimeValue.text = timeAgo
}
}
binding.llUnbind.setOnClickListener {
if (info == null) return@setOnClickListener
val intent = Intent(requireContext(), UnBindDialogActivity::class.java).apply {
putExtra(GlobalKey.PARAM_EQUIPMENT_INFO, info)
}
dialogLauncher.launch(intent)
requireActivity().overridePendingTransition(0, 0)
}
binding.tvBack.setOnClickListener {
activity?.finish()
}
}
override fun registerDataChange() {
super.registerDataChange()
// lifecycleScope.launch {
// viewModel.bindStateChange.collect {
// updateBindState(it)
// } // }
// } // }
} // }
private fun updateBindState(pair: Pair<Boolean?, ErrorInfo>){ //
if (pair.first == null) return // 绑定状态过滤 // override fun initialize() {
val errorInfo = pair.second // arguments?.let { args ->
if (errorInfo.isSuccess()) { // info = args.getParcelable(ARG_PARAM1)
ToastUtils.showToast("解绑成功") // info?.let {
// activity?.finish() // binding.tvNum.text = "${it.equipmentName}-${it.equipmentBoxCode}"
// 关闭宿主 Activity // binding.tvPlateNumberValue.text = it.plateNumber
requireActivity().finish() // binding.tvNameValue.text = it.name.maskName()
requireActivity().overridePendingTransition(0, 0) // binding.tvPhoneValue.text = it.phone.maskPhone()
} else { // val timeInfo = DateTimeUtils.parseDateTime(it.updateTime)
ToastUtils.showToast(errorInfo.msg) // val timeAgo = DateTimeUtils.getTimeAgo(timeInfo)
} // binding.tvUpdateTimeValue.text = timeAgo
} // }
} // }
// binding.llUnbind.setOnClickListener {
// if (info == null) return@setOnClickListener
// val intent = Intent(requireContext(), UnBindDialogActivity::class.java).apply {
// putExtra(GlobalKey.PARAM_EQUIPMENT_INFO, info)
// }
// dialogLauncher.launch(intent)
// requireActivity().overridePendingTransition(0, 0)
// }
// binding.tvBack.setOnClickListener {
// activity?.finish()
// }
// }
//
// override fun registerDataChange() {
// super.registerDataChange()
//// lifecycleScope.launch {
//// viewModel.bindStateChange.collect {
//// updateBindState(it)
//// }
//// }
// }
// private fun updateBindState(pair: Pair<Boolean?, ErrorInfo>){
// if (pair.first == null) return // 绑定状态过滤
// val errorInfo = pair.second
// if (errorInfo.isSuccess()) {
// ToastUtils.showToast("解绑成功")
//// activity?.finish()
// // 关闭宿主 Activity
// requireActivity().finish()
// requireActivity().overridePendingTransition(0, 0)
// } else {
// ToastUtils.showToast(errorInfo.msg)
// }
// }
//}
@@ -21,28 +21,6 @@ data class DeviceConfig(
var arcsoftActiveKey: String? = null var arcsoftActiveKey: String? = null
) )
/**
* V2 设备配置(人脸 5.0 体系)
*/
data class DeviceConfigV2(
/** 业务服务器 BASE URL */
val appPackageUrl: String? = null,
/** 食堂/餐厅名称 */
val canteenName: String? = null,
/** 食堂/餐厅 ID(后续所有业务 API 的 restId */
val canteenId: String? = null,
/** 虹软人脸 SDK App ID */
val arcsoftAppId: String? = null,
/** 虹软人脸 SDK Key */
val arcsoftSdkKey: String? = null,
/** 虹软人脸 SDK Active Key */
val arcsoftActiveKey: String? = null,
/** MQTT 客户端服务器 IP */
val clientServerIp: String? = null,
/** 智慧食堂服务器 IP(MQTT 端口) */
val zhstServerIp: String? = null
)
data class CodeMsg( data class CodeMsg(
val code: String? = "", val code: String? = "",
val msg: String? = "" val msg: String? = ""
@@ -1,6 +0,0 @@
package com.sw.platecabinet.model.request
data class FeatureBody(
val url:String,
val featureChar:String
)
@@ -1,13 +0,0 @@
package com.sw.platecabinet.model.request
/**
* 通用 ID 请求体
*
* 用于按用户 id(餐盘号)查询或上报类的接口,例如:
* - P-09a 取餐盘(/neglect/pickup/plate-pickup
*
* @property id 用户 id(餐盘号 = userId
*/
data class IdDTO(
val id: Long
)
@@ -11,19 +11,19 @@ import kotlinx.parcelize.Parcelize
*/ */
@Parcelize @Parcelize
data class EquipmentUserInfo( data class EquipmentUserInfo(
/** // /**
* 主键 // * 主键
*/ // */
val id: Long? = 0, // val id: Long? = 0,
/** // /**
* // *
* 设备Id // * 设备Id
*/ // */
val equipmentId: String? = "", // val equipmentId: String? = "",
/** // /**
* 设备编号 // * 设备编号
*/ // */
var equipmentCode: String? = "", // var equipmentCode: String? = "",
/** /**
* 设备盒子编号 * 设备盒子编号
*/ */
@@ -31,60 +31,60 @@ data class EquipmentUserInfo(
/** /**
* 会员Id * 会员Id
*/ */
val faceId: String? = "", var faceId: String? = "",
/** /**
* 餐盘编号 * 餐盘编号
*/ */
var plateNumber: String? = "", var plateNumber: String? = "",
/** // /**
* 设备名称 // * 设备名称
*/ // */
val equipmentName: String? = "", // val equipmentName: String? = "",
val orderNo: String? = "", // val orderNo: String? = "",
/** /**
* *
* 更新时间 * 更新时间
*/ */
val updateTime: String? = "", var updateTime: String? = "",
val name: String? = "", // val name: String? = "",
val phone: String? = "", // val phone: String? = "",
val faceUrl: String? = "", // val faceUrl: String? = "",
val mealTime: String? = "", // val mealTime: String? = "",
val mealTimeInterval: String? = "", // val mealTimeInterval: String? = "",
val openTime: String? = "", // val openTime: String? = "",
val openTimeInterval: String? = "", // val openTimeInterval: String? = "",
val eatCount: Int? = 0, // val eatCount: Int? = 0,
//
// var cardBalance: Double? = 0.toDouble(),
var cardBalance: Double? = 0.toDouble(), // /**
// * 用于cardBalance后续拦截判断
/** // */
* 用于cardBalance后续拦截判断 // var showBalanceNotEnoughDialog: Boolean = false,
*/ // var isIntercept: Boolean = false
var showBalanceNotEnoughDialog: Boolean = false,
var isIntercept: Boolean = false
) : Parcelable { ) : Parcelable {
/** // /**
* 是否已绑定 // * 是否已绑定
*/ // */
fun isBound(): Boolean { // fun isBound(): Boolean {
return faceId != null // return faceId.isNullOrBlank().not()
} // }
/** // /**
* 是否是其他设备 // * 是否是其他设备
*/ // */
fun isOtherEquipment(): Boolean { // fun isOtherEquipment(): Boolean {
return equipmentCode != null && equipmentCode != "" && GlobalData.globalEquipmentCode != equipmentCode // return equipmentCode != null && equipmentCode != "" && GlobalData.globalEquipmentCode != equipmentCode
} // }
fun hasEquipmentBox(): Boolean { // fun hasEquipmentBox(): Boolean {
return !TextUtils.isEmpty(equipmentBoxCode) // return !TextUtils.isEmpty(equipmentBoxCode)
} // }
//
fun isPlaceholder(): Boolean { // fun isPlaceholder(): Boolean {
return equipmentBoxCode == null || equipmentBoxCode == "" // return equipmentBoxCode.isNullOrBlank()
} // }
} }
@@ -9,6 +9,19 @@ import kotlinx.parcelize.Parcelize
*/ */
@Parcelize @Parcelize
data class UserFaceModel( data class UserFaceModel(
// @SerializedName("faceFeature")
// val faceFeature: String? = "",
// @SerializedName("faceFeatureString")
// val faceFeatureString: String? = "",
// @SerializedName("faceType")
// val faceType: String? = "",
// @SerializedName("userFaceId")
// val userFaceId: String? = "",
// @SerializedName("userId")
// val userId: String? = "",
// @SerializedName("updateTimeStamp")
// val updateTimeStamp: Long = 0,
val userId: String? = "", val userId: String? = "",
val faceFeatureStr: String? = "", val faceFeatureStr: String? = "",
val faceUpdateTimestamp: Long? = null, val faceUpdateTimestamp: Long? = null,
@@ -19,10 +32,10 @@ data class UserFaceModel(
/** /**
* 会员编号 * 会员编号
*/ */
val cardNo: String? = null, val cardNo: String,
/** /**
* 是否会员 * 是否会员
*/ */
val member: Boolean? = null val member: Boolean
) : Parcelable ) : Parcelable
@@ -1,44 +0,0 @@
package com.sw.platecabinet.model.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import kotlinx.parcelize.Parcelize
/**
* 用户人脸信息
*/
@Parcelize
data class UserFaceModelV2(
val userFaceId: String? = null,
val userId: String? = null,
val faceFeature: String? = null,
val faceFeatureStr: String? = null,
val faceFeatureString: String? = null,
val faceUpdateTimestamp: Long? = null,
val cardNo: String? = null,
val member: Boolean? = false,
val faceDeleted: Boolean? = false,
val personType: String? = null
) : Parcelable {
fun resolveFeatureStr(): String? {
return faceFeature?.takeIf { it.isNotEmpty() }
?: faceFeatureStr?.takeIf { it.isNotEmpty() }
?: faceFeatureString?.takeIf { it.isNotEmpty() }
}
fun toFaceEntity(): FaceEntity {
val featureBase64 = resolveFeatureStr()
return FaceEntity(
userId,
android.util.Base64.decode(featureBase64, android.util.Base64.DEFAULT),
personType,
cardNo,
userId,
userFaceId,
member ?: false,
faceUpdateTimestamp ?: 0L
)
}
}
@@ -3,7 +3,6 @@ package com.sw.platecabinet.network
import com.sw.platecabinet.GlobalData import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.MyApp import com.sw.platecabinet.MyApp
import com.sw.platecabinet.network.api.ApiService import com.sw.platecabinet.network.api.ApiService
import com.sw.platecabinet.network.api.ApiServiceV2
import com.sw.platecabinet.network.interceptor.RequestInterceptor import com.sw.platecabinet.network.interceptor.RequestInterceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
@@ -38,8 +37,4 @@ object ApiClient {
val apiService: ApiService by lazy { val apiService: ApiService by lazy {
retrofit.create(ApiService::class.java) retrofit.create(ApiService::class.java)
} }
val apiServiceV2: ApiServiceV2 by lazy {
retrofit.create(ApiServiceV2::class.java)
}
} }
@@ -11,12 +11,15 @@ import com.sw.platecabinet.model.response.EquipmentInfo
import com.sw.platecabinet.model.response.EquipmentUserInfo import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.SearchResult import com.sw.platecabinet.model.response.SearchResult
import com.sw.platecabinet.model.response.UserFaceModel import com.sw.platecabinet.model.response.UserFaceModel
import okhttp3.MultipartBody
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.Field import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Header import retrofit2.http.Header
import retrofit2.http.Multipart
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.Query import retrofit2.http.Query
import retrofit2.http.Url import retrofit2.http.Url
@@ -75,7 +78,7 @@ interface ApiService {
// @Body param: LoginParam // @Body param: LoginParam
// ): ApiResponse<EquipmentUserInfo> // ): ApiResponse<EquipmentUserInfo>
/** /**
* 餐盘用户信息获取 * 餐盘用户信息获取
*/ */
@POST//("/shuwei-zhct/swEquipmentRelUser/equipmentBoxLogin") @POST//("/shuwei-zhct/swEquipmentRelUser/equipmentBoxLogin")
@@ -145,7 +148,7 @@ interface ApiService {
// ): ApiResponse<EquipmentUserInfo> // ): ApiResponse<EquipmentUserInfo>
/** /**
* 通过餐盘号获取信息 * 通过餐盘号获取信息
*/ */
@GET @GET
@@ -155,8 +158,6 @@ interface ApiService {
): ApiResponse<EquipmentUserInfo> ): ApiResponse<EquipmentUserInfo>
/** /**
* 获取人脸增量数据 * 获取人脸增量数据
* *
@@ -173,8 +174,8 @@ interface ApiService {
*/ */
@POST @POST
suspend fun getFaceIncrementList( suspend fun getFaceIncrementList(
@Url url:String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/faceFeature/increment/list", @Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/faceFeature/increment/list",
@Body param:Map<String, Long> @Body param: Map<String, Long>
): ApiResponse<List<UserFaceModel>?> ): ApiResponse<List<UserFaceModel>?>
/** /**
@@ -185,5 +186,22 @@ interface ApiService {
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/getYxEquipmentByEquipmentCode" @Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/getYxEquipmentByEquipmentCode"
): ApiResponse<DeviceConfig?> ): ApiResponse<DeviceConfig?>
/**
* 添加人脸数据
*/
@POST
@FormUrlEncoded
suspend fun addUserFace(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/addUserByFace",
@Field("featureChar") faceData:String,
@Field("url") imageUr:String
): ApiResponse<UserFaceModel?>
@Multipart
@POST
suspend fun uploadImage(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/upload",
@Part file: MultipartBody.Part,
): ApiResponse<String?>
} }
@@ -1,78 +0,0 @@
package com.sw.platecabinet.network.api
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.model.DeviceConfigV2
import com.sw.platecabinet.model.request.FeatureBody
import com.sw.platecabinet.model.request.IdDTO
import com.sw.platecabinet.model.response.ApiResponse
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.model.response.UserFaceModelV2
import okhttp3.MultipartBody
import retrofit2.http.Body
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.Url
interface ApiServiceV2 {
/**
* 获取人脸缓存数据
*/
@POST
suspend fun getUserFaceCache(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/common/face/page",
@Body param: Map<String, Int>
): ApiResponse<List<UserFaceModelV2>>
/**
* 获取人脸增量数据
*/
@POST
suspend fun getFaceIncrementList(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/common/face/increment",
@Body param: Map<String, Long>
): ApiResponse<List<UserFaceModelV2>?>
/**
* 获取设备配置数据
*/
@GET
suspend fun getDeviceConfig(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/pickup/device/config"
): ApiResponse<DeviceConfigV2?>
/**
* 添加人脸数据
*/
@POST
suspend fun addUserFace(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/user/add-by-face",
@Body faceData: FeatureBody
): ApiResponse<UserFaceModelV2?>
/**
* 上传图片
*/
@Multipart
@POST
suspend fun uploadImage(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/upload",
@Part file: MultipartBody.Part,
): ApiResponse<String?>
/**
* 取餐盘(P-09a):记录用户开始就餐时刻
* 调用一次即 upsert 到主单 plate_pickup_time,是 P-09 提交就餐记录的强前置。
* @param param IdDTOid 为用户 id(餐盘号 = userId
* @return ApiResponse<String?>data 为主单 recordNo
*/
@POST
suspend fun platePickup(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/pickup/plate-pickup",
@Body param: IdDTO
): ApiResponse<String?>
}
@@ -1,92 +1,104 @@
package com.sw.platecabinet.network.task //package com.sw.platecabinet.network.task
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.sw.plate.utils.arcface.FaceApi
import com.sw.platecabinet.network.ApiClient
import com.sw.platecabinet.repository.RemoteRepository
class HeartBeatTask(appContext: Context, workerParams: WorkerParameters) :
CoroutineWorker(appContext, workerParams) {
companion object {
private const val TAG = "HeartBeatTask"
}
override suspend fun doWork(): Result {
return try {
// 执行后台任务逻辑
performSync()
TaskManager.startTask()
Result.success()
} catch (e: Exception) {
e.printStackTrace()
Result.retry()
}
}
private val repository: RemoteRepository by lazy {
RemoteRepository(ApiClient.apiService)
}
private val faceApi: FaceApi = FaceApi()
private suspend fun performSync() {
// withContext(Dispatchers.IO) {
// val faceUpdateTimeStamp = PrefUtils.getLong(App.getContext(), "faceUpdateTimeStamp", 0)
// if (faceUpdateTimeStamp == 0L) {
// val timestamp = System.currentTimeMillis()
// PrefUtils.setLong(App.getContext(), "faceUpdateTimeStamp", timestamp)
// }
// val resp = repository.getHeartbeatInterface(
// PrefUtils.getLong(
// App.getContext(),
// "faceUpdateTimeStamp",
// 0
// )
// )
// Timber.tag(TAG).d("performSync: ${GsonUtils.toJson(resp)}")
// val list = resp.data
// if (list.isNullOrEmpty()) {
// return@withContext
// }
// val faceServer = FaceServer.getInstance()
// val faceEngine: FaceEngine? = faceServer.faceEngine
// //
// list.forEachIndexed { index, it -> //import android.content.Context
// try { //import android.util.Log
// if (index == list.size - 1) { //import androidx.work.CoroutineWorker
// PrefUtils.setLong( //import androidx.work.WorkerParameters
// App.getContext(), //import com.arcsoft.face.FaceEngine
// "faceUpdateTimeStamp", //import com.sw.inbound.utils.GsonUtils
// it.updateTimeStamp //import com.sw.plate.App
// ) //import com.sw.plate.utils.Base64
// } //import com.sw.plate.utils.PrefUtils
// if (it.faceType == "1") { //import com.sw.plate.utils.arcface.FaceApi
// if (it.userId.isNullOrBlank().not()) { //import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
// //userId非空,根据userId删除人脸数据 //import com.sw.plate.utils.arcface.faceserver.FaceServer
// faceApi.deleteByUserName(it.userId) //import com.sw.platecabinet.network.ApiClient
// faceEngine?.removeFaceFeature(it.userId.toInt()) //import com.sw.platecabinet.repository.RemoteRepository
// } //import kotlinx.coroutines.Dispatchers
// } else if (it.faceType == "0") { //import kotlinx.coroutines.withContext
// //新增人脸数据 //import org.greenrobot.eventbus.EventBus
// val featureData = Base64.decode(it.faceFeature) //import timber.log.Timber
// val insertEntity = FaceEntity(it.userId, null, featureData)
// val faceId = faceApi.insert(insertEntity)
// insertEntity.faceId = faceId
//// faceServer.registerFaceFeatureInfoFromDb(insertEntity, faceEngine)
// //
// EventBus.getDefault().post(insertEntity) //class HeartBeatTask(appContext: Context, workerParams: WorkerParameters) :
//// callback(insertEntity) // CoroutineWorker(appContext, workerParams) {
// } //
// } catch (e: Exception) { // companion object {
// e.printStackTrace() // private const val TAG = "HeartBeatTask"
// } // }
// } //
// override suspend fun doWork(): Result {
// return try {
// // 执行后台任务逻辑
// performSync()
//
// TaskManager.startTask()
//
// Result.success()
// } catch (e: Exception) {
// e.printStackTrace()
// Result.retry()
// } // }
} // }
//
} // private val repository: RemoteRepository by lazy {
// RemoteRepository(ApiClient.apiService)
// }
//
// private val faceApi: FaceApi = FaceApi()
//
// private suspend fun performSync() {
//// withContext(Dispatchers.IO) {
//// val faceUpdateTimeStamp = PrefUtils.getLong(App.getContext(), "faceUpdateTimeStamp", 0)
//// if (faceUpdateTimeStamp == 0L) {
//// val timestamp = System.currentTimeMillis()
//// PrefUtils.setLong(App.getContext(), "faceUpdateTimeStamp", timestamp)
//// }
//// val resp = repository.getHeartbeatInterface(
//// PrefUtils.getLong(
//// App.getContext(),
//// "faceUpdateTimeStamp",
//// 0
//// )
//// )
//// Timber.tag(TAG).d("performSync: ${GsonUtils.toJson(resp)}")
//// val list = resp.data
//// if (list.isNullOrEmpty()) {
//// return@withContext
//// }
//// val faceServer = FaceServer.getInstance()
//// val faceEngine: FaceEngine? = faceServer.faceEngine
////
//// list.forEachIndexed { index, it ->
//// try {
//// if (index == list.size - 1) {
//// PrefUtils.setLong(
//// App.getContext(),
//// "faceUpdateTimeStamp",
//// it.updateTimeStamp
//// )
//// }
//// if (it.faceType == "1") {
//// if (it.userId.isNullOrBlank().not()) {
//// //userId非空,根据userId删除人脸数据
//// faceApi.deleteByUserName(it.userId)
//// faceEngine?.removeFaceFeature(it.userId.toInt())
//// }
//// } else if (it.faceType == "0") {
//// //新增人脸数据
//// val featureData = Base64.decode(it.faceFeature)
//// val insertEntity = FaceEntity(it.userId, null, featureData)
//// val faceId = faceApi.insert(insertEntity)
//// insertEntity.faceId = faceId
////// faceServer.registerFaceFeatureInfoFromDb(insertEntity, faceEngine)
////
//// EventBus.getDefault().post(insertEntity)
////// callback(insertEntity)
//// }
//// } catch (e: Exception) {
//// e.printStackTrace()
//// }
//// }
//// }
// }
//
//}
@@ -1,40 +1,40 @@
package com.sw.platecabinet.network.task //package com.sw.platecabinet.network.task
//
import androidx.lifecycle.LifecycleOwner //import androidx.lifecycle.LifecycleOwner
import androidx.work.ExistingWorkPolicy //import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder //import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo //import androidx.work.WorkInfo
import androidx.work.WorkManager //import androidx.work.WorkManager
import com.sw.plate.App //import com.sw.plate.App
import java.util.concurrent.TimeUnit //import java.util.concurrent.TimeUnit
//
object TaskManager { //object TaskManager {
private const val TASK_NAME = "SmartPlateCabinetTask" // private const val TASK_NAME = "SmartPlateCabinetTask"
// private var owner: LifecycleOwner?=null //// private var owner: LifecycleOwner?=null
// fun setOwner(owner: LifecycleOwner) { //// fun setOwner(owner: LifecycleOwner) {
// this.owner = owner //// this.owner = owner
//// }
// fun startTask() {
// val nextRequest = OneTimeWorkRequestBuilder<HeartBeatTask>()
// .setInitialDelay(1, TimeUnit.MINUTES)
//// .setInputData(inputData)
// .build()
// WorkManager.getInstance(App.getContext())
//// .apply{
//// getWorkInfoByIdLiveData(nextRequest.id)
//// .observe(owner) { workInfo ->
//// if (workInfo?.state == WorkInfo.State.SUCCEEDED) {
//// val result = workInfo.outputData.getString("RESULT_KEY")
//// // 使用返回的结果
//// }
//// }
//// }
// .enqueueUniqueWork(TASK_NAME, ExistingWorkPolicy.REPLACE, nextRequest)
// } // }
fun startTask() { //
val nextRequest = OneTimeWorkRequestBuilder<HeartBeatTask>() // fun cancelTask() {
.setInitialDelay(1, TimeUnit.MINUTES) // WorkManager.getInstance(App.getContext())
// .setInputData(inputData) // .cancelUniqueWork(TASK_NAME)
.build() // }
WorkManager.getInstance(App.getContext()) //
// .apply{ //}
// getWorkInfoByIdLiveData(nextRequest.id)
// .observe(owner) { workInfo ->
// if (workInfo?.state == WorkInfo.State.SUCCEEDED) {
// val result = workInfo.outputData.getString("RESULT_KEY")
// // 使用返回的结果
// }
// }
// }
.enqueueUniqueWork(TASK_NAME, ExistingWorkPolicy.REPLACE, nextRequest)
}
fun cancelTask() {
WorkManager.getInstance(App.getContext())
.cancelUniqueWork(TASK_NAME)
}
}
@@ -1,14 +1,14 @@
package com.sw.platecabinet.repository package com.sw.platecabinet.repository
import com.sw.platecabinet.model.DeviceConfig import com.sw.platecabinet.model.DeviceConfig
import com.sw.platecabinet.model.request.BindParam
import com.sw.platecabinet.model.request.LoginParam
import com.sw.platecabinet.model.request.SearchParam import com.sw.platecabinet.model.request.SearchParam
import com.sw.platecabinet.model.response.ApiResponse import com.sw.platecabinet.model.response.ApiResponse
import com.sw.platecabinet.model.response.EquipmentUserInfo import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.SearchResult import com.sw.platecabinet.model.response.SearchResult
import com.sw.platecabinet.model.response.UserFaceModel import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.network.api.ApiService import com.sw.platecabinet.network.api.ApiService
import com.sw.platecabinet.utils.FileUtils
import java.io.File
/** /**
* 远程数据处理 * 远程数据处理
@@ -70,19 +70,19 @@ class RemoteRepository constructor(
} }
} }
/** // /**
* 登录 // * 登录
*/ // */
suspend fun equipmentBoxLogin(param: LoginParam): ApiResponse<EquipmentUserInfo> { // suspend fun equipmentBoxLogin(param: LoginParam): ApiResponse<EquipmentUserInfo> {
return safeApiCall { apiService.equipmentBoxLogin(param = param) } // return safeApiCall { apiService.equipmentBoxLogin(param = param) }
} // }
/** // /**
* 获取设备绑定用户列表 // * 获取设备绑定用户列表
*/ // */
suspend fun getEquipmentList(): ApiResponse<List<EquipmentUserInfo>> { // suspend fun getEquipmentList(): ApiResponse<List<EquipmentUserInfo>> {
return safeApiCall { apiService.getEquipmentList() } // return safeApiCall { apiService.getEquipmentList() }
} // }
// /** // /**
// * 餐盘用户信息绑定解绑 // * 餐盘用户信息绑定解绑
@@ -91,13 +91,13 @@ class RemoteRepository constructor(
// return safeApiCall { apiService.bindEquipment(param = param) } // return safeApiCall { apiService.bindEquipment(param = param) }
// } // }
suspend fun plateBind(param: BindParam): ApiResponse<EquipmentUserInfo?> { // suspend fun plateBind(param: BindParam): ApiResponse<EquipmentUserInfo?> {
return safeApiCall { apiService.plateBind(param = param) } // return safeApiCall { apiService.plateBind(param = param) }
} // }
suspend fun plateUnbind(id: Long?): ApiResponse<Any?> { // suspend fun plateUnbind(id: Long?): ApiResponse<Any?> {
return safeApiCall { apiService.plateUnbind(id = id) } // return safeApiCall { apiService.plateUnbind(id = id) }
} // }
/** /**
* 用户信息模糊搜索 * 用户信息模糊搜索
@@ -113,11 +113,11 @@ class RemoteRepository constructor(
return safeApiCall { apiService.findByPlateNumber(plateNumber = plateNumber) } return safeApiCall { apiService.findByPlateNumber(plateNumber = plateNumber) }
} }
/** // /**
* 获取人脸增量数据 // * 获取人脸增量数据
* // *
* @param timeDate 时间戳(毫秒级) // * @param timeDate 时间戳(毫秒级)
*/ // */
// suspend fun getHeartbeatInterface(timeDate: Long): ApiResponse<List<UserFaceModel>> { // suspend fun getHeartbeatInterface(timeDate: Long): ApiResponse<List<UserFaceModel>> {
// return safeApiCall { apiService.getHeartbeatInterface(timeDate = timeDate) } // return safeApiCall { apiService.getHeartbeatInterface(timeDate = timeDate) }
// } // }
@@ -141,4 +141,19 @@ class RemoteRepository constructor(
suspend fun getDeviceConfig(): ApiResponse<DeviceConfig?> { suspend fun getDeviceConfig(): ApiResponse<DeviceConfig?> {
return safeApiCall { apiService.getDeviceConfig() } return safeApiCall { apiService.getDeviceConfig() }
} }
suspend fun addUserFace(faceData: String, imageUr:String): ApiResponse<UserFaceModel?> {
return safeApiCall { apiService.addUserFace(faceData = faceData, imageUr = imageUr) }
}
suspend fun uploadImage(file: File): ApiResponse<String?> {
return safeApiCall {
val part = FileUtils.genRequestPart(file)
if (part == null) {
ApiResponse(code = "-1", msg = "解析图片失败")
} else {
apiService.uploadImage(file = part)
}
}
}
} }
@@ -1,82 +0,0 @@
package com.sw.platecabinet.repository
import com.sw.platecabinet.model.DeviceConfigV2
import com.sw.platecabinet.model.request.FeatureBody
import com.sw.platecabinet.model.request.IdDTO
import com.sw.platecabinet.model.response.ApiResponse
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.model.response.UserFaceModelV2
import com.sw.platecabinet.network.api.ApiServiceV2
import com.sw.platecabinet.utils.FileUtils
import java.io.File
/**
* 远程数据处理
*/
class RemoteRepositoryV2 constructor(
private val apiService: ApiServiceV2
) : BaseRepository() {
/**
* 获取人脸数据
*/
suspend fun getUserFaceCache(
pageNum: Int,
pageSize: Int = 100,
): ApiResponse<List<UserFaceModelV2>> {
return safeApiCall {
apiService.getUserFaceCache(
param = mapOf(
"pageNum" to pageNum, "pageSize" to pageSize
)
)
}
}
suspend fun getFaceIncrementList(
pageNum: Long, pageSize: Long = 100L, timestamp: Long
): ApiResponse<List<UserFaceModelV2>?> {
return safeApiCall {
apiService.getFaceIncrementList(
param = mapOf(
"pageNum" to pageNum, "pageSize" to pageSize, "timestamp" to timestamp
)
)
}
}
suspend fun getDeviceConfig(): ApiResponse<DeviceConfigV2?> {
return safeApiCall { apiService.getDeviceConfig() }
}
suspend fun addUserFace(faceData: String, imageUrl: String): ApiResponse<UserFaceModelV2?> {
return safeApiCall {
apiService.addUserFace(
faceData = FeatureBody(
url = imageUrl, featureChar = faceData
)
)
}
}
suspend fun uploadImage(file: File): ApiResponse<String?> {
return safeApiCall {
val part = FileUtils.genRequestPart(file)
if (part == null) {
ApiResponse(code = "-1", msg = "解析图片失败")
} else {
apiService.uploadImage(file = part)
}
}
}
/**
* 取餐盘(P-09a):上报用户开始就餐时刻
* @param userId 用户 id(餐盘号 = userId
* @return ApiResponse<String?>data 为主单 recordNo
*/
suspend fun platePickup(userId: Long): ApiResponse<String?> {
return safeApiCall { apiService.platePickup(param = IdDTO(id = userId)) }
}
}
@@ -0,0 +1,300 @@
package com.sw.platecabinet.socket;
import android.annotation.SuppressLint;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 局域网多客户端通信管理器:支持并发、心跳、认证、广播、点对点发送
* 可用于 Android 和 JVM 程序。
*/
public class LanCommunicationManager {
// ============ 监听配置 ============
private final int port;
private final long HEARTBEAT_TIMEOUT_MS;
private final int MAX_CLIENT_THREADS;
// ============ 状态 ============
private volatile boolean running = false;
private ServerSocket serverSocket;
// ============ 线程池 ============
private final ExecutorService acceptExecutor = Executors.newSingleThreadExecutor();
private final ExecutorService clientExecutor;
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
// ============ 客户端会话 ============
private final ConcurrentHashMap<String, ClientSession> clients = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Socket, ClientSession> unAuthSessions = new ConcurrentHashMap<>();
// ============ 回调接口 ============
public interface Listener {
// 新客户端完成 AUTH / 认证
void onClientConnected(String clientId);
// 客户端断开
void onClientDisconnected(String clientId);
// 收到业务消息(type != heartbeat/auth)
void onMessageReceived(String clientId, JSONObject message);
}
private Listener listener;
// ============ 构造 ============
public LanCommunicationManager(int port, int maxClientThreads, long heartbeatTimeoutMs) {
this.port = port;
this.MAX_CLIENT_THREADS = maxClientThreads;
this.HEARTBEAT_TIMEOUT_MS = heartbeatTimeoutMs;
this.clientExecutor = Executors.newFixedThreadPool(Math.max(2, maxClientThreads));
}
public void setListener(Listener listener) {
this.listener = listener;
}
// ============ 启动服务端 ============
@SuppressLint("DiscouragedApi")
public void start() throws IOException {
if (running) return;
running = true;
serverSocket = new ServerSocket(port);
serverSocket.setSoTimeout(2000);
acceptExecutor.execute(this::acceptLoop);
scheduler.scheduleAtFixedRate(this::heartbeatCheck,
HEARTBEAT_TIMEOUT_MS,
HEARTBEAT_TIMEOUT_MS,
TimeUnit.MILLISECONDS);
System.out.println("LanCommunicationManager started on port " + port);
}
private void acceptLoop() {
while (running) {
try {
Socket socket = serverSocket.accept();
socket.setSoTimeout((int) HEARTBEAT_TIMEOUT_MS * 2);
ClientSession session = new ClientSession(socket);
unAuthSessions.put(socket, session);
clientExecutor.execute(() -> clientReadLoop(session));
} catch (SocketTimeoutException ignore) {
} catch (Exception e) {
if (running) e.printStackTrace();
}
}
}
// ============ 处理客户端数据读取 ============
private void clientReadLoop(ClientSession session) {
Socket socket = session.socket;
try (DataInputStream in = new DataInputStream(socket.getInputStream())) {
while (running && !socket.isClosed()) {
int len;
try {
len = in.readInt();
} catch (SocketTimeoutException ste) {
continue;
}
if (len <= 0 || len > 10 * 1024 * 1024) break;
byte[] buf = new byte[len];
in.readFully(buf);
session.updateLastSeen();
JSONObject msg = new JSONObject(new String(buf));
handleMessage(session, msg);
}
} catch (Exception ignored) {
} finally {
closeSession(session);
}
}
private void handleMessage(ClientSession session, JSONObject msg) {
String type = msg.optString("type", "");
switch (type) {
case "auth":
handleAuth(session, msg);
break;
case "heartbeat":
session.updateLastSeen();
break;
default:
if (listener != null && session.clientId != null) {
listener.onMessageReceived(session.clientId, msg);
}
break;
}
}
private void handleAuth(ClientSession session, JSONObject msg) {
String clientId = msg.optString("clientId", null);
if (clientId == null) return;
session.clientId = clientId;
// 移动到已认证 map
unAuthSessions.remove(session.socket);
clients.put(clientId, session);
if (listener != null) listener.onClientConnected(clientId);
sendToSession(session, ack("auth_ok"));
}
// ============ 心跳超时 ============
private void heartbeatCheck() {
long now = System.currentTimeMillis();
for (Map.Entry<String, ClientSession> e : clients.entrySet()) {
ClientSession s = e.getValue();
if (now - s.lastSeen > HEARTBEAT_TIMEOUT_MS) {
closeSession(s);
}
}
for (ClientSession s : unAuthSessions.values()) {
if (now - s.lastSeen > HEARTBEAT_TIMEOUT_MS * 2) {
closeSession(s);
}
}
}
// ============ 发送 ============
public boolean sendToClient(String clientId, JSONObject json) {
ClientSession s = clients.get(clientId);
return s != null && sendToSession(s, json);
}
public void broadcast(JSONObject json) {
for (ClientSession s : clients.values()) {
sendToSession(s, json);
}
}
private boolean sendToSession(ClientSession s, JSONObject json) {
try {
DataOutputStream out = s.out;
synchronized (out) {
byte[] data = json.toString().getBytes();
out.writeInt(data.length);
out.write(data);
out.flush();
}
return true;
} catch (Exception e) {
closeSession(s);
return false;
}
}
// ============ ACK ============
private JSONObject ack(String type) {
JSONObject j = new JSONObject();
try {
j.put("type", "ack");
j.put("ack", type);
} catch (JSONException e) {
throw new RuntimeException(e);
}
return j;
}
// ============ 停止 ============
public void stop() {
running = false;
try {
serverSocket.close();
} catch (Exception ignored) {
}
for (ClientSession s : clients.values()) closeSession(s);
for (ClientSession s : unAuthSessions.values()) closeSession(s);
acceptExecutor.shutdownNow();
clientExecutor.shutdownNow();
scheduler.shutdownNow();
System.out.println("LanCommunicationManager stopped");
}
// ============ 会话类 ============
public static class ClientSession {
public final Socket socket;
public final DataOutputStream out;
public volatile long lastSeen = System.currentTimeMillis();
public volatile String clientId;
public ClientSession(Socket socket) throws IOException {
this.socket = socket;
this.out = new DataOutputStream(socket.getOutputStream());
}
public void updateLastSeen() {
lastSeen = System.currentTimeMillis();
}
}
public void closeSession(ClientSession session) {
if (session == null) return;
try {
Socket socket = session.socket;
// 1. 从已认证表移除
if (session.clientId != null) {
ClientSession removed = clients.remove(session.clientId);
if (removed != null && listener != null) {
listener.onClientDisconnected(session.clientId);
}
}
// 2. 从未认证表移除
unAuthSessions.remove(socket);
// 3. 关闭输出流
try {
session.out.close();
} catch (Exception ignored) {
}
// 4. 关闭 socket
try {
if (!socket.isClosed()) socket.close();
} catch (Exception ignored) {
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
@@ -0,0 +1,35 @@
package com.sw.platecabinet.socket;
public class LanServer {
public static final int TYPE_ADD_FACE_DATA = 1;
public static final int TYPE_CLEAR_FACE_DATA = 2;
private static volatile LanCommunicationManager instance;
public static LanCommunicationManager getInstance() {
if (instance == null) {
synchronized (LanServer.class) {
if (instance == null) {
instance = new LanCommunicationManager(
5000, // 监听端口
20, // 最大客户端数
10_000 // 心跳超时时间 10 秒
);
}
}
}
return instance;
}
void a() {
//HashMap<String, Object> data = new HashMap<>();
//data.put("type", LanServer.TYPE_ADD_FACE_DATA);
//data.put("faceId", tempUserId);//收到采集信息,每次生成新的ID
//data.put("faceFeature", faceFeatureString);
//lanServer.broadcast(new JSONObject(data));
//
//HashMap<String, Object> data = new HashMap<>();
//data.put("type", LanServer.TYPE_CLEAR_FACE_DATA);
//lanServer.broadcast(new JSONObject(data));
}
}
@@ -0,0 +1,21 @@
package com.sw.platecabinet.socket
import android.util.Log
import org.json.JSONObject
open class LanServerListenerImpl: LanCommunicationManager.Listener {
companion object {
private const val TAG = "LanServerListenerImpl"
}
override fun onClientConnected(clientId: String?) {
Log.d(TAG, "addSocketListener,onClientConnected: clientId = $clientId")
}
override fun onClientDisconnected(clientId: String?) {
Log.d(TAG, "addSocketListener,onClientDisconnected: clientId = $clientId")
}
override fun onMessageReceived(clientId: String?, message: JSONObject?) {
Log.d(TAG, "addSocketListener,onMessageReceived: clientId = $clientIdmessage = $message")
}
}
@@ -0,0 +1,330 @@
package com.sw.platecabinet.socket;
import android.util.Log;
import org.json.JSONObject;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* TcpClient - Android ready
* <p>
* Features:
* - length-prefix protocol (int length + bytes)
* - separate read thread and write queue & writer thread
* - auto-reconnect with exponential backoff
* - heartbeat scheduler
* - send queue with optional callback for send result
* - auto send "auth" JSON after connection
*/
public class TcpClient {
private static final String TAG = "TcpClient";
// configuration
private final String serverIp;
private final int serverPort;
private final String clientId; // will be sent in auth message
private final int connectTimeoutMs;
private final long heartbeatIntervalMs;
private final long heartbeatTimeoutMs;
// socket + streams
private Socket socket;
private DataOutputStream out;
private DataInputStream in;
// threads & executors
private final ExecutorService writerExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "TcpClient-Writer"));
private final ExecutorService readerExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "TcpClient-Reader"));
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "TcpClient-Scheduler"));
private final ExecutorService connectExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "TcpClient-Connect"));
// send queue
private final BlockingQueue<JSONObject> sendQueue = new LinkedBlockingQueue<>();
// state
private final AtomicBoolean running = new AtomicBoolean(false);
private final AtomicBoolean connected = new AtomicBoolean(false);
private final AtomicBoolean authSent = new AtomicBoolean(false);
// reconnection/backoff
private final long baseReconnectDelayMs = 1000; // 1s
private final long maxReconnectDelayMs = 30_000; // 30s
private final AtomicInteger reconnectAttempt = new AtomicInteger(0);
// heartbeat task future
private ScheduledFuture<?> heartbeatFuture;
// listener
public interface Listener {
void onConnected();
void onDisconnected(Exception e);
void onMessage(JSONObject json);
void onSendSuccess(JSONObject json);
void onSendFailed(JSONObject json, Exception e);
}
private Listener listener;
public void setListener(Listener l) {
this.listener = l;
}
// ctor
public TcpClient(String serverIp, int serverPort, String clientId,
int connectTimeoutMs, long heartbeatIntervalMs, long heartbeatTimeoutMs) {
this.serverIp = serverIp;
this.serverPort = serverPort;
this.clientId = clientId;
this.connectTimeoutMs = connectTimeoutMs;
this.heartbeatIntervalMs = heartbeatIntervalMs;
this.heartbeatTimeoutMs = heartbeatTimeoutMs;
}
// start client (will attempt connect)
public void start() {
if (running.getAndSet(true)) return;
scheduleConnect(0);
// writer thread drains sendQueue
writerExecutor.execute(this::writerLoop);
}
// stop client and cleanup
public void stop() {
running.set(false);
cancelHeartbeat();
closeSocketQuiet();
writerExecutor.shutdownNow();
readerExecutor.shutdownNow();
scheduler.shutdownNow();
connectExecutor.shutdownNow();
sendQueue.clear();
}
// send JSON (queued). Non-blocking.
public void send(JSONObject json) {
if (!running.get()) return;
sendQueue.offer(json);
}
// AUTH shortcut (immediately send auth JSON)
private void sendAuth() {
try {
JSONObject auth = new JSONObject();
auth.put("type", "auth");
auth.put("clientId", clientId);
sendQueue.offer(auth);
authSent.set(true);
} catch (Exception ignored) {
}
}
// writer thread loop (serializes sends)
private void writerLoop() {
while (running.get()) {
try {
JSONObject json = sendQueue.take(); // blocks
if (connected.get() && out != null) {
try {
byte[] data = json.toString().getBytes();
synchronized (out) {
out.writeInt(data.length);
out.write(data);
out.flush();
}
if (listener != null) listener.onSendSuccess(json);
} catch (Exception e) {
if (listener != null) listener.onSendFailed(json, e);
// on write failure, attempt reconnect
safeCloseAndScheduleReconnect(e);
}
} else {
// not connected: requeue it and wait for connection
sendQueue.offer(json);
Thread.sleep(500); // avoid busy loop
}
} catch (InterruptedException ignored) {
break;
}
}
}
// reader loop (runs in readerExecutor)
private void startReaderLoop() {
readerExecutor.execute(() -> {
try {
while (running.get() && connected.get() && in != null) {
int length;
try {
length = in.readInt(); // will throw SocketTimeoutException if set
} catch (SocketTimeoutException ste) {
// used to detect socket liveness; continue loop
continue;
}
if (length <= 0 || length > 10 * 1024 * 1024) {
// invalid length, break
throw new RuntimeException("Invalid message length: " + length);
}
byte[] buf = new byte[length];
in.readFully(buf);
String s = new String(buf);
try {
JSONObject json = new JSONObject(s);
// update last seen time via heartbeat ack if needed
if ("heartbeat".equals(json.optString("type"))) {
// optionally respond or update time
} else if ("auth_ok".equals(json.optString("type")) || "ack".equals(json.optString("type"))) {
// ignore or process ack
} else {
if (listener != null) listener.onMessage(json);
}
} catch (Exception je) {
Log.w(TAG, "Invalid JSON from server: " + s, je);
}
}
} catch (Exception e) {
if (running.get()) {
safeCloseAndScheduleReconnect(e);
}
}
});
}
// schedule connect attempt with delay (ms)
private void scheduleConnect(long delayMs) {
connectExecutor.execute(() -> {
try {
if (delayMs > 0) Thread.sleep(delayMs);
} catch (InterruptedException ignored) {
}
if (!running.get()) return;
tryConnect();
});
}
// connect logic
private void tryConnect() {
if (!running.get()) return;
closeSocketQuiet(); // ensure closed
try {
Socket s = new Socket();
s.connect(new InetSocketAddress(serverIp, serverPort), connectTimeoutMs);
s.setSoTimeout((int) Math.max(heartbeatTimeoutMs, 5_000));
socket = s;
out = new DataOutputStream(socket.getOutputStream());
in = new DataInputStream(socket.getInputStream());
connected.set(true);
reconnectAttempt.set(0);
authSent.set(false);
// start reader
startReaderLoop();
// send auth immediately
sendAuth();
// start heartbeat
startHeartbeat();
if (listener != null) listener.onConnected();
Log.i(TAG, "Connected to " + serverIp + ":" + serverPort);
} catch (Exception e) {
Log.w(TAG, "Connect failed: " + e.getMessage());
scheduleReconnectWithBackoff();
}
}
// start heartbeat scheduler
private void startHeartbeat() {
cancelHeartbeat();
heartbeatFuture = scheduler.scheduleAtFixedRate(() -> {
if (!running.get() || !connected.get()) return;
try {
JSONObject hb = new JSONObject();
hb.put("type", "heartbeat");
hb.put("time", System.currentTimeMillis());
sendQueue.offer(hb);
} catch (Exception ignored) {
}
}, 0, heartbeatIntervalMs, TimeUnit.MILLISECONDS);
}
private void cancelHeartbeat() {
if (heartbeatFuture != null && !heartbeatFuture.isCancelled()) {
heartbeatFuture.cancel(true);
heartbeatFuture = null;
}
}
// close socket quietly and notify listener
private void safeCloseAndScheduleReconnect(Exception cause) {
closeSocketQuiet();
if (listener != null) listener.onDisconnected(cause);
scheduleReconnectWithBackoff();
}
private void scheduleReconnectWithBackoff() {
int attempt = reconnectAttempt.incrementAndGet();
long delay = Math.min(maxReconnectDelayMsFromAttempt(attempt), maxReconnectDelayMs);
Log.i(TAG, "Scheduling reconnect attempt " + attempt + " after " + delay + "ms");
scheduleConnect(delay);
}
// compute exponential backoff
private long maxReconnectDelayMsFromAttempt(int attempt) {
long d = baseReconnectDelayMs * (1L << Math.min(attempt, 30));
if (d < 0) d = maxReconnectDelayMs;
return Math.min(d, maxReconnectDelayMs);
}
// close socket and streams
private void closeSocketQuiet() {
connected.set(false);
cancelHeartbeat();
try {
if (out != null) {
out.close();
}
} catch (Exception ignored) {
}
try {
if (in != null) {
in.close();
}
} catch (Exception ignored) {
}
try {
if (socket != null && !socket.isClosed()) {
socket.close();
}
} catch (Exception ignored) {
}
out = null;
in = null;
socket = null;
}
// helper: when manually call reconnect (immediately)
public void reconnectNow() {
scheduleConnect(0);
}
// helper to set immediate send of a JSON and wait (blocking) until it is queued (not until delivered)
public boolean sendBlocking(JSONObject json, long timeoutMs) throws InterruptedException {
return sendQueue.offer(json, timeoutMs, TimeUnit.MILLISECONDS);
}
}
@@ -0,0 +1,33 @@
package com.sw.platecabinet.socket
import android.util.Log
import com.sw.platecabinet.socket.TcpClient.*
import org.json.JSONObject
import java.lang.Exception
open class TcpClientListenerImpl : Listener {
companion object {
private const val TAG = "TcpClientListenerImpl"
}
override fun onConnected() {
Log.d(TAG, "addSocketListener,onConnected: ")
}
override fun onDisconnected(e: Exception?) {
Log.d(TAG, "addSocketListener,onDisconnected: ${e?.toString()}")
}
override fun onMessage(json: JSONObject?) {
Log.d(TAG, "addSocketListener,onMessage: $json")
}
override fun onSendSuccess(json: JSONObject?) {
Log.d(TAG, "addSocketListener,onSendSuccess: $json")
}
override fun onSendFailed(json: JSONObject?, e: Exception?) {
Log.d(TAG, "addSocketListener,onSendFailed: $json,e:${e?.toString()}")
}
}
@@ -0,0 +1,47 @@
package com.sw.platecabinet.socket;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
/**
* Simple UDP discovery client:
* Sends "DISCOVER_SERVER" broadcast and waits for first reply "SERVER_FOUND:serverName:ip"
*/
public class UdpDiscoveryClient {
public interface Listener {
void onFound(String ip, String serverName);
void onError(Exception e);
}
public void discover(int discoveryPort, int timeoutMs, Listener listener) {
new Thread(() -> {
try (DatagramSocket socket = new DatagramSocket()) {
socket.setBroadcast(true);
byte[] data = "DISCOVER_SERVER".getBytes();
DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getByName("255.255.255.255"), discoveryPort);
socket.send(packet);
socket.setSoTimeout(timeoutMs);
byte[] buf = new byte[512];
DatagramPacket resp = new DatagramPacket(buf, buf.length);
socket.receive(resp);
String msg = new String(resp.getData(), 0, resp.getLength());
if (msg.startsWith("SERVER_FOUND")) {
// format: SERVER_FOUND:serverName:ip
String[] parts = msg.split(":", 3);
if (parts.length >= 3) {
listener.onFound(parts[2], parts[1]);
return;
}
}
listener.onError(new Exception("Invalid response"));
} catch (Exception e) {
listener.onError(e);
}
}).start();
}
}
@@ -0,0 +1,113 @@
package com.sw.platecabinet.socket;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
public class test {
LanCommunicationManager manager = new LanCommunicationManager(
9999, // 端口
20, // 最大客户端线程
30_000 // 心跳超时 30 秒
);
public void test() {
manager.setListener(new LanCommunicationManager.Listener() {
@Override
public void onClientConnected(String clientId) {
System.out.println("新的客户端上线:" + clientId);
}
@Override
public void onClientDisconnected(String clientId) {
System.out.println("客户端离线:" + clientId);
}
@Override
public void onMessageReceived(String clientId, JSONObject msg) {
System.out.println("收到 " + clientId + " 的消息:" + msg);
}
});
// 启动
try {
manager.start();
} catch (IOException e) {
throw new RuntimeException(e);
}
// 给某个客户端发送
JSONObject j = new JSONObject();
try {
j.put("type", "cmd");
j.put("content", "hello");
} catch (JSONException e) {
throw new RuntimeException(e);
}
manager.sendToClient("device123", j);
// 广播
manager.broadcast(j);
}
public void clientTets() {
// 1) discover server (optional)
UdpDiscoveryClient disc = new UdpDiscoveryClient();
disc.discover(9876, 3000, new UdpDiscoveryClient.Listener() {
@Override
public void onFound(String ip, String serverName) {
startClient(ip);
}
@Override
public void onError(Exception e) { /* fallback to manual IP */ }
});
}
// 2) start client
private TcpClient client;
private void startClient(String serverIp) {
client = new TcpClient(
serverIp,
9999,
"device123", // clientId
5000, // connectTimeoutMs
10_000, // heartbeatIntervalMs
30_000 // heartbeatTimeoutMs
);
client.setListener(new TcpClient.Listener() {
@Override
public void onConnected() {
Log.i("APP", "connected");
}
@Override
public void onDisconnected(Exception e) {
Log.i("APP", "disconnected", e);
}
@Override
public void onMessage(JSONObject json) {
Log.i("APP", "msg:" + json);
}
@Override
public void onSendSuccess(JSONObject json) {
}
@Override
public void onSendFailed(JSONObject json, Exception e) {
}
});
client.start();
}
}
@@ -0,0 +1,59 @@
package com.sw.platecabinet.utils
import android.content.Context
import android.graphics.Bitmap
import android.os.Environment
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import kotlin.io.use
object BitmapSaver {
// 保存到公共目录(需WRITE_EXTERNAL_STORAGE权限)
fun saveToPublicDirectory(
bitmap: Bitmap,
folderName: String = Environment.DIRECTORY_PICTURES,
fileName: String,
format: Bitmap.CompressFormat = Bitmap.CompressFormat.PNG,
quality: Int = 100
): File? {
val dir = Environment.getExternalStoragePublicDirectory(folderName)
if (!dir.exists()) dir.mkdirs()
return saveBitmap(bitmap, File(dir, fileName), format, quality)
}
// 保存到应用私有目录(无需权限)
fun saveToAppFilesDir(
bitmap: Bitmap,
context: Context,
fileName: String,
format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
quality: Int = 100
): File? {
//val dir = context.getExternalFilesDir(null)
val dir = context.cacheDir
val cropFile = File(dir, "crop")
if (cropFile.exists().not()) {
cropFile.mkdirs()
}
return saveBitmap(bitmap, File(cropFile, fileName), format, quality)
}
private fun saveBitmap(
bitmap: Bitmap,
outputFile: File,
format: Bitmap.CompressFormat,
quality: Int
): File? {
return try {
FileOutputStream(outputFile).use { fos ->
bitmap.compress(format, quality, fos)
fos.flush()
}
outputFile
} catch (e: IOException) {
e.printStackTrace()
null
}
}
}
@@ -0,0 +1,79 @@
package com.sw.platecabinet.utils
import android.util.Log
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import java.util.concurrent.atomic.AtomicBoolean
/**
* 基于协程的倒计时工具类(实例化使用,各调用方持有独立实例,互不干扰)
* 修复:onCompletion 区分正常结束与取消,手动取消时不触发 onFinish
*/
class CountDownUtil {
private val isCounting = AtomicBoolean(false)
private var currentJob: Job? = null
/**
* 启动倒计时(若已在运行则忽略,需重启请先调用 cancelCountDown
* @param total 总秒数
* @param lifecycleScope Activity/Fragment 的 lifecycleScope
* @param onStart 启动回调(主线程)
* @param onTick 每秒回调剩余秒数(主线程)
* @param onFinish 正常结束回调(主线程,手动取消时不触发)
*/
fun startCountDown(
total: Int = 15,
lifecycleScope: CoroutineScope,
onStart: () -> Unit = {},
onTick: (Int) -> Unit = {},
onFinish: () -> Unit
) {
if (isCounting.get()) return
isCounting.set(true)
currentJob = flow {
for (i in total downTo 1) {
emit(i)
delay(1000L)
}
}
.flowOn(Dispatchers.IO)
.onStart {
withContext(Dispatchers.Main) { onStart() }
}
.onEach { remaining ->
withContext(Dispatchers.Main) { onTick(remaining) }
}
.onCompletion { cause ->
withContext(Dispatchers.Main) {
isCounting.set(false)
currentJob = null
// cause == null 为正常结束,有异常(含 CancellationException)时不触发 onFinish
if (cause == null) {
Log.d("CountDownUtil", "倒计时正常结束")
onFinish()
} else {
Log.d("CountDownUtil", "倒计时被取消:${cause.message}")
}
}
}
.catch { e ->
Log.e("CountDownUtil", "倒计时异常:${e.message}", e)
isCounting.set(false)
currentJob = null
}
.launchIn(lifecycleScope)
}
/**
* 取消倒计时(不触发 onFinish)
*/
fun cancelCountDown() {
currentJob?.cancel()
currentJob = null
isCounting.set(false)
Log.d("CountDownUtil", "倒计时已取消")
}
}
@@ -115,7 +115,11 @@ class CrashHandler private constructor(private val context: Context) :
collectDeviceInfo(this) collectDeviceInfo(this)
// 收集应用日志 // 收集应用日志
append("\n\n").append(collectLogs()) try {
append("\n\n").append(collectLogs())
} catch (e: OutOfMemoryError) {
append("collectLogs发生OutOfMemoryError:${e.message}")
}
// 收集线程和异常信息 // 收集线程和异常信息
append("\n\n========== Thread & Exception Info ==========\n") append("\n\n========== Thread & Exception Info ==========\n")
@@ -0,0 +1,33 @@
package com.sw.platecabinet.utils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlin.ranges.downTo
// 倒计时Flow扩展函数
fun countDownByFlow(
total: Int,
scope: CoroutineScope,
onTick: (Int) -> Unit,
onStart: (() -> Unit)? = null,
onFinish: (() -> Unit)? = null
): Job {
return flow {
for (i in total downTo 0) {
emit(i)
if (i != 0) delay(1000)
}
}.flowOn(Dispatchers.Main)
.onStart { onStart?.invoke() }
.onCompletion { onFinish?.invoke() }
.onEach { onTick.invoke(it) }
.launchIn(scope)
}
@@ -15,6 +15,7 @@ class IntervalExecutor {
fun startIntervalTask(delayMillis: Long, action: suspend () -> Unit): Job { fun startIntervalTask(delayMillis: Long, action: suspend () -> Unit): Job {
return CoroutineScope(Dispatchers.Default).launch { return CoroutineScope(Dispatchers.Default).launch {
while (isActive) { while (isActive) {
ensureActive()
action() action()
delay(delayMillis) delay(delayMillis)
} }
@@ -36,6 +37,7 @@ class IntervalExecutor {
return CoroutineScope(Dispatchers.Default).launch { return CoroutineScope(Dispatchers.Default).launch {
delay(initialDelay) delay(initialDelay)
while (isActive) { while (isActive) {
ensureActive()
action() action()
delay(delayMillis) delay(delayMillis)
} }
@@ -44,23 +46,23 @@ class IntervalExecutor {
} }
// 使用示例 // 使用示例
fun main() = runBlocking { //fun main() = runBlocking {
val executor = IntervalExecutor() // val executor = IntervalExecutor()
//
// 示例1:每隔10秒执行一次 // // 示例1:每隔10秒执行一次
val job1 = executor.startIntervalTask(10000) { // val job1 = executor.startIntervalTask(10000) {
println("定时任务执行: ${System.currentTimeMillis()}") // println("定时任务执行: ${System.currentTimeMillis()}")
// 这里可以执行你的业务逻辑 // // 这里可以执行你的业务逻辑
} // }
//
// 示例2:先延迟5秒,然后每隔3秒执行一次 // // 示例2:先延迟5秒,然后每隔3秒执行一次
val job2 = executor.startIntervalTaskWithInitialDelay(5000, 3000) { // val job2 = executor.startIntervalTaskWithInitialDelay(5000, 3000) {
println("带初始延迟的定时任务: ${System.currentTimeMillis()}") // println("带初始延迟的定时任务: ${System.currentTimeMillis()}")
} // }
//
// 运行30秒后取消任务 // // 运行30秒后取消任务
delay(30000) // delay(30000)
job1.cancel() // job1.cancel()
job2.cancel() // job2.cancel()
println("所有定时任务已取消") // println("所有定时任务已取消")
} //}
@@ -0,0 +1,57 @@
package com.sw.platecabinet.utils;
/**
* Log统一管理类
*/
public class L {
private L() {
/* cannot be instantiated */
throw new UnsupportedOperationException("cannot be instantiated");
}
public static boolean isDebug = true;// 是否需要打印bug,可以在application的onCreate函数里面初始化
private static final String TAG = "mzf";
// 下面四个是默认tag的函数
public static void i(String msg) {
if (isDebug)
android.util.Log.i(TAG, msg);
}
public static void d(String msg) {
if (isDebug)
android.util.Log.d(TAG, msg);
}
public static void e(String msg) {
if (isDebug)
android.util.Log.e(TAG, msg);
}
public static void v(String msg) {
if (isDebug)
android.util.Log.v(TAG, msg);
}
// 下面是传入自定义tag的函数
public static void i(String tag, String msg) {
if (isDebug)
android.util.Log.i(tag, msg);
}
public static void d(String tag, String msg) {
if (isDebug)
android.util.Log.d(tag, msg);
}
public static void e(String tag, String msg) {
if (isDebug)
android.util.Log.e(tag, msg);
}
public static void v(String tag, String msg) {
if (isDebug)
android.util.Log.v(tag, msg);
}
}
@@ -0,0 +1,697 @@
package com.sw.platecabinet.utils;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import com.sw.plate.App;
import com.sw.plate.utils.AppUtil;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringReader;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
public class LogFileUtil {
private static boolean isSaveLog = true;
public static final String FILE_STR_PATH = App.getContext().getExternalCacheDir().getAbsolutePath();
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;
}
new Thread() {
@Override
public void run() {
super.run();
StringBuilder sb = new StringBuilder();
for (String log : logs) {
sb.append("===")
.append((AppUtil.formatDateGetCurrentTime()))
.append("===")
.append("\n")
.append(log)
.append("\n");
}
String fileName = "log" + AppUtil.formatDateGetDay(System.currentTimeMillis()) + ".txt";
LogFileUtil.saveStrFile(sb.toString(), fileName, FILE_STR_PATH, true);
}
}.start();
}
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,66 @@
package com.sw.platecabinet.utils
import androidx.fragment.app.FragmentActivity
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.comn.SerialApi
import com.sw.plate.utils.comn.SerialPortManager
import com.sw.platecabinet.activity.BaseActivity
import com.sw.platecabinet.activity.MainActivity
import com.sw.platecabinet.activity.PageType
import timber.log.Timber
object PlateUtils {
fun open(
fragmentActivity: FragmentActivity,
code: String?,
isScanCode: Boolean,
isSetting: Boolean = false,
callback: () -> Unit = {}
) {
val activity = fragmentActivity as BaseActivity<*>
Timber.d("itemClick llOpen equipmentBoxCode = ${code}")
val equipmentBoxCode: Int = code?.toInt() ?: -1
if (equipmentBoxCode < 0) {
ToastUtils.showToast("餐盘柜编号错误")
return
}
activity.showWaitingDialog("请稍等")
SerialApi.openPlate(equipmentBoxCode, object : SerialPortManager.SendCallback {
override fun onSuccess() {
activity.hideWaitingDialog()
if (isSetting.not()) {
MainActivity.start(
activity,
pageType = PageType.PLATE_TIP,
isScanCode = isScanCode
)
} else {
ToastUtils.showToast("柜门打开成功")
callback()
}
}
override fun onFail(e: Exception?) {
activity.hideWaitingDialog()
ToastUtils.showToast("柜门打开失败")
callback()
}
})
}
fun openDirect(boxCode: String?) {
val boxNumber = boxCode?.toIntOrNull() ?: -1
if (boxNumber < 0) return
SerialApi.openPlate(boxNumber, object : SerialPortManager.SendCallback {
override fun onSuccess() {
}
override fun onFail(e: Exception?) {
}
})
}
}
@@ -0,0 +1,15 @@
package com.sw.platecabinet.utils
class RegisterCallbackHandler {
private var isCallbackCalled = false
private val lock = Any()
fun setCallback(callback: () -> Unit) {
synchronized(lock) {
if (!isCallbackCalled) {
isCallbackCalled = true
callback()
}
}
}
}
@@ -0,0 +1,427 @@
package com.sw.platecabinet.utils;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Build;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class SoundPoolUtil {
private static final String TAG = SoundPoolUtil.class.getSimpleName();
private static SoundPoolUtil mSound;
private SoundPool mSoundPool;
private boolean isLoadC = false;
private Map<String, Integer> idCache;
private List<Integer> sidCache;
public SoundPoolUtil() {
idCache = new HashMap<>();
sidCache = new ArrayList<>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
AudioAttributes aab = new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build();
mSoundPool = new SoundPool.Builder()
.setMaxStreams(10)
.setAudioAttributes(aab)
.build();
} else {
mSoundPool = new SoundPool(10, AudioManager.STREAM_MUSIC, 8);
}
// mSoundPool = new SoundPool(60, AudioManager.USE_DEFAULT_STREAM_TYPE, 7);
mSoundPool.setOnLoadCompleteListener(new MyOnLoadCompleteListener());
}
public static SoundPoolUtil getInstance() {
synchronized (SoundPoolUtil.class) {
if (mSound == null) {
mSound = new SoundPoolUtil();
}
}
return mSound;
}
private int loadCompleteSize = 0;
private LoadCompletion completionListener;
public void setCompletionListener(LoadCompletion listener) {
this.completionListener = listener;
}
private class MyOnLoadCompleteListener implements SoundPool.OnLoadCompleteListener {
@Override
public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {
loadCompleteSize++;
L.e("loadSize" + loadSize + "===" + loadCompleteSize);
if (loadSize == loadCompleteSize) {
isLoadC = true;
if (completionListener != null) {
completionListener.onCompletion();
}
}
}
}
/**
* 加载指定资源
*
* @param name
* @param path
*/
public void loadR(String name, String path) {
if (checkSoundPool()) {
if (!idCache.containsKey(name)) {
idCache.put(name, mSoundPool.load(path, 1));
}
}
}
private int loadSize = 0;
/**
* 加载指定路径列表的资源
*
* @param map
*/
public void loadR(Map<String, String> map) {
loadSize = map.size();
Set<Map.Entry<String, String>> entries = map.entrySet();
for (Map.Entry<String, String> entry : entries) {
String key = entry.getKey();
if (checkSoundPool()) {
if (!idCache.containsKey(key)) {
idCache.put(key, mSoundPool.load(entry.getValue(), 1));
}
}
}
}
/**
* 加载指定AssetFileDescriptor的资源
*
* @param name
* @param afd
*/
public void loadRF(String name, AssetFileDescriptor afd) {
if (checkSoundPool()) {
if (!idCache.containsKey(name)) {
idCache.put(name, mSoundPool.load(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength(), 1));
}
}
}
/**
* 加载指定AssetFileDescriptor列表的资源
*
* @param map
*/
public void loadRF(Map<String, AssetFileDescriptor> map) {
Set<Map.Entry<String, AssetFileDescriptor>> entries = map.entrySet();
for (Map.Entry<String, AssetFileDescriptor> entry : entries) {
String key = entry.getKey();
if (checkSoundPool()) {
if (!idCache.containsKey(key)) {
idCache.put(key, mSoundPool.load(entry.getValue().getFileDescriptor(), entry.getValue().getStartOffset(), entry.getValue().getLength(), 1));
}
}
}
}
/**
* 加载指定列表资源
*
* @param context
* @param map
*/
public void loadR(Context context, Map<String, Integer> map) {
loadSize = map.size();
Set<Map.Entry<String, Integer>> entries = map.entrySet();
for (Map.Entry<String, Integer> entry : entries) {
String key = entry.getKey();
if (checkSoundPool()) {
if (!idCache.containsKey(key)) {
idCache.put(key, mSoundPool.load(context, entry.getValue(), 1));
}
}
}
}
/**
* 加载单个音频
*
* @param context
* @param name
* @param res
*/
public void loadR(Context context, String name, int res) {
if (checkSoundPool()) {
if (!idCache.containsKey(name)) {
idCache.put(name, mSoundPool.load(context, res, 1));
}
}
}
/**
* 播放指定音频,并返用于停止、暂停、恢复的StreamId
*
* @param name
* @param times
* @return
*/
public int play(String name, int times) {
L.e(String.format("play %s times=%s", name, times));
return this.play(name, 1, 1, 1, times, 1);
}
/**
* 播放指定音频,并指定播放次数和频率
*
* @param name
* @param times
* @param rate
* @return
*/
public int play(String name, int times, int rate) {
return this.play(name, 1, 1, 1, times, rate);
}
/**
* 播放指定音频,并指定优先级和播放频率
*
* @param name
* @param property
* @param times
* @param rate
* @return
*/
public int play(String name, int property, int times, int rate) {
return this.play(name, 1, 1, property, times, rate);
}
/**
* 播放指定音频,并指定左右声道、优先级、播放次数、播放频率
*
* @param name
* @param leftVolume
* @param rightVolume
* @param property
* @param times
* @param rate
* @return
*/
public int play(String name, float leftVolume, float rightVolume, int property, int times, int rate) {
int streamId = -1;
if (checkSoundPool()) {
L.d(TAG, "play: " + name);
if (idCache.containsKey(name) && isLoadC) {
L.d(TAG, "name:" + idCache.get(name));
streamId = mSoundPool.play(idCache.get(name), leftVolume, rightVolume, property, times, rate);
L.d(TAG, "streadmId:" + streamId);
sidCache.add(streamId);
}
}
return streamId;
}
/**
* 播放指定列表的音频,并返回并返用于停止、暂停、恢复的StreamId列表
*
* @param names
* @param times
* @return
*/
public List<Integer> play(List<String> names, int times) {
return this.play(names, 1, 1, 1, times, 1);
}
/**
* 播放指定列表的音频,并返回并返用于停止、暂停、恢复的StreamId列表,指定次数和频率
*
* @param names
* @param times
* @param rate
* @return
*/
public List<Integer> play(List<String> names, int times, int rate) {
return this.play(names, 1, 1, 1, times, rate);
}
/**
* 播放指定列表的音频,并返回并返用于停止、暂停、恢复的StreamId列表,指定所有参数
*
* @param names
* @param leftVolume
* @param rightVolume
* @param property
* @param times
* @param rate
* @return
*/
public List<Integer> play(List<String> names, int leftVolume, int rightVolume, int property, int times, int rate) {
List<Integer> streamIds = new ArrayList<>();
if (checkSoundPool()) {
for (String name : names) {
if (idCache.containsKey(name) && isLoadC) {
int a = mSoundPool.play(idCache.get(name), leftVolume, rightVolume, property, times, rate);
streamIds.add(a);
sidCache.add(a);
}
}
}
return streamIds;
}
/**
* 停止指定id音频
*/
public void stop(int r) {
if (checkSoundPool()) {
mSoundPool.stop(r);
}
}
/**
* 停止指定列表音频
*/
public void stopAll() {
if (checkSoundPool()) {
for (int r : sidCache) {
mSoundPool.stop(r);
}
}
}
/**
* 暂停指定音效
*
* @param r
*/
public void pause(int r) {
if (checkSoundPool()) {
mSoundPool.pause(r);
}
}
/**
* 暂停指定列表音频
*
* @param list
*/
public void pause(List<Integer> list) {
if (checkSoundPool()) {
for (int r : list) {
mSoundPool.pause(r);
}
}
}
/**
* 暂停所有音效
*/
public void pauseAll() {
mSoundPool.autoPause();
}
/**
* 恢复指定音频播放
*
* @param r
*/
public void resume(int r) {
if (checkSoundPool()) {
mSoundPool.resume(r);
}
}
/**
* 恢复指定列表的音频
*
* @param list
*/
public void resume(List<Integer> list) {
if (checkSoundPool()) {
for (int r : list) {
mSoundPool.resume(r);
}
}
}
/**
* 恢复所有暂停的音频
*/
public void resumeAll() {
if (checkSoundPool()) {
mSoundPool.autoResume();
}
}
/**
* 卸载指定音频
*
* @param name
*/
public void unLoad(String name) {
if (checkSoundPool()) {
if (idCache.containsKey(name)) {
mSoundPool.unload(idCache.get(name));
idCache.remove(name);
}
}
}
/**
* 卸载指定列表的音频
*
* @param names
*/
public void unLoad(List<String> names) {
if (checkSoundPool()) {
for (String name : names) {
if (idCache.containsKey(name)) {
mSoundPool.unload(idCache.get(name));
idCache.remove(name);
}
}
}
}
/**
* 释放所有资源,如果想继续播放,需要重新加载资源
*/
public void release() {
if (checkSoundPool()) {
mSound = null;
mSoundPool.release();
idCache.clear();
}
}
private boolean checkSoundPool() {
if (mSoundPool != null) {
return true;
}
return false;
}
public interface LoadCompletion {
void onCompletion();
}
}
@@ -1,42 +0,0 @@
package com.sw.platecabinet.utils;
import com.sw.inbound.utils.SPUtil;
import com.sw.platecabinet.GlobalKey;
import com.sw.platecabinet.MyApp;
public class SpTool {
public static final String LAST_FACE_TIMESTAMP = "faceTimestamp";
public static final String IS_FIRST_GET_FACE = "isFirstGetFace";
public static long getLastFaceTimestamp() {
return SPUtil.Companion.getInstance(MyApp.getContext(), "default_sp").get(LAST_FACE_TIMESTAMP, 0L);
}
public static void setLastFaceTimestamp(long timestamp) {
SPUtil.Companion.getInstance(MyApp.getContext(), "default_sp").put(LAST_FACE_TIMESTAMP, timestamp);
}
/**
* 是否首次拉取人脸缓存(Kotlin 侧以 SpTool.firstGetFace 属性语法访问)
*/
public static boolean getFirstGetFace() {
return SPUtil.Companion.getInstance(MyApp.getContext(), "default_sp").get(IS_FIRST_GET_FACE, true);
}
public static void setFirstGetFace(boolean value) {
SPUtil.Companion.getInstance(MyApp.getContext(), "default_sp").put(IS_FIRST_GET_FACE, value);
}
/**
* 业务服务器 BaseUrlKotlin 侧以 SpTool.baseUrl 属性语法访问)
*/
public static String getBaseUrl() {
return SPUtil.Companion.getInstance(MyApp.getContext(), "default_sp").get(GlobalKey.KEY_BASE_URL, "");
}
public static void setBaseUrl(String value) {
SPUtil.Companion.getInstance(MyApp.getContext(), "default_sp").put(GlobalKey.KEY_BASE_URL, value);
}
}
@@ -0,0 +1,52 @@
package com.sw.platecabinet.utils
import com.google.gson.Gson
import com.sw.inbound.utils.GsonUtils
import com.sw.inbound.utils.SPUtil
import com.sw.inbound.utils.SPUtil.Companion.getInstance
import com.sw.platecabinet.MyApp
import com.sw.platecabinet.model.response.EquipmentUserInfo
import kotlin.text.get
object SpTool {
const val LAST_FACE_TIMESTAMP: String = "faceTimestamp"
const val PLATE_BOX_DATA: String = "plateBoxBind"
const val IS_FIRST_GET_FACE = "isFirstGetFace"
private var spUtil: SPUtil? = null
init {
spUtil = getInstance(MyApp.instance!!, "default_sp")
}
fun putString(key:String, value:String) {
spUtil?.put(key, value)
}
var lastFaceTimestamp: Long
get() = spUtil?.get<Long?>(SpTool.LAST_FACE_TIMESTAMP, 0L) ?: 0
set(timestamp) {
spUtil?.put(LAST_FACE_TIMESTAMP, timestamp)
}
fun savePlateData(items: List<EquipmentUserInfo>) {
val jsonData = Gson().toJson(items)
putString(PLATE_BOX_DATA, jsonData)
}
fun getPlateData(): List<EquipmentUserInfo>? {
val jsonData = spUtil?.get(PLATE_BOX_DATA, "")
if (jsonData.isNullOrBlank()) {
return null
}
return GsonUtils.fromJsonList(jsonData, EquipmentUserInfo::class.java)
}
var firstGetFace: Boolean
get() = spUtil?.get(IS_FIRST_GET_FACE, true)?:true
set(value) {
spUtil?.boolean(IS_FIRST_GET_FACE, value)
}
}
@@ -3,6 +3,7 @@ package com.sw.platecabinet.viewmodel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.model.response.ApiResponse import com.sw.platecabinet.model.response.ApiResponse
import com.sw.platecabinet.model.response.EquipmentInfo import com.sw.platecabinet.model.response.EquipmentInfo
import com.sw.platecabinet.network.ApiClient import com.sw.platecabinet.network.ApiClient
@@ -1,325 +0,0 @@
package com.sw.platecabinet.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.arcsoft.face.ErrorInfo
import com.sw.inbound.utils.SPUtil
import com.sw.plate.App
import com.sw.plate.utils.Base64
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.FaceApi
import com.sw.plate.utils.arcface.facedb.FaceDatabase
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.GlobalKey
import com.sw.platecabinet.model.DeviceConfigV2
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.model.response.UserFaceModelV2
import com.sw.platecabinet.network.ApiClient
import com.sw.platecabinet.repository.RemoteRepositoryV2
import com.sw.platecabinet.utils.SpTool
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
/**
* 用户viewmodel
*/
class NetViewModelV2 : ViewModel() {
companion object {
const val PAGE_SIZE = 100
}
private val faceApi: FaceApi = FaceApi()
private val repository = RemoteRepositoryV2(ApiClient.apiServiceV2)
//fun getUserFace(
// pageNo: Int,
// pageSize: Int,
// callback: (Boolean, List<UserFaceModelV2>) -> Unit
//) {
// viewModelScope.launch {
// val response = repository.getUserFaceCache(pageNum = pageNo, pageSize = pageSize)
// if (response.isSuccess()) callback(true, response.data ?: emptyList())
// else callback(false, emptyList())
// }
//}
/**
* 获取人脸数据
*/
fun getUserFaceCache(
pageNo: Int = 1,
pageSize: Int = PAGE_SIZE,
callback: (Boolean, String?) -> Unit = { _, _ -> }
) {
var currentPageNo = pageNo
viewModelScope.launch {
val response = repository.getUserFaceCache(pageNo, pageSize)
if (response.isSuccess().not()) {
callback(false, response.msg)
return@launch
}
// 获取成功一次后缓存状态
SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true)
val list = response.data ?: emptyList()
if (pageNo == 1 && list.isEmpty()) {
callback(false, "暂无数据")
return@launch
}
val faceEntityList = list.mapNotNull { face ->
val featureBase64 = face.resolveFeatureStr()
if (featureBase64.isNullOrBlank()) null
else {
val featureData = base64ToByteArray(featureBase64)
if (featureData == null) null
else face.toFaceEntity()
}
}
withContext(Dispatchers.Default) {
if (currentPageNo == 1) {
//第一页数据删除本地数据库,同时重置时间戳
faceApi.clearFaceData()
faceTimestamp = 0L
}
faceApi.updateFaceData(pageNo, faceEntityList)
if (list.size >= pageSize) {
// 取当前页最大时间戳,与已累积的比较取最大值
val pageMaxTimestamp = maxFaceTimestamp(list)
if (pageMaxTimestamp > faceTimestamp) {
faceTimestamp = pageMaxTimestamp
}
currentPageNo++
getUserFaceCache(
pageNo = currentPageNo, callback = callback
)
return@withContext
}
if (list.isNotEmpty()) {
// 最后一页:取当前页最大时间戳,与已累积的比较取最大值
val pageMaxTimestamp = maxFaceTimestamp(list)
if (pageMaxTimestamp > faceTimestamp) {
faceTimestamp = pageMaxTimestamp
}
}
SpTool.setLastFaceTimestamp(faceTimestamp)
callback(true, "查询完成")
}
}
}
private var faceTimestamp = 0L
/**
* 激活人脸识别引擎
*/
fun activeEngine() {
Timber.d("activeEngine")
faceApi.activeEngine(
App.getContext(),
GlobalData.appId,
GlobalData.sdkKey,
GlobalData.activeKey,
object : FaceApi.ActiveCallback {
override fun onSuccess(activeCode: Int) {
Timber.d("activeEngine activeCode = $activeCode")
// ThreadUtils.launch {
viewModelScope.launch(Dispatchers.Main) {
when (activeCode) {
ErrorInfo.MOK -> {
ToastUtils.showToast("激活引擎成功")
}
ErrorInfo.MERR_ASF_ALREADY_ACTIVATED -> {
// ToastUtils.showToast("引擎已激活,无需再次激活")
}
else -> {
ToastUtils.showToast("激活引擎失败($activeCode)")
}
}
}
}
override fun onFail(e: Exception?) {
viewModelScope.launch(Dispatchers.Main) {
ToastUtils.showToast("激活引擎异常,${e?.message}")
}
}
})
}
fun getDeviceConfig(onSuccess: (DeviceConfigV2?) -> Unit, onFailure: (String?) -> Unit) {
// Timber.tag(TAG).d("getDeviceConfig")
viewModelScope.launch {
val response = repository.getDeviceConfig()
if (response.isSuccess()) {
onSuccess(response.data)
} else {
onFailure(response.msg)
}
}
}
/**
* 增量获取人脸数据(支持分页递归,取所有页中的最大时间戳)
*/
fun getFaceIncrementList(
pageNo: Int = 1,
pageSize: Int = PAGE_SIZE,
timestamp: Long,
refreshFaceList: () -> Unit
) {
var currentPageNo = pageNo
viewModelScope.launch {
val response = repository.getFaceIncrementList(
pageNum = currentPageNo.toLong(),
pageSize = pageSize.toLong(),
timestamp = timestamp
)
if (response.isSuccess().not()) {
return@launch
}
withContext(Dispatchers.IO) {
val list = response.data ?: emptyList()
updateFaceData(list)
// 取当前页最大时间戳,与已累积的比较取最大值
val pageMaxTimestamp = maxFaceTimestamp(list)
if (pageMaxTimestamp > faceTimestamp) {
faceTimestamp = pageMaxTimestamp
}
if (list.size >= pageSize) {
// 还有下一页,继续递归拉取
currentPageNo++
getFaceIncrementList(
pageNo = currentPageNo,
pageSize = pageSize,
timestamp = timestamp,
refreshFaceList = refreshFaceList
)
return@withContext
}
// 所有页拉取完毕,保存最终时间戳(只升不降)
val storedTimestamp = SpTool.getLastFaceTimestamp()
if (faceTimestamp > storedTimestamp) {
SpTool.setLastFaceTimestamp(faceTimestamp)
}
try {
refreshFaceList()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
private fun updateFaceData(list: List<UserFaceModelV2>) {
try {
list.forEach { model ->
if (model.faceDeleted == true) {
//删除数据
// 仅删除 userFaceId 对应的单条人脸记录,避免误删该用户名下所有特征
val userFaceId = model.userFaceId
if (!userFaceId.isNullOrEmpty()) {
faceApi.deleteByUserFaceId(userFaceId)
}
} else {
// 优先按 userFaceId 精确判重,避免特征字段不一致导致重复入库
val userFaceId = model.userFaceId
if (!userFaceId.isNullOrEmpty() && faceApi.queryByUserFaceId(userFaceId) != null) {
// 本地已存在该 userFaceId,跳过插入
return@forEach
}
// userFaceId 为空时,回退到特征比较去重(修正字段来源,保证与入库一致)
val featureStr = model.resolveFeatureStr()
val existList = faceApi.queryAllByUserName(model.userId)
val alreadyExists = existList.any { entity ->
featureStr != null && Base64.encode(entity.featureData) == featureStr
}
if (!alreadyExists) {
faceApi.insert(model.toFaceEntity())
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
/**
* 添加人脸数据
*/
fun addUserFace(faceData: String, imageFile: File, block: (Boolean, UserFaceModelV2?) -> Unit) {
viewModelScope.launch {
try {
val imageResp = repository.uploadImage(imageFile)
val imageUrl = imageResp.data ?: ""
if (imageUrl.isBlank()) {
block(false, null)
return@launch
}
val faceResp = repository.addUserFace(faceData, imageUrl)
if (faceResp.isSuccess().not()) {
block(false, null)
return@launch
}
block(true, faceResp.data)
} catch (e: Exception) {
// 网络或解析异常时同样回调失败,避免 block 永不触发导致上层无反馈
e.printStackTrace()
block(false, null)
}
}
}
/**
* 取餐盘上报(P-09a):记录用户开始就餐时刻,作为后续就餐记录(P-09)的强前置。
* 失败仅回调通知,不阻断吐盘等业务主流程。
*
* @param userId 用户 id(餐盘号 = userId
* @param callback 上报结果回调:Boolean 是否成功,String? 成功为 recordNo、失败为错误信息
*/
fun platePickup(
userId: Long,
callback: (Boolean, String?) -> Unit = { _, _ -> }
) {
viewModelScope.launch {
try {
val response = repository.platePickup(userId)
if (response.isSuccess()) {
callback(true, response.data)
} else {
callback(false, response.msg)
}
} catch (e: Exception) {
e.printStackTrace()
callback(false, e.message)
}
}
}
private fun base64ToByteArray(base64: String?): ByteArray? {
base64 ?: return null
return try {
Base64.decode(base64)
} catch (e: Exception) {
null
}
}
/**
* 从人脸数据列表中取出最大的 faceUpdateTimestamp(跳过 null 值)
*/
private fun maxFaceTimestamp(list: List<UserFaceModelV2>): Long {
return list.mapNotNull { it.faceUpdateTimestamp }.maxOrNull() ?: 0L
}
}
@@ -1,22 +1,14 @@
package com.sw.platecabinet.viewmodel package com.sw.platecabinet.viewmodel
import android.text.TextUtils
import com.sw.inbound.utils.DateTimeUtils
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.GlobalData.globalEquipmentCode import com.sw.platecabinet.GlobalData.globalEquipmentCode
import com.sw.platecabinet.model.ErrorInfo
import com.sw.platecabinet.model.request.BindParam
import com.sw.platecabinet.model.request.EquipmentParam
import com.sw.platecabinet.model.request.SearchParam
import com.sw.platecabinet.model.response.EquipmentUserInfo import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.SearchResult import com.sw.platecabinet.model.response.SearchResult
import com.sw.platecabinet.model.response.UserFaceModel import com.sw.platecabinet.model.response.UserFaceModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import java.io.File
/** /**
* 管理员操作viewmodel * 管理员操作viewmodel
@@ -61,53 +53,53 @@ class SettingViewModel : BaseViewModel() {
//// } //// }
//// _equipmentList.value = list //// _equipmentList.value = list
// } // }
fun getEquipmentList(block: (List<EquipmentUserInfo>) -> Unit) { // fun getEquipmentList(block: (List<EquipmentUserInfo>) -> Unit) {
launchWithLoading { // launchWithLoading {
val response = repository.getEquipmentList() // val response = repository.getEquipmentList()
if (parseResponse(response)) { // if (parseResponse(response)) {
val list = response.data ?: emptyList() // val list = response.data ?: emptyList()
block(list) // block(list)
} // }
} // }
} // }
/** // /**
* 搜索会员信息列表 // * 搜索会员信息列表
*/ // */
fun getSearchMemberList(param:String, pageNum: Int = 1, pageSize: Int = 50) { // fun getSearchMemberList(param:String, pageNum: Int = 1, pageSize: Int = 50) {
if (TextUtils.isEmpty(param)) return // if (TextUtils.isEmpty(param)) return
currentPage = pageNum // currentPage = pageNum
var name ="" // var name =""
var phone ="" // var phone =""
val num = param.toIntOrNull() // val num = param.toIntOrNull()
if (num != null) { // if (num != null) {
phone = param // phone = param
} else { // } else {
name = param // name = param
} // }
launch { // launch {
val searchParam = SearchParam( // val searchParam = SearchParam(
// appVersion = GlobalData.appVersion, //// appVersion = GlobalData.appVersion,
name = name, // name = name,
phone = phone, // phone = phone,
pageNum = pageNum, // pageNum = pageNum,
pageSize = pageSize // pageSize = pageSize
) // )
isLoading = true // isLoading = true
val response = repository.searchUser(searchParam) // val response = repository.searchUser(searchParam)
isLoading = false // isLoading = false
if (parseResponse(response)) { // if (parseResponse(response)) {
response.data?.let { list -> // response.data?.let { list ->
if (pageNum == 1) { // if (pageNum == 1) {
_searchMemberList.value = list // _searchMemberList.value = list
} else { // } else {
_searchMemberList.value = _searchMemberList.value.plus(list) // _searchMemberList.value = _searchMemberList.value.plus(list)
} // }
if (list.size >= pageSize) currentPage++ // if (list.size >= pageSize) currentPage++
} // }
} // }
} // }
} // }
// fun bindPlate( // fun bindPlate(
// equipmentBoxCode: String, // equipmentBoxCode: String,
@@ -158,54 +150,54 @@ class SettingViewModel : BaseViewModel() {
// } // }
// } // }
fun unbindPlate(id: Long?, block:(Pair<Boolean?, ErrorInfo>)-> Unit) { // fun unbindPlate(id: Long?, block:(Pair<Boolean?, ErrorInfo>)-> Unit) {
block(null to ErrorInfo()) // block(null to ErrorInfo())
launchWithLoading { // launchWithLoading {
val response = repository.plateUnbind(id) // val response = repository.plateUnbind(id)
if (parseResponse(response)) { // if (parseResponse(response)) {
block(false to ErrorInfo()) // block(false to ErrorInfo())
} else { // } else {
block(false to ErrorInfo(response.code, response.msg.toString())) // block(false to ErrorInfo(response.code, response.msg.toString()))
} // }
} // }
} // }
fun bindPlate(bindParam: BindParam, block:(Pair<Boolean?, ErrorInfo>)-> Unit) { // fun bindPlate(bindParam: BindParam, block:(Pair<Boolean?, ErrorInfo>)-> Unit) {
launchWithLoading { // launchWithLoading {
val response = repository.plateBind(bindParam) //// val response = repository.plateBind(bindParam)
if (parseResponse(response)) { //// if (parseResponse(response)) {
var userInfo = response.data //// var userInfo = response.data
if (userInfo == null) { //// if (userInfo == null) {
userInfo = EquipmentUserInfo() //// userInfo = EquipmentUserInfo()
userInfo.equipmentBoxCode = bindParam.equipmentBoxCode //// userInfo.equipmentBoxCode = bindParam.equipmentBoxCode
userInfo.plateNumber = bindParam.plateNumber //// userInfo.plateNumber = bindParam.plateNumber
userInfo.equipmentCode = bindParam.equipmentCode //// userInfo.equipmentCode = bindParam.equipmentCode
} //// }
block(true to ErrorInfo(equipmentUserInfo = userInfo)) //// block(true to ErrorInfo(equipmentUserInfo = userInfo))
} else { //// } else {
block(true to ErrorInfo(response.code, response.msg.toString())) //// block(true to ErrorInfo(response.code, response.msg.toString()))
} //// }
} // }
} // }
fun findByPlateNumber( fun findByPlateNumber(
plateNumber: String, plateNumber: String,
equipmentCode: String = globalEquipmentCode equipmentCode: String = globalEquipmentCode
) { ) {
_searchUserInfo.value = null // _searchUserInfo.value = null
launchWithLoading { // launchWithLoading {
// val bindParam = BindParam(plateNumber = plateNumber) //// val bindParam = BindParam(plateNumber = plateNumber)
val response = repository.findByPlateNumber(plateNumber) // val response = repository.findByPlateNumber(plateNumber)
if (response.code == "00000") { // if (response.code == "00000") {
_searchUserInfo.value = response.data // _searchUserInfo.value = response.data
} else { // } else {
_searchUserInfo.value = EquipmentUserInfo( // _searchUserInfo.value = EquipmentUserInfo(
plateNumber = plateNumber, // plateNumber = plateNumber,
equipmentCode = equipmentCode, // equipmentCode = equipmentCode,
updateTime = DateTimeUtils.getDateTimeString() // updateTime = DateTimeUtils.getDateTimeString()
) // )
} // }
} // }
} }
val PAGE_SIZE = 100 val PAGE_SIZE = 100
@@ -234,4 +226,25 @@ class SettingViewModel : BaseViewModel() {
} }
} }
/**
* 添加人脸数据
*/
fun addUserFace(faceData: String, imageFile: File, block: (Boolean, UserFaceModel?) -> Unit) {
launch {
val imageResp = repository.uploadImage(imageFile)
val imageUrl = imageResp.data?:""
if (imageUrl.isBlank()) {
block(false, null)
return@launch
}
val faceResp = repository.addUserFace(faceData, imageUrl)
val status = parseResponse(faceResp)
if (status.not()) {
block(false, null)
return@launch
}
block(true, faceResp.data)
}
}
} }
@@ -9,16 +9,12 @@ import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.FaceApi import com.sw.plate.utils.arcface.FaceApi
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import com.sw.platecabinet.GlobalData import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.GlobalData.globalEquipmentCode
import com.sw.platecabinet.GlobalKey import com.sw.platecabinet.GlobalKey
import com.sw.platecabinet.model.DeviceConfig import com.sw.platecabinet.model.DeviceConfig
import com.sw.platecabinet.model.request.LoginParam import com.sw.platecabinet.model.request.LoginParam
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.UserFaceModel import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.utils.SpTool import com.sw.platecabinet.utils.SpTool
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
@@ -33,8 +29,8 @@ class UserViewModel : BaseViewModel() {
} }
// 通过人脸获取到的用户信息 // 通过人脸获取到的用户信息
private val _currentUserInfo = MutableStateFlow<EquipmentUserInfo?>(null) // private val _currentUserInfo = MutableStateFlow<EquipmentUserInfo?>(null)
val currentUserInfo: StateFlow<EquipmentUserInfo?> = _currentUserInfo // val currentUserInfo: StateFlow<EquipmentUserInfo?> = _currentUserInfo
private val faceApi: FaceApi = FaceApi() private val faceApi: FaceApi = FaceApi()
@@ -55,43 +51,74 @@ class UserViewModel : BaseViewModel() {
// } // }
// } // }
fun getUserFace(
pageNo: Int,
pageSize: Int,
callback: (List<UserFaceModel>) -> Unit
) {
launchWithLoading{
val response = repository.getUserFaceCache(pageNum = pageNo, pageSize = pageSize)
val status = parseResponse(response)
if (status) callback(response.data?:emptyList())
else emptyList<UserFaceModel>()
}
}
/** /**
* 获取人脸数据 * 获取人脸数据
*/ */
fun getUserFaceCache(pageNo: Int = 1, pageSize: Int = PAGE_SIZE) { fun getUserFaceCache(
pageNo: Int = 1,
pageSize: Int = PAGE_SIZE,
callback: (Boolean, String?) -> Unit = { statue, msg -> }
) {
var currentPageNo = pageNo var currentPageNo = pageNo
launchWithLoading { launchWithLoading {
val response = repository.getUserFaceCache(pageNum = pageNo, pageSize = pageSize) val response = repository.getUserFaceCache(pageNum = pageNo, pageSize = pageSize)
if (parseResponse(response)) { val status = parseResponse(response)
// 获取成功一次后缓存状态 if (status.not()) {
SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true) callback(false, response.msg)
val list: List<UserFaceModel> = response.data ?: emptyList() return@launchWithLoading
if (pageNo==1&&list.isEmpty()) { }
return@launchWithLoading // 获取成功一次后缓存状态
SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true)
val list: List<UserFaceModel> = response.data ?: emptyList()
if (pageNo == 1 && list.isEmpty()) {
callback(true, "暂无数据")
return@launchWithLoading
}
val faceEntityList = list.map {
FaceEntity(it.userId, null, Base64.decode(it.faceFeatureStr)).apply {
userType = if (it.member) "1" else "2"
} }
val faceEntityList = list.map { }
FaceEntity(it.userId, null, Base64.decode(it.faceFeatureStr)) withContext(Dispatchers.Default) {
if (currentPageNo == 1) {
//第一页数据删除本地数据库
faceApi.clearFaceData()
} }
withContext(Dispatchers.Default) { faceApi.updateFaceData(pageNo, faceEntityList)
faceApi.updateFaceData(pageNo, faceEntityList) if (list.size >= pageSize) {
if (list.size >= pageSize) { faceTimestamp = list.last().faceUpdateTimestamp ?: 0
faceTimestamp = list.last().faceUpdateTimestamp?:0 currentPageNo++
currentPageNo++ getUserFaceCache(
getUserFaceCache(currentPageNo) pageNo = currentPageNo,
return@withContext callback = callback
} )
if (list.isNotEmpty()) { return@withContext
faceTimestamp = list.last().faceUpdateTimestamp?:0
}
SpTool.setLastFaceTimestamp(faceTimestamp)
//activeEngine()
} }
if (list.isNotEmpty()) {
faceTimestamp = list.last().faceUpdateTimestamp ?: 0
}
SpTool.lastFaceTimestamp = faceTimestamp
callback(true, "查询完成")
} }
} }
} }
private var faceTimestamp = 0L private var faceTimestamp = 0L
/** /**
* 激活人脸识别引擎 * 激活人脸识别引擎
*/ */
@@ -136,36 +163,37 @@ class UserViewModel : BaseViewModel() {
* 校验码登录 * 校验码登录
*/ */
fun loginWithPwd(loginParam: LoginParam) { fun loginWithPwd(loginParam: LoginParam) {
launchWithLoading { // launchWithLoading {
val response = repository.equipmentBoxLogin(loginParam) // val response = repository.equipmentBoxLogin(loginParam)
if (parseResponse(response)) { // if (parseResponse(response)) {
_currentUserInfo.value = response.data // _currentUserInfo.value = response.data
} // }
} // }
} }
/** /**
* 通过用户id获取用户信息 * 通过用户id获取用户信息
*/ */
fun getUserInfoById(memberId: String?, action:(EquipmentUserInfo?)->Unit={}) { // fun getUserInfoById(memberId: String?, action: (EquipmentUserInfo?) -> Unit = {}) {
_currentUserInfo.value = null // _currentUserInfo.value = null
launchWithLoading { // launchWithLoading {
val loginParam = LoginParam(faceId = memberId) // val loginParam = LoginParam(faceId = memberId)
val response = repository.equipmentBoxLogin(loginParam) // val response = repository.equipmentBoxLogin(loginParam)
if (parseResponse(response)) { // if (parseResponse(response)) {
_currentUserInfo.value = response.data // _currentUserInfo.value = response.data
action(response.data) // action(response.data)
} // }
} // }
} // }
/** /**
* 重置用户信息 * 重置用户信息
*/ */
fun resetUserInfo() { fun resetUserInfo() {
Timber.d("resetUserInfo") Timber.d("resetUserInfo")
_currentUserInfo.value = null // _currentUserInfo.value = null
} }
fun getDeviceConfig(block: (DeviceConfig?) -> Unit) { fun getDeviceConfig(block: (DeviceConfig?) -> Unit) {
// Timber.tag(TAG).d("getDeviceConfig") // Timber.tag(TAG).d("getDeviceConfig")
launch { launch {
+20 -3
View File
@@ -1,18 +1,35 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item <item
android:width="600dp" android:width="600dp"
android:height="1024dp"> android:height="1024dp">
<bitmap android:src="@drawable/bg"/> <bitmap android:src="@drawable/bg" />
</item> </item>
<item <item
android:width="337dp" android:width="337dp"
android:height="322dp" android:height="322dp"
android:gravity="top|center_horizontal" android:gravity="top|center_horizontal"
android:top="112dp"> android:top="112dp">
<bitmap android:src="@drawable/img_init" android:scaleType="fitCenter"/> <bitmap
android:scaleType="fitCenter"
android:src="@drawable/img_init" />
</item> </item>
<!-- TODO 临时测试 -->
<!-- <item-->
<!-- android:width="828dp"-->
<!-- android:height="1768dp">-->
<!-- <bitmap android:src="@drawable/bg" />-->
<!-- </item>-->
<!-- <item-->
<!-- android:width="337dp"-->
<!-- android:height="322dp"-->
<!-- android:gravity="top|center_horizontal"-->
<!-- android:top="360dp">-->
<!-- <bitmap-->
<!-- android:scaleType="fitCenter"-->
<!-- android:src="@drawable/img_init" />-->
<!-- </item>-->
</layer-list> </layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/white"/>
<corners android:radius="30dp"/>
</shape>
@@ -6,14 +6,14 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<ImageView <!-- <ImageView-->
android:layout_width="337dp" <!-- android:layout_width="337dp"-->
android:layout_height="322dp" <!-- android:layout_height="322dp"-->
android:layout_gravity="center_horizontal" <!-- android:layout_gravity="center_horizontal"-->
android:layout_marginTop="112dp" <!-- android:layout_marginTop="112dp"-->
android:src="@drawable/img_init" <!-- android:src="@drawable/img_init"-->
tools:ignore="ContentDescription" <!-- tools:ignore="ContentDescription"-->
android:visibility="invisible"/> <!-- android:visibility="invisible"/>-->
<TextView <TextView
android:id="@+id/btnInit" android:id="@+id/btnInit"
+110
View File
@@ -0,0 +1,110 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg"
android:orientation="vertical"
xmlns:tools="http://schemas.android.com/tools">
<include
android:id="@+id/includeHeader"
layout="@layout/item_title_time" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:gravity="center_vertical"
android:layout_marginTop="30dp">
<TextView
android:id="@+id/tvMemberUserCount"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:textSize="16sp"
android:textColor="@color/white"
android:gravity="center"
tools:text="会员用户:100人"/>
<View
android:layout_width="1dp"
android:layout_height="30dp"
android:background="@color/white"/>
<TextView
android:id="@+id/tvTempUserCount"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:textSize="16sp"
android:textColor="@color/white"
android:gravity="center"
tools:text="临时用户:100人"/>
</LinearLayout>
<TextView
android:id="@+id/btnClearFace"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="30dp"
android:textSize="24sp"
android:gravity="center"
android:layout_marginHorizontal="50dp"
android:paddingHorizontal="30dp"
android:background="@drawable/btn_outline"
android:text="清除人脸数据"
android:textColor="#FFCC99" />
<TextView
android:id="@+id/btnClearAndGetAll"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="30dp"
android:textSize="24sp"
android:gravity="center"
android:layout_marginHorizontal="50dp"
android:paddingHorizontal="30dp"
android:background="@drawable/btn_outline"
android:text="清除并拉取全量数据"
android:textColor="#FFCC99" />
<TextView
android:id="@+id/btnOpenAllPlate"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="30dp"
android:textSize="24sp"
android:gravity="center"
android:layout_marginHorizontal="50dp"
android:paddingHorizontal="30dp"
android:background="@drawable/btn_outline"
android:text="打开全部柜子"
android:textColor="#FFCC99" />
<TextView
android:id="@+id/btnPutPlate"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="30dp"
android:textSize="24sp"
android:gravity="center"
android:layout_marginHorizontal="50dp"
android:paddingHorizontal="30dp"
android:background="@drawable/btn_outline"
android:text="模拟放置餐盘"
android:textColor="#FFCC99" />
<TextView
android:id="@+id/btnTestFeature"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="30dp"
android:textSize="24sp"
android:gravity="center"
android:layout_marginHorizontal="50dp"
android:paddingHorizontal="30dp"
android:background="@drawable/btn_outline"
android:text="人脸特征比较"
android:textColor="#FFCC99"
android:visibility="gone"/>
</LinearLayout>
+17 -9
View File
@@ -43,16 +43,24 @@
<!-- android:layout_marginTop="20dp"--> <!-- android:layout_marginTop="20dp"-->
<!-- android:src="@drawable/ic_init_press" />--> <!-- android:src="@drawable/ic_init_press" />-->
<TextView android:id="@+id/takeButton" <FrameLayout
android:id="@+id/takeButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="60dp" android:layout_height="wrap_content"
android:text="点击取盘" android:layout_marginTop="-35dp">
android:textColor="#FFCC99" <TextView
android:textSize="24sp" android:layout_width="wrap_content"
android:paddingHorizontal="45dp" android:layout_height="60dp"
android:gravity="center" android:text="点击取盘"
android:background="@drawable/btn_outline" android:textColor="#FFCC99"
tools:ignore="HardcodedText" /> android:textSize="24sp"
android:layout_marginHorizontal="60dp"
android:layout_marginVertical="50dp"
android:paddingHorizontal="45dp"
android:gravity="center"
android:background="@drawable/btn_outline"
tools:ignore="HardcodedText" />
</FrameLayout>
</LinearLayout> </LinearLayout>
<View <View
+150 -22
View File
@@ -49,7 +49,8 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginTop="0dp" android:layout_marginTop="0dp"
android:background="@drawable/bg_face" android:scaleType="centerCrop"
android:src="@drawable/bg_face"
android:visibility="visible" /> android:visibility="visible" />
<TextView <TextView
@@ -59,39 +60,166 @@
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:layout_marginTop="129dp" android:layout_marginTop="129dp"
android:text="请正视屏幕" android:text="请正视屏幕"
android:textStyle="bold"
android:textColor="#FFCC99" android:textColor="#FFCC99"
android:textSize="36sp" /> android:textSize="36sp"
android:textStyle="bold" />
<LinearLayout <TextView
android:id="@+id/ll_to_pwd" android:id="@+id/tvSimilarValue"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="208dp" android:textSize="26sp"
android:layout_marginTop="730dp" android:layout_marginTop="80dp"
android:background="@drawable/btn_outline" tools:text="0.6183次"
android:gravity="center" android:layout_gravity="center_horizontal"
android:paddingHorizontal="45dp" android:textColor="@color/white"/>
android:paddingVertical="18dp">
<TextView <!-- <LinearLayout-->
android:layout_width="wrap_content" <!-- android:id="@+id/ll_to_pwd"-->
android:layout_height="wrap_content" <!-- android:layout_width="wrap_content"-->
android:text="密码登录" <!-- android:layout_height="wrap_content"-->
android:textColor="#FFCC99" <!-- android:layout_marginStart="208dp"-->
android:textSize="24sp" /> <!-- android:layout_marginTop="730dp"-->
</LinearLayout> <!-- android:background="@drawable/btn_outline"-->
<!-- android:gravity="center"-->
<!-- android:paddingHorizontal="45dp"-->
<!-- android:paddingVertical="18dp"-->
<!-- android:visibility="gone">-->
<!-- <TextView-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="密码登录"-->
<!-- android:textColor="#FFCC99"-->
<!-- android:textSize="24sp" />-->
<!-- </LinearLayout>-->
<TextView <TextView
android:id="@+id/tv_to_init" android:id="@+id/tv_to_init"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:text="30"
android:padding="12dp"
android:gravity="center"
android:layout_marginTop="185dp" android:layout_marginTop="185dp"
android:gravity="center"
android:padding="12dp"
android:textColor="#FFCC99" android:textColor="#FFCC99"
android:textSize="36sp" /> android:textSize="36sp"
tools:text="30" />
<LinearLayout
android:id="@+id/llCollectButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_gravity="bottom"
android:layout_marginBottom="100dp"
android:visibility="gone">
<TextView
android:id="@+id/btnRetry"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:text="再次重试"
android:textColor="#FFCC99"
android:textSize="24sp"
android:layout_marginHorizontal="12dp"
android:layout_marginVertical="10dp"
android:paddingHorizontal="45dp"
android:gravity="center"
android:background="@drawable/btn_outline"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/btnCollect"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:text="新用户采集"
android:textColor="#FFCC99"
android:textSize="24sp"
android:layout_marginHorizontal="12dp"
android:layout_marginVertical="10dp"
android:paddingHorizontal="45dp"
android:gravity="center"
android:background="@drawable/btn_outline"
tools:ignore="HardcodedText" />
</LinearLayout>
<!-- <LinearLayout-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:gravity="center_horizontal"-->
<!-- android:layout_gravity="bottom|center_horizontal"-->
<!-- android:layout_marginBottom="130dp"-->
<!-- android:orientation="vertical">-->
<!-- <LinearLayout-->
<!-- android:id="@+id/layoutState"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="50dp"-->
<!-- android:gravity="center_vertical"-->
<!-- android:orientation="horizontal"-->
<!-- android:visibility="invisible">-->
<!-- <ProgressBar-->
<!-- android:id="@+id/pbCollectLoading"-->
<!-- android:layout_width="32dp"-->
<!-- android:layout_height="32dp"-->
<!-- android:indeterminateTint="@color/white" />-->
<!-- <TextView-->
<!-- android:id="@+id/tvCollectState"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginStart="10dp"-->
<!-- android:text="采集中......"-->
<!-- android:textColor="@color/white"-->
<!-- android:textSize="24sp" />-->
<!-- </LinearLayout>-->
<!-- <TextView-->
<!-- android:id="@+id/btnCollectFace"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="60dp"-->
<!-- android:background="@drawable/btn_outline"-->
<!-- android:gravity="center"-->
<!-- android:layout_marginTop="10dp"-->
<!-- android:paddingHorizontal="45dp"-->
<!-- android:text="开始采集"-->
<!-- android:textColor="#FFCC99"-->
<!-- android:textSize="24sp"-->
<!-- tools:ignore="HardcodedText" />-->
<!-- </LinearLayout>-->
<!-- <androidx.recyclerview.widget.RecyclerView-->
<!-- android:id="@+id/rvLogInfo"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="match_parent"-->
<!-- android:layout_gravity="bottom"-->
<!-- android:overScrollMode="never"-->
<!-- android:visibility="gone"-->
<!-- android:background="@color/black"-->
<!-- tools:listitem="@layout/list_item_log_info" />-->
<!-- <ImageView-->
<!-- android:id="@+id/btnLogState"-->
<!-- android:layout_width="60dp"-->
<!-- android:layout_height="60dp"-->
<!-- android:layout_gravity="bottom|end"-->
<!-- android:background="@drawable/shape_white_circle"-->
<!-- android:padding="10dp"-->
<!-- android:layout_marginEnd="10dp"-->
<!-- android:layout_marginBottom="10dp"-->
<!-- android:src="@drawable/ic_log_show"-->
<!-- android:textSize="16sp" />-->
<ImageView
android:id="@+id/ivCollectPhoto"
android:layout_width="200dp"
android:layout_height="200dp"
android:adjustViewBounds="true"
android:layout_gravity="top|start"
android:visibility="gone"/>
</FrameLayout> </FrameLayout>
</LinearLayout> </LinearLayout>
+77
View File
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_dialog"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginTop="30dp"
android:src="@drawable/ic_tip_warn_unbind" />
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:textColor="#ffffffff"
android:textSize="28sp"
android:textStyle="bold"
tools:text="即将解除【曹德磊】的餐盘"
android:visibility="gone" />
<TextView
android:id="@+id/tvContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:layout_marginHorizontal="50dp"
android:gravity="center"
tools:text="解绑后该用户将无法使用餐盘柜"
android:textColor="#ff999999"
android:textSize="24sp"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="30dp"
android:background="#ff333333" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="90dp"
android:orientation="horizontal">
<TextView
android:id="@+id/btnCancel"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="取消"
android:textColor="#ff999999"
android:textSize="30sp"
android:textStyle="bold" />
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="#333333" />
<TextView
android:id="@+id/btnConfirm"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="确定"
android:textColor="#ffffcc99"
android:textSize="30sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
+12 -14
View File
@@ -1,31 +1,29 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="match_parent"> android:layout_height="wrap_content">
<RelativeLayout <LinearLayout
android:layout_width="240dp" android:layout_width="240dp"
android:layout_height="80dp" android:layout_height="80dp"
android:gravity="center_vertical"
android:background="@drawable/dialog_waiting"> android:background="@drawable/dialog_waiting">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:indeterminateTint="@color/teal_200"
android:indeterminateTintMode="src_atop" />
<TextView <TextView
android:id="@+id/tvTip" android:id="@+id/tvTip"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_marginStart="30dp"
android:textColor="@color/white" android:textColor="@color/white"
android:text="请稍等" android:text="请稍等"
android:textSize="22sp" /> android:textSize="22sp" />
<ProgressBar </LinearLayout>
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginRight="30dp"
android:layout_toLeftOf="@id/tvTip"
android:indeterminateTint="@color/teal_200"
android:indeterminateTintMode="src_atop" />
</RelativeLayout>
</FrameLayout> </FrameLayout>
@@ -1,16 +1,23 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main" android:id="@+id/main"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
tools:context=".fragment.SettingListFragment"> tools:context=".fragment.SettingListFragment"
tools:background="@color/black">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/mainRecyclerView" android:id="@+id/mainRecyclerView"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent"
app:spanCount="2"
tools:itemCount="22"
android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
tools:listitem="@layout/item_bind_view"/>
</LinearLayout> </LinearLayout>
+6 -5
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/ll_root" android:id="@+id/ll_root"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -15,7 +16,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="24dp" android:layout_marginStart="24dp"
android:text="02" tools:text="02"
android:textColor="#FFCC99" android:textColor="#FFCC99"
android:textSize="24sp" android:textSize="24sp"
android:textStyle="bold" /> android:textStyle="bold" />
@@ -44,11 +45,10 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="24dp" android:layout_marginStart="24dp"
android:text="马*飞(8000)"
android:textColor="#585868" android:textColor="#585868"
android:textSize="18sp" android:textSize="18sp"
android:textStyle="bold" /> android:textStyle="bold"
tools:text="马*飞(8000)" />
<TextView <TextView
android:id="@+id/tv_last_time" android:id="@+id/tv_last_time"
@@ -58,7 +58,8 @@
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:text="9小时前使用" android:text="9小时前使用"
android:textColor="#585868" android:textColor="#585868"
android:textSize="14sp" /> android:textSize="14sp"
android:visibility="gone"/>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tvLogText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:layout_marginVertical="10dp"
android:textColor="@color/white"
android:textSize="20sp"
tools:text="log" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginHorizontal="20dp"
android:background="#30FFFFFF" />
</LinearLayout>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,5 +1,5 @@
<resources> <resources>
<string name="app_name">餐盘柜会员版</string> <string name="app_name">餐盘柜</string>
<!-- TODO: Remove or change this placeholder text --> <!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string> <string name="hello_blank_fragment">Hello blank fragment</string>
@@ -1,211 +0,0 @@
# 智能餐盘柜(开柜门版)业务接口对齐文档
> 版本:v1.02026-08-18
> 适用设备:会员版智能餐盘柜(开柜门,applicationId `com.sw.platecabinet.member`
> 用途:供后端对照本设备端实际调用的接口,对齐业务功能与字段
## 1. 通用约定
### 1.1 服务器环境
设备端 BaseUrl 运行时确定,优先级:设备配置接口下发的 `appPackageUrl` > 本地缓存(SP)> 内置默认。
| 环境 | 地址 | 说明 |
| --- | --- | --- |
| 本地 | `http://192.168.10.101:24801` | LOCAL_BASE_URL |
| 测试 | `https://dev.yixiong-tech.com:8081` | TEST_BASE_URL |
| 生产 | `https://platform-api.uat.shuziweidao.com` | PROD_BASE_URL |
### 1.2 请求头(所有请求统一携带)
| Header | 值 | 说明 |
| --- | --- | --- |
| Content-Type | application/json | JSON 请求体 |
| Accept | application/json | — |
| X-Access-Token | 固定 JWT 字符串(当前硬编码) | 设备免登录态 |
| X-DEVICE-CODE | 设备 SN`GlobalData.deviceId` | 设备标识 |
| authorization | 固定 key `57ee87183f2a4fa59683ec9ef41c8f5d` | 网关鉴权 |
### 1.3 统一响应格式
```json
{
"code": "00000", // 成功固定为 "00000",其余视为失败
"msg": "",
"data": { ... } // 业务数据,可为 null / 数组
}
```
## 2. 接口总览
本设备端接口分两套体系(两套设备共用同一后端时请都保留):
| 体系 | 用途 | 状态 |
| --- | --- | --- |
| V2/nutrition/neglect/** | 人脸体系:设备配置、人脸全量/增量同步、人脸采集、取餐上报 | **当前启用** |
| V1/terminal/neglect/** | 开柜门业务:登录、餐盘绑定/解绑、会员搜索、扫描枪查询 | **当前启用** |
| V1 旧人脸接口(faceFeature 系列) | 旧人脸同步 | **已停用**(被 V2 替代,可下线) |
## 3. V2 人脸体系接口(当前启用)
### 3.1 获取设备配置
- **GET** `/nutrition/neglect/pickup/device/config`
- 无参数
- 调用时机:设备启动(DeviceInitActivity),人脸缓存同步之前
响应 `data`DeviceConfigV2):
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| appPackageUrl | String | 业务服务器 BASE URL(下发后全局生效) |
| canteenName | String | 食堂名称 |
| canteenId | String | 食堂 ID |
| arcsoftAppId | String | 虹软 SDK AppID(设备激活用) |
| arcsoftSdkKey | String | 虹软 SDK Key |
| arcsoftActiveKey | String | 虹软 SDK 激活码 |
| clientServerIp | String | MQTT 客户端服务器 IP |
| zhstServerIp | String | 智慧食堂服务器 IPMQTT 端口) |
### 3.2 获取人脸缓存(全量,分页)
- **POST** `/nutrition/neglect/common/face/page`
- 请求体:`{ "pageNum": 1, "pageSize": 100 }`pageSize 固定 100,客户端自动递归翻页直到不足一页)
- 调用时机:设备首次启动(本地标记 isFirstGetFace)时全量拉取,拉取成功后清空本地人脸库重建
响应 `data``UserFaceModelV2` 数组:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| userFaceId | String | 人脸记录唯一 ID(增量删除/判重的关键) |
| userId | String | 用户 ID |
| faceFeature / faceFeatureStr / faceFeatureString | String | 人脸特征 Base64(三字段取第一个非空) |
| faceUpdateTimestamp | Long | 人脸更新时间戳(毫秒) |
| cardNo | String | 会员编号 |
| member | Boolean | 是否会员 |
| faceDeleted | Boolean | 删除标识(增量接口用) |
| personType | String | 人员类型 |
### 3.3 获取人脸增量数据
- **POST** `/nutrition/neglect/common/face/increment`
- 请求体:`{ "pageNum": 1, "pageSize": 100, "timestamp": 1723900000000 }`(timestamp 为上次同步的最大 faceUpdateTimestamp
- 调用时机:登录页每 5 分钟定时任务(人脸库非空时)
- 客户端处理:`faceDeleted=true``userFaceId` 删单条;否则按 `userFaceId` 判重后插入;同步完成后保存最大时间戳(只升不降)
- 响应:同 3.2 的 `UserFaceModelV2` 数组
### 3.4 上传人脸照片(预留,本版本未启用)
- **POST** `/nutrition/neglect/upload`multipart/form-data,字段名 `file`
- 响应 `data`:图片 URLString
### 3.5 新增人脸数据(预留,本版本未启用)
- **POST** `/nutrition/neglect/user/add-by-face`
- 请求体:`{ "url": "<图片URL>", "featureChar": "<特征Base64>" }`
- 响应 `data``UserFaceModelV2`(含服务端生成的 userFaceId / userId
### 3.6 取餐盘时刻上报(预留,本版本未启用)
- **POST** `/nutrition/neglect/pickup/plate-pickup`
- 请求体:`{ "id": <用户id> }`
- 响应 `data`:主单 recordNoString
- 说明:吐盘机 5.0 在识别成功出盘后调用;开柜门版暂未启用,后端可先行保留
## 4. V1 开柜门业务接口(当前启用)
> 基础路径 `/terminal/neglect`,本设备核心业务流。
### 4.1 登录(获取餐盘用户信息)
- **POST** `/terminal/neglect/sideboard/app/getPlateBoxUserInfo`
- 请求体(LoginParam):
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| equipmentId | Int | 设备 ID |
| faceId | String | 会员人脸 ID(人脸识别登录时传) |
| phone | String | 手机号(密码登录时传) |
| password | String | 密码(密码登录时传) |
- 调用时机:人脸识别成功(相似度 ≥ 0.8,3 秒防抖)/ 手机号密码登录
响应 `data`EquipmentUserInfo):
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| id | Long | 绑定记录主键(解绑时使用) |
| equipmentId | String | 设备 ID |
| equipmentCode | String | 设备编号 |
| equipmentBoxCode | String | 柜门/格子编号(**非空即执行开柜门**,转 Int 后下发串口) |
| faceId | String | 会员人脸 ID(非 null 表示已绑定餐盘) |
| plateNumber | String | 餐盘编号 |
| equipmentName | String | 设备名称 |
| orderNo | String | 订单号 |
| updateTime | String | 更新时间 |
| name / phone / faceUrl | String | 会员基础信息 |
| mealTime / mealTimeInterval | String | 就餐时段 |
| openTime / openTimeInterval | String | 开柜时段 |
| eatCount | Int | 就餐次数 |
| cardBalance | Double | 卡余额(≤0 弹余额不足提示并拦截) |
### 4.2 查询设备下的绑定列表
- **GET** `/terminal/neglect/sideboard/app/getYxMemberRefPlateByEquipmentCode`(设备编号走 `X-DEVICE-CODE` 头)
- 调用时机:管理员登录且未绑定格子时,取可绑定的空格子
- 响应 `data`EquipmentUserInfo 数组
### 4.3 餐盘绑定
- **POST** `/terminal/neglect/sideboard/app/plateBinding`
- 请求体(BindParam):
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| equipmentId | String | 设备 ID |
| equipmentCode | String | 设备编号 |
| equipmentBoxCode | String | 格子编号 |
| memberId | String | 会员 ID |
| faceId | String | 会员人脸 ID |
| plateNumber | String | 餐盘编号 |
- 响应 `data`EquipmentUserInfo
### 4.4 餐盘解绑
- **POST** `/terminal/neglect/sideboard/app/plateUnbind`form-urlencoded
- 参数:`id`Long,绑定记录主键)
- 响应 `data`:无
### 4.5 会员模糊搜索
- **POST** `/terminal/neglect/common/app/getUserInfoByNameOrPhone`
- 请求体:`{ "pageNum": 0, "pageSize": 0, "name": "张", "phone": "138..." }`
- 响应 `data`Member 数组):`id``faceId``name``phone`
- 调用时机:管理界面按姓名/手机号搜索会员
### 4.6 按餐盘号查询(扫描枪)
- **GET** `/terminal/neglect/sideboard/app/getMemberRefPlateByPlateNumber?plateNumber=xxx`
- 响应 `data`EquipmentUserInfo
- 调用时机:扫描枪扫餐盘码
## 5. 已停用的 V1 旧接口(可下线)
以下接口在本次人脸体系切换(V2)后**设备端已无调用**,后端对齐时可安排下线:
| 接口 | 路径 |
| --- | --- |
| 旧人脸缓存(V1 | POST `/terminal/neglect/common/app/faceFeature/list` |
| 旧人脸增量(V1 | POST `/terminal/neglect/common/app/faceFeature/increment/list` |
| 旧设备配置(V1 | GET `/terminal/neglect/common/app/getYxEquipmentByEquipmentCode` |
## 6. 设备端业务主流程(供对照)
1. 启动 → 拉取设备配置(V2 3.1,注入虹软激活参数、BaseUrl)
2. 首次启动全量拉取人脸(V2 3.2,分页递归重建本地库)
3. 进入人脸识别登录页,每 5 分钟增量同步(V2 3.3)
4. 人脸识别成功(阈值 0.8)→ 登录接口(V1 4.1)查会员与绑定信息
5. 已绑格子(equipmentBoxCode 非空)→ 串口开柜门 → 跳转结果页
6. 余额不足(cardBalance ≤ 0)→ 弹窗拦截
7. 管理员路径:搜索会员(4.5)/ 扫描枪(4.6)→ 绑定(4.3)/ 解绑(4.4)格子
+1 -1
View File
@@ -13,7 +13,7 @@ android {
consumerProguardFiles("consumer-rules.pro") consumerProguardFiles("consumer-rules.pro")
ndk { ndk {
abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a")) abiFilters.addAll(listOf("armeabi-v7a"/*, "arm64-v8a"*/))
} }
} }
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -87,10 +87,10 @@ public class ConfigUtil {
/** /**
* 默认相机分辨率 * 默认相机分辨率
*/ */
// private static final String DEFAULT_PREVIEW_SIZE = "1280x720"; private static final String DEFAULT_PREVIEW_SIZE = "1280x720";
private static final String DEFAULT_PREVIEW_SIZE = "1024x768";
// private static final String DEFAULT_PREVIEW_SIZE = "400x640"; // private static final String DEFAULT_PREVIEW_SIZE = "400x640";
/** /**
* 获取String类型的preference * 获取String类型的preference
* *
@@ -32,7 +32,7 @@ public class FaceApi {
*/ */
public void updateFaceData(int index, List<FaceEntity> list) { public void updateFaceData(int index, List<FaceEntity> list) {
Log.d(TAG, "updateFaceData: index = " + index + ", listSize = " + list.size()); Log.d(TAG, "updateFaceData: index = " + index + ", listSize = " + list.size());
FaceDao faceDao = getFaceDao(); FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao();
//if (index == 1) { //if (index == 1) {
// faceDao.deleteAll(); // faceDao.deleteAll();
// faceDao.resetId(); // faceDao.resetId();
@@ -41,7 +41,7 @@ public class FaceApi {
} }
public void clearFaceData() { public void clearFaceData() {
FaceDao faceDao = getFaceDao(); FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao();
faceDao.deleteAll(); faceDao.deleteAll();
faceDao.resetId(); faceDao.resetId();
} }
@@ -88,51 +88,29 @@ public class FaceApi {
} }
public void deleteByUserName(String userName) { public void deleteByUserName(String userName) {
getFaceDao().deleteFaceById(userName); FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao();
} faceDao.deleteFaceById(userName);
/**
* 按 userFaceId 删除单条人脸记录
*/
public void deleteByUserFaceId(String userFaceId) {
getFaceDao().deleteFaceByUserFaceId(userFaceId);
}
/**
* 按 userFaceId 查询单条人脸记录
*/
public FaceEntity queryByUserFaceId(String userFaceId) {
if (userFaceId == null || userFaceId.isEmpty()) {
return null;
}
return getFaceDao().queryByUserFaceId(userFaceId);
} }
public Long insert(FaceEntity entity) { public Long insert(FaceEntity entity) {
if (entity == null) { if (entity == null) {
return 0L; return 0L;
} }
return getFaceDao().insert(entity); FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao();
return faceDao.insert(entity);
} }
public FaceEntity queryByUserName(String userName) { public FaceEntity queryByUserName(String userName) {
if (TextUtils.isEmpty(userName)) { if (TextUtils.isEmpty(userName)) {
return null; return null;
} }
return getFaceDao().queryByUserName(userName); FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao();
return faceDao.queryByUserName(userName);
} }
public int queryFaceCount() { public int queryFaceCount() {
return getFaceDao().getFaceCount(); FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao();
} return faceDao.getFaceCount();
public List<FaceEntity> queryAllByUserName(String userName) {
return getFaceDao().queryAllByUserName(userName);
}
public FaceDao getFaceDao() {
return FaceDatabase.getInstance(App.getContext()).faceDao();
} }
} }
@@ -39,6 +39,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -103,10 +104,6 @@ public class FaceHelper implements FaceListener {
* 活体检测引擎 * 活体检测引擎
*/ */
private FaceEngine flEngine; private FaceEngine flEngine;
/**
* 口罩检测引擎(5.0 独立引擎)
*/
private FaceEngine maskEngine;
private Camera.Size previewSize; private Camera.Size previewSize;
@@ -151,6 +148,11 @@ public class FaceHelper implements FaceListener {
*/ */
private boolean onlyDetectLiveness; private boolean onlyDetectLiveness;
/**
* 是否需要更新faceData
*/
private boolean needUpdateFaceData;
/** /**
* 识别的配置项 * 识别的配置项
*/ */
@@ -180,9 +182,9 @@ public class FaceHelper implements FaceListener {
} }
private FaceHelper(Builder builder) { private FaceHelper(Builder builder) {
needUpdateFaceData = builder.needUpdateFaceData;
onlyDetectLiveness = builder.onlyDetectLiveness; onlyDetectLiveness = builder.onlyDetectLiveness;
ftEngine = builder.ftEngine; ftEngine = builder.ftEngine;
maskEngine = builder.maskEngine;
trackedFaceCount = builder.trackedFaceCount; trackedFaceCount = builder.trackedFaceCount;
previewSize = builder.previewSize; previewSize = builder.previewSize;
frEngine = builder.frEngine; frEngine = builder.frEngine;
@@ -341,20 +343,12 @@ public class FaceHelper implements FaceListener {
refreshTrackId(faceInfoList); refreshTrackId(faceInfoList);
if (faceInfoList.isEmpty()) { if (faceInfoList.isEmpty()) {
return facePreviewInfoList; return facePreviewInfoList;
} else {
FaceInfo currentFaceInfo = faceInfoList.get(0);
// L.e("currentFaceInfo width=" + currentFaceInfo.getRect().width() +
// "---height" + currentFaceInfo.getRect().height());
if (currentFaceInfo.getRect().width() < 200) {//距离远(人脸小),不识别
clearLeftFace(facePreviewInfoList);
return facePreviewInfoList;
}
} }
if (!onlyDetectLiveness && maskEngine != null) { if (!onlyDetectLiveness) {
code = maskEngine.process(rgbNv21, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, faceInfoList, code = ftEngine.process(rgbNv21, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, faceInfoList,
FaceEngine.ASF_MASK_DETECT); FaceEngine.ASF_MASK_DETECT);
if (code == ErrorInfo.MOK) { if (code == ErrorInfo.MOK) {
code = maskEngine.getMask(maskInfoList); code = ftEngine.getMask(maskInfoList);
if (code != ErrorInfo.MOK) { if (code != ErrorInfo.MOK) {
onFail(new Exception("process getMask failed,code is " + code)); onFail(new Exception("process getMask failed,code is " + code));
return facePreviewInfoList; return facePreviewInfoList;
@@ -878,7 +872,18 @@ public class FaceHelper implements FaceListener {
int fdCode = flEngine.detectFaces(nv21Data, width, height, format, faceInfoList); int fdCode = flEngine.detectFaces(nv21Data, width, height, format, faceInfoList);
boolean isFaceExists = isFaceExists(faceInfoList, faceInfo); boolean isFaceExists = isFaceExists(faceInfoList, faceInfo);
if (fdCode == ErrorInfo.MOK && isFaceExists) { if (fdCode == ErrorInfo.MOK && isFaceExists) {
flCode = flEngine.processIr(nv21Data, width, height, format, Arrays.asList(faceInfo), FaceEngine.ASF_IR_LIVENESS); if (needUpdateFaceData) {
/*
* 若IR人脸框有偏移,则需要对IR的人脸数据进行updateFaceData处理,再将处理后的FaceInfo信息传输给活体检测接口
*/
flCode = flEngine.updateFaceData(nv21Data, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21,
new ArrayList<>(Collections.singletonList(faceInfo)));
if (flCode == ErrorInfo.MOK) {
flCode = flEngine.processIr(nv21Data, width, height, format, Arrays.asList(faceInfo), FaceEngine.ASF_IR_LIVENESS);
}
} else {
flCode = flEngine.processIr(nv21Data, width, height, format, Arrays.asList(faceInfo), FaceEngine.ASF_IR_LIVENESS);
}
} else { } else {
onFail(new Exception("ir detectFaces failed fdCode:" + fdCode + ",isFaceExists:" + isFaceExists)); onFail(new Exception("ir detectFaces failed fdCode:" + fdCode + ",isFaceExists:" + isFaceExists));
} }
@@ -1085,9 +1090,9 @@ public class FaceHelper implements FaceListener {
private FaceEngine ftEngine; private FaceEngine ftEngine;
private FaceEngine frEngine; private FaceEngine frEngine;
private FaceEngine flEngine; private FaceEngine flEngine;
private FaceEngine maskEngine;
private Camera.Size previewSize; private Camera.Size previewSize;
private boolean onlyDetectLiveness; private boolean onlyDetectLiveness;
private boolean needUpdateFaceData;
private RecognizeConfiguration recognizeConfiguration; private RecognizeConfiguration recognizeConfiguration;
private RecognizeCallback recognizeCallback; private RecognizeCallback recognizeCallback;
private IDualCameraFaceInfoTransformer dualCameraFaceInfoTransformer; private IDualCameraFaceInfoTransformer dualCameraFaceInfoTransformer;
@@ -1128,11 +1133,6 @@ public class FaceHelper implements FaceListener {
return this; return this;
} }
public Builder maskEngine(FaceEngine val) {
maskEngine = val;
return this;
}
public Builder previewSize(Camera.Size val) { public Builder previewSize(Camera.Size val) {
previewSize = val; previewSize = val;
return this; return this;
@@ -1158,6 +1158,11 @@ public class FaceHelper implements FaceListener {
return this; return this;
} }
public Builder needUpdateFaceData(boolean val) {
needUpdateFaceData = val;
return this;
}
public FaceHelper build() { public FaceHelper build() {
return new FaceHelper(this); return new FaceHelper(this);
} }
@@ -9,7 +9,7 @@ import androidx.room.RoomDatabase;
import com.sw.plate.utils.arcface.facedb.dao.FaceDao; import com.sw.plate.utils.arcface.facedb.dao.FaceDao;
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity; import com.sw.plate.utils.arcface.facedb.entity.FaceEntity;
@Database(entities = {FaceEntity.class}, version = 2, exportSchema = false) @Database(entities = {FaceEntity.class}, version = 1, exportSchema = false)
public abstract class FaceDatabase extends RoomDatabase { public abstract class FaceDatabase extends RoomDatabase {
public abstract FaceDao faceDao(); public abstract FaceDao faceDao();
@@ -22,10 +22,7 @@ public abstract class FaceDatabase extends RoomDatabase {
faceDatabase = Room.databaseBuilder(context, FaceDatabase.class, faceDatabase = Room.databaseBuilder(context, FaceDatabase.class,
context.getDatabasePath("faceDB.db").getPath() context.getDatabasePath("faceDB.db").getPath()
// context.getExternalFilesDir("database") + File.separator + "faceDB.db" // context.getExternalFilesDir("database") + File.separator + "faceDB.db"
) ).build();
// 5.0 特征模型不兼容,升级时直接删除旧表重建
.fallbackToDestructiveMigration()
.build();
} }
} }
} }

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