Author SHA1 Message Date
mazengfei 57345f8750 feat(environment): 新增环境切换功能及相关优化
- 新增业务环境选择弹窗 EnvironmentSelectDialog,支持本地/测试/生产环境切换
- 实现连点三次屏幕触发环境切换弹窗功能,便于快速切换接口环境
- 切换环境时清空本地人脸库并重置增量同步状态,保证环境切换的完整生效
- 业务全局 BaseUrl 支持从持久化读取,首次无值时默认测试环境
- Retrofit Client 接口 BaseUrl 优化,自动规范尾部斜杠,保证 BaseUrl 合法
- 设备初始化界面增强,增加等待框触摸穿透,优化接口请求失败时的提示信息
- 提升人脸缓存拉取接口分页大小,默认请求 500 条数据
- 修正登录界面布局细节,隐藏密码切换按钮,调整控件顺序及样式细节
2026-09-02 09:59:48 +08:00
mazengfeiandClaude Fable 5 d33f941d12 docs: 补充切环境后 ArcSoft 参数自动适配说明
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 17:29:53 +08:00
mazengfeiandClaude Fable 5 41cfec0583 docs: 新增业务 BaseUrl 切换设计文档
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 17:15:04 +08:00
mazengfei d53ea8f798 feat(api): 新增V2版本接口支持并集成至网络请求客户端
- 新增 ApiServiceV2 接口,支持人脸缓存、增量数据、设备配置等接口
- ApiClient 中新增 apiServiceV2 对象,复用 Retrofit 实例,调整超时时间至60秒
- DeviceInitActivity 改用 netViewModelV2 进行人脸缓存全量拉取及设备配置读取
- BaseActivity 新增对人脸增量同步接口调用,调用成功后刷新本地识别缓存
- BaseActivity 新增清空本地人脸库功能,异步清理数据库并刷新识别缓存
- FaceApi 增加清空本地人脸库接口,重写数据库操作逻辑
- 升级 lib_face 模块 Room 数据库版本,新增字段以支持多标识人脸实体扩展
- FaceEntity 实体扩展会员编号、用户ID、人脸ID、会员标识、更新时间等字段
- FaceDao 新增按人脸ID查询和删除接口,支持多重人脸数据操作
- 优化网络层 OkHttpClient 配置,简化拦截器写法及日志级别判断
- 升级 lib_face 模块支持 arm64-v8a 架构,增强兼容性
- 规范模块间依赖关系,统一版本管理及包路径声明
- 完善 CLAUDE.md 文档,补充项目架构、模块划分与开发流程说明
2026-08-18 17:14:06 +08:00
mazengfei 5b8db65fa2 优化 2026-03-02 16:35:36 +08:00
lvmeng 147fa08e9b 会员版修改包名 2026-03-02 15:23:52 +08:00
120 changed files with 3731 additions and 8475 deletions
Generated
+1 -1
View File
@@ -1 +1 @@
SmartTakePlate
SmartPlateCabinet
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="com.codeverse.userSettings.MarscodeWorkspaceAppSettingsState">
<option name="progress" value="1.0" />
</component>
</project>
-27
View File
@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AppInsightsSettings">
<option name="selectedTabId" value="Firebase Crashlytics" />
<option name="tabSettings">
<map>
<entry key="Firebase Crashlytics">
<value>
<InsightsFilterSettings>
<option name="connection">
<ConnectionSetting>
<option name="appId" value="PLACEHOLDER" />
<option name="mobileSdkAppId" value="" />
<option name="projectId" value="" />
<option name="projectNumber" value="" />
</ConnectionSetting>
</option>
<option name="signal" value="SIGNAL_UNSPECIFIED" />
<option name="timeIntervalDays" value="THIRTY_DAYS" />
<option name="visibilityType" value="ALL" />
</InsightsFilterSettings>
</value>
</entry>
</map>
</option>
</component>
</project>
+8 -3
View File
@@ -2,11 +2,16 @@
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="SmartPlateCabinet.app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-11-06T09:21:30.965681400Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="Default" identifier="serial=?;connection=3c39678a" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
-13
View File
@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>
+1 -1
View File
@@ -6,7 +6,7 @@
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="corretto-17" />
<option name="gradleJvm" value="jbr-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
-6
View File
@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="UsePropertyAccessSyntax" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
</profile>
</component>
+2 -1
View File
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="corretto-17" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
+55
View File
@@ -0,0 +1,55 @@
# 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
@@ -0,0 +1,198 @@
# 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) {
// 开柜失败
}
});
```
+4 -7
View File
@@ -14,11 +14,11 @@ android {
keyPassword = "123456"
}
}
namespace = "com.sw.platecabinet"
namespace = "com.sw.platecabinet.member"
compileSdk = 35
defaultConfig {
applicationId = "com.sw.take.plate"
applicationId = "com.sw.platecabinet.member"
minSdk = 24
targetSdk = 35
versionCode = 1
@@ -27,7 +27,7 @@ android {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ndk {
abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a"))
abiFilters.addAll(listOf("armeabi-v7a"/*, "arm64-v8a"*/))
}
setProperty("archivesBaseName", "zncpg_${versionName}")
}
@@ -59,10 +59,7 @@ dependencies {
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
implementation(project(":lib_face")) {
exclude(group = "com.licheedev", module = "android-serialport")
}
implementation(files("libs/plc_aar_104.aar"))
implementation(project(":lib_face"))
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
Binary file not shown.
-5
View File
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<lint>
<!-- Glide库中的NotificationTarget触发了此检查,但本kiosk应用不使用通知功能 -->
<issue id="NotificationPermission" severity="ignore" />
</lint>
Binary file not shown.
Binary file not shown.
+37
View File
@@ -0,0 +1,37 @@
{
"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
}
+17 -16
View File
@@ -10,21 +10,20 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32"
tools:ignore="ScopedStorage" />
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:name=".MyApp"
android:name="com.sw.platecabinet.MyApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@drawable/logo_launcher"
android:icon="@mipmap/ic_logo512"
android:label="@string/app_name"
android:roundIcon="@drawable/logo_launcher"
android:roundIcon="@mipmap/ic_logo512"
android:supportsRtl="true"
android:theme="@style/Theme.SmartPlateCabinet"
android:networkSecurityConfig="@xml/network_security_config"
@@ -32,26 +31,32 @@
tools:targetApi="31">
<!-- 注册BootReceiver,监听开机完成广播 -->
<receiver
android:name=".receiver.BootReceiver"
android:name="com.sw.platecabinet.receiver.BootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<activity
android:name=".activity.InitActivity"
android:name="com.sw.platecabinet.activity.UnBindDialogActivity"
android:exported="false"
android:theme="@style/DialogActivity" />
<activity
android:name="com.sw.platecabinet.activity.InitActivity"
android:exported="false"
android:launchMode="singleTask">
</activity>
<activity
android:name=".activity.LoginByFaceActivity"
android:name="com.sw.platecabinet.activity.LoginByPwdActivity"
android:exported="false"
android:launchMode="singleTask"/>
<activity
android:name="com.sw.platecabinet.activity.LoginByFaceActivity"
android:exported="true"
android:launchMode="singleTask"/>
<activity
android:name=".activity.DeviceInitActivity"
android:name="com.sw.platecabinet.activity.DeviceInitActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
@@ -61,13 +66,9 @@
</intent-filter>
</activity>
<activity
android:name=".activity.MainActivity"
android:name="com.sw.platecabinet.activity.MainActivity"
android:exported="false">
</activity>
<activity
android:name=".activity.PlcSettingActivity"
android:exported="false" />
<!-- <activity android:name="com.sw.platecabinet.activity.FunActivity" />-->
</application>
</manifest>
@@ -21,6 +21,7 @@ object GlobalData {
* 竖排数量
*/
var arrayVertical: Int = 11
/**
* 排列方式 0 垂直 1 水平
*/
@@ -38,9 +39,9 @@ object GlobalData {
/**
* 具体业务 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"
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
@@ -69,4 +70,23 @@ object GlobalKey {
const val KEY_USER_INFO = "userInfoKey"
const val PARAM_EQUIPMENT_INFO = "equipmentUserInfo"
const val KEY_BASE_URL = "baseUrlKey"
}
}
/**
* 业务环境配置项
* @param name 环境名称(用于弹窗展示)
* @param url 环境 BaseUrl(不带尾斜杠)
*/
data class Environment(
val name: String,
val url: String
)
/**
* 预设环境列表(本地 / 测试 / 生产)
*/
val ENVIRONMENTS: List<Environment> = listOf(
Environment("本地", GlobalData.LOCAL_BASE_URL),
Environment("测试", GlobalData.TEST_BASE_URL),
Environment("生产", GlobalData.PROD_BASE_URL)
)
+5 -26
View File
@@ -1,6 +1,5 @@
package com.sw.platecabinet
import android.annotation.SuppressLint
import android.util.Log
import com.sw.plate.App
import com.sw.plate.utils.AppUtil
@@ -11,13 +10,10 @@ import timber.log.Timber
class MyApp : App() {
companion object {
const val DEBUG: Boolean = true
var instance: MyApp? = null
// var plateList: List<EquipmentUserInfo>? = null
}
override fun onCreate() {
super.onCreate()
instance = this
Timber.plant(Timber.DebugTree())
Timber.d("初始化")
initGlobalData()
@@ -29,35 +25,18 @@ class MyApp : App() {
/**
* 初始化全局数据
*/
@SuppressLint("LogNotTimber")
private fun initGlobalData() {
val deviceId = AppUtil.getUDID(this)
// deviceId = "be154831-3466-3ba2-a2ea-57652c919fed"
var deviceId = AppUtil.getUDID(this)
deviceId = "be154831-3466-3ba2-a2ea-57652c919fed"
Log.d("MyApp", "initialize: deviceId=$deviceId")
GlobalData.deviceId = deviceId
val url = SpTool.baseUrl
if (url.isNotBlank()) {
GlobalData.appBaseUrl = url
} else {
GlobalData.appBaseUrl =
if ("14d2dad5-7d33-3ced-8a59-f14cb1ef2d21" == deviceId)
GlobalData.LOCAL_BASE_URL
else
// GlobalData.PROD_BASE_URL
GlobalData.TEST_BASE_URL
}
// TODO: 临时测试----------------
// GlobalData.appBaseUrl = GlobalData.TEST_BASE_URL
// GlobalData.deviceId = "79814f20-f253-397c-9da7-8430ffa5ef00"
// if ("2987f0c5-5754-33e9-b00a-251db5e2e55f" == deviceId) {
// GlobalData.deviceId = "79814f20-f253-397c-9da7-8430ffa5ef00"
// }
GlobalData.appVersion = AppUtil.getAppVersionCode(this).toString()
GlobalData.globalEquipmentCode = "202501171634"
// 从持久化读取业务 BaseUrl,首次无值兜底测试环境
GlobalData.appBaseUrl = SpTool.getBaseUrl().ifBlank { GlobalData.TEST_BASE_URL }
Timber.d("initGlobalData appBaseUrl=${GlobalData.appBaseUrl}")
// GlobalData.appBaseUrl = "http://192.168.1.201:14801"
}
@@ -1,34 +1,41 @@
package com.sw.platecabinet.activity
import android.app.Dialog
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
import android.view.KeyEvent
import android.view.View
import android.view.WindowInsetsController
import android.view.WindowManager
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import com.sw.inbound.utils.DateTimeUtils
import com.sw.plate.App
import com.sw.plate.utils.Base64
import com.sw.plate.utils.ScanGunKeyEventHelper
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.facedb.FaceDatabase
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import com.sw.platecabinet.R
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.ext.clickWithCoroutines
import com.sw.plate.utils.comn.SerialApi
import com.sw.plate.utils.comn.SerialPortManager
import com.sw.platecabinet.member.R
import com.sw.platecabinet.member.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.dialog.BalanceNotEnoughDialog
import com.sw.platecabinet.ext.setClickListeners
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.utils.IntervalExecutor
import com.sw.platecabinet.utils.PermissionHelper
import com.sw.platecabinet.utils.SpTool
import com.sw.platecabinet.view.CustomLoadingDialog
import com.sw.platecabinet.viewmodel.NetViewModelV2
import com.sw.platecabinet.viewmodel.SettingViewModel
import com.sw.platecabinet.viewmodel.UserViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@@ -44,11 +51,17 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
protected lateinit var context: Context
private var timeJob: Job? = null
private var mDialogWaiting: CustomLoadingDialog? = null
// private val permissionHelpers = mutableMapOf<Int, PermissionHelper>()
// protected var keyEventHelper: ScanGunKeyEventHelper? = null
private val permissionHelpers = mutableMapOf<Int, PermissionHelper>()
protected var keyEventHelper: ScanGunKeyEventHelper? = null
public val recognizeViewModel by viewModels<RecognizeViewModel>()
public val netViewModelV2 by viewModels<NetViewModelV2>()
// 管理员对应的viewmodel
private val settingViewModel by viewModels<SettingViewModel>()
// 用户对应的viewModel
protected val viewModel by viewModels<UserViewModel>()
// V2 网络请求 viewModel(人脸缓存/增量同步/设备配置)
val netViewModelV2 by viewModels<NetViewModelV2>()
private var startTime: Long = 0
@@ -70,83 +83,60 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
updateTime()
registerDataChange()
initialize()
// registerKeyEvent()
registerKeyEvent()
val durationTime = System.currentTimeMillis() - startTime
Timber.d("启动时间:$durationTime")
}
// /**
// * 监听扫描枪扫描事件
// */
// protected fun registerKeyEvent() {
// keyEventHelper =
// ScanGunKeyEventHelper(context, object : ScanGunKeyEventHelper.OnScanSuccessListener {
// override fun onScanSuccess(barcode: String?) {
// Timber.d("onScanSuccess barcode = $barcode")
// if (barcode == null) return
// handleScanKeyInfo(barcode)
// }
// })
// }
/**
* 监听扫描枪扫描事件
*/
protected fun registerKeyEvent() {
keyEventHelper =
ScanGunKeyEventHelper(context, object : ScanGunKeyEventHelper.OnScanSuccessListener {
override fun onScanSuccess(barcode: String?) {
Timber.d("onScanSuccess barcode = $barcode")
if (barcode == null) return
handleScanKeyInfo(barcode)
}
})
}
/**
* 处理扫描枪数据
*/
protected open fun handleScanKeyInfo(scanInfo: String) {
//// settingViewModel.findByPlateNumber(plateNumber = scanInfo)
// var plateNumber = scanInfo
// if (scanInfo.length > 6) {
// plateNumber = scanInfo.take(6)
// }
// Log.d("TAG", "handleScanKeyInfo: " + Gson().toJson(MyApp.plateList))
// if (MyApp.plateList == null) {
// initPlateList()
// }
// //柜子未关联该餐盘,直接找空位置放入盘子
// val item = MyApp.plateList?.firstOrNull { it.plateNumber.isNullOrBlank() }
// if (item == null) {
// //柜子已满
// MainActivity.start(context = context, pageType = PageType.PLATE_CABINET_FULL)
// return
// }
// //找到位置,放入餐盘,清除已有id和时间
// item.plateNumber = plateNumber
// item.faceId = null
// item.updateTime = null
// PlateUtils.open(this, item.equipmentBoxCode, true)
// SpTool.savePlateData(MyApp.plateList!!)
settingViewModel.findByPlateNumber(plateNumber = scanInfo)
}
// override fun dispatchKeyEvent(event: KeyEvent): Boolean {
// if (keyEventHelper != null) {
// if (keyEventHelper!!.isScanGunEvent(event)) {
// keyEventHelper!!.analysisKeyEvent(event)
// return true
// }
// }
// return super.dispatchKeyEvent(event)
// }
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (keyEventHelper != null) {
if (keyEventHelper!!.isScanGunEvent(event)) {
keyEventHelper!!.analysisKeyEvent(event)
return true
}
}
return super.dispatchKeyEvent(event)
}
fun updateTime() {
headerBinding?.let {
// 双击判定窗口放宽至 600ms:默认 300ms 对触摸屏上的手动双击过短,
// 两次点击间隔常在 300~600ms,导致设置页"点很多次才进"
it.tvLeftDate.clickWithCoroutines(
doubleClickInterval = 600,
it.tvLeftDate.setClickListeners(
onDoubleClick = {
onLeftDoubleClick()
}
)
it.tvRightTime.clickWithCoroutines(
doubleClickInterval = 600,
it.tvRightTime.setClickListeners(
onDoubleClick = {
onRightDoubleClick()
}
)
timeJob = lifecycleScope.launch {
val scope = CoroutineScope(Dispatchers.Main)
timeJob = scope.launch {
DateTimeUtils.realTimeChineseDateFlow()
.collect { date ->
it.tvRightTime.text = date
.collect { (date, time) ->
it.tvLeftDate.text = date
it.tvRightTime.text = time
}
}
}
@@ -163,36 +153,36 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
* 右侧时间双击
*/
open fun onRightDoubleClick() {
//MainActivity.start(context, pageType = PageType.SETTING_LIST)
MainActivity.start(context, pageType = PageType.SETTING_LIST)
}
// override fun onRequestPermissionsResult(
// requestCode: Int,
// permissions: Array<out String>,
// grantResults: IntArray
// ) {
// super.onRequestPermissionsResult(requestCode, permissions, grantResults)
//
// val helper = permissionHelpers[requestCode]
// val allGranted = PermissionHelper.handlePermissionResult(
// this,
// requestCode,
// permissions,
// grantResults,
// permissionHelper = helper
// )
//
// if (allGranted) {
// permissionHelpers.remove(requestCode)
// }
// }
//
// /**
// * 注册PermissionHelper以便处理结果
// */
// fun registerPermissionHelper(helper: PermissionHelper) {
// permissionHelpers[helper.requestCode] = helper
// }
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
val helper = permissionHelpers[requestCode]
val allGranted = PermissionHelper.handlePermissionResult(
this,
requestCode,
permissions,
grantResults,
permissionHelper = helper
)
if (allGranted) {
permissionHelpers.remove(requestCode)
}
}
/**
* 注册PermissionHelper以便处理结果
*/
fun registerPermissionHelper(helper: PermissionHelper) {
permissionHelpers[helper.requestCode] = helper
}
override fun onResume() {
super.onResume()
@@ -247,28 +237,22 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
/**
* 显示等待提示框
*/
fun showWaitingDialog(tip: String?) {
runOnUiThread {
hideWaitingDialog()
val view = View.inflate(this, R.layout.dialog_waiting, null)
if (!TextUtils.isEmpty(tip)) (view.findViewById<View?>(R.id.tvTip) as TextView).setText(
tip
)
mDialogWaiting = CustomLoadingDialog(this, view, R.style.MyDialog)
mDialogWaiting!!.show()
mDialogWaiting!!.setCancelable(true)
mDialogWaiting!!.setCanceledOnTouchOutside(true)
}
fun showWaitingDialog(tip: String?): Dialog? {
hideWaitingDialog()
val view = View.inflate(this, R.layout.dialog_waiting, null)
if (!TextUtils.isEmpty(tip)) (view.findViewById<View?>(R.id.tvTip) as TextView).setText(tip)
mDialogWaiting = CustomLoadingDialog(this, view, R.style.MyDialog)
mDialogWaiting!!.show()
mDialogWaiting!!.setCancelable(true)
return mDialogWaiting
}
/**
* 隐藏等待提示框
*/
fun hideWaitingDialog() {
runOnUiThread {
mDialogWaiting?.dismiss()
mDialogWaiting = null
}
mDialogWaiting?.dismiss()
mDialogWaiting = null
}
protected abstract fun inflateViewBinding(): VB
@@ -282,12 +266,125 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
*/
protected open fun registerDataChange() {
Timber.d("registerDataChange")
lifecycleScope.launch {
viewModel.showLoading.collect {
Timber.d("registerDataChange 用户 showLoading = $it")
if (it) {
showWaitingDialog("")
} else {
hideWaitingDialog()
}
}
}
lifecycleScope.launch {
viewModel.currentUserInfo.collect {
if (it == null) return@collect
it.showBalanceNotEnoughDialog = true
handleLoginSuccess(it, false)
}
}
lifecycleScope.launch {
settingViewModel.showLoading.collect {
Timber.d("registerDataChange 管理员 showLoading = $it")
if (it) {
showWaitingDialog("")
} else {
hideWaitingDialog()
}
}
}
lifecycleScope.launch {
settingViewModel.searchUserInfo.collect {
Timber.d("registerDataChange 管理员 currentUserInfo = $it")
if (it == null) return@collect
handleLoginSuccess(it, true)
}
}
// lifecycleScope.launch {
// settingViewModel.equipmentList.collect {
// loadEquipmentList(it)
// }
// }
}
private fun loadEquipmentList(items: List<EquipmentUserInfo>) {
if (items.isEmpty()) return
val unbindList = items.filter { !it.isBound() }
if (unbindList.isEmpty()) {
MainActivity.start(context = context, pageType = PageType.PLATE_CABINET_FULL)
} else {
val firstInfo = unbindList[0]
val currentUserInfo = settingViewModel.searchUserInfo.value
firstInfo.plateNumber = currentUserInfo?.plateNumber ?: ""
MainActivity.start(
context = context,
pageType = PageType.BIND_PLATE,
equipmentUserInfo = firstInfo
)
}
}
/**
* 处理登录成功操作
* @param isAdmin true 管理员 执行绑盘此操作 false 用户,提示错误
*/
open fun handleLoginSuccess(equipmentUserInfo: EquipmentUserInfo, isAdmin: Boolean) {
Timber.d("handleLoginSuccess isAdmin = $isAdmin")
equipmentUserInfo.isIntercept = false
val cardBalance = equipmentUserInfo.cardBalance ?: 0.toDouble()
val balanceIsNotEnough = cardBalance <= 0.toDouble()
if (balanceIsNotEnough && equipmentUserInfo.showBalanceNotEnoughDialog) {
//提示余额不足弹窗
equipmentUserInfo.isIntercept = true
BalanceNotEnoughDialog(this).show()
return
}
viewModel.resetUserInfo()
// if (equipmentUserInfo.isOtherEquipment()) {
// MainActivity.start(
// context,
// pageType = PageType.PLATE_TIP,
// equipmentUserInfo = equipmentUserInfo,
// isAdmin
// )
// return
// }
if (equipmentUserInfo.equipmentBoxCode?.isNotEmpty() == true) {
SerialApi.openPlate(
equipmentUserInfo.equipmentBoxCode!!.toInt(),
object : SerialPortManager.SendCallback {
override fun onSuccess() {
MainActivity.start(
context,
pageType = PageType.PLATE_TIP,
isAdmin = isAdmin
)
}
override fun onFail(e: Exception?) {
ToastUtils.showToast("柜门打开失败")
}
})
} else {
if (isAdmin) {
settingViewModel.getEquipmentList {
loadEquipmentList(it)
}
} else {
Timber.d("无餐盘信息")
MainActivity.start(
context,
pageType = PageType.PLATE_TIP,
equipmentUserInfo = equipmentUserInfo,
isAdmin = isAdmin
)
}
}
}
override fun onDestroy() {
timeJob?.cancel()
// keyEventHelper?.onDestroy()
keyEventHelper?.onDestroy()
super.onDestroy()
}
@@ -297,8 +394,11 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
// private val initialDelay = 5 * 60 * 1000L
// private val dealyMillis = 10 * 60 * 1000L
private val initialDelay = 1 * 1000L
private val dealyMillis = 10 * 1000L
private val initialDelay = 5 * 60 * 1000L
private val dealyMillis = 5 * 60 * 1000L
val recognizeViewModel by viewModels<RecognizeViewModel>()
fun startFaceTask() {
faceTaskJob =
intervalExecutor.startIntervalTaskWithInitialDelay(initialDelay, dealyMillis) {
@@ -306,11 +406,14 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
}
}
private var faceTimestamp = 0L
//private var taskPageNo = 1
/**
* 人脸增量同步(V2,同步逻辑在 NetViewModelV2 内部处理)
*/
private fun getFaceIncrementList(pageNo: Int = 1) {
val timestamp = SpTool.lastFaceTimestamp
val timestamp = SpTool.getLastFaceTimestamp()
if (timestamp == 0L) {
return
}
netViewModelV2.getFaceIncrementList(
pageNo = pageNo,
timestamp = timestamp
@@ -319,9 +422,12 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
}
}
/**
* 清空本地人脸库
*/
fun clearAllFace(block: () -> Unit) {
lifecycleScope.launch(Dispatchers.IO) {
val faceDao = FaceDatabase.getInstance(App.getContext()).faceDao()
val faceDao = FaceDatabase.getInstance(this@BaseActivity).faceDao()
faceDao.deleteAll()
faceDao.resetId()
recognizeViewModel.refreshFaceList()
@@ -329,31 +435,4 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
}
}
private var permissionCallback: ((isGranted: Boolean) -> Unit)? = null
private var activityCallback: ((intent: Intent?) -> Unit)? = null
fun requestPermission(permission: String, callback: (isGranted: Boolean) -> Unit) {
this.permissionCallback = callback
requestPermissionLauncher.launch(permission)
}
// 权限请求回调
val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
permissionCallback?.invoke(isGranted)
}
fun startActivity(intent: Intent, callback: (Intent?) -> Unit) {
this.activityCallback = callback
startActivityLauncher.launch(intent)
}
// activity页面返回的回调
private val startActivityLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
activityCallback?.invoke(it.data)
}
}
@@ -1,21 +1,34 @@
package com.sw.platecabinet.activity
import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.view.MotionEvent
import android.view.WindowManager
import androidx.lifecycle.lifecycleScope
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.facedb.FaceDatabase
import com.sw.platecabinet.Environment
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.databinding.ActivityDeviceInitBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.dialog.EnvironmentSelectDialog
import com.sw.platecabinet.member.databinding.ActivityDeviceInitBinding
import com.sw.platecabinet.member.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.utils.SpTool
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
/**
* 设备初始化界面
*/
class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
// 连点切换环境的计数与复位
private var tapCount = 0
private val tapHandler = Handler(Looper.getMainLooper())
private val resetTapRunnable = Runnable { tapCount = 0 }
override fun inflateViewBinding(): ActivityDeviceInitBinding {
return ActivityDeviceInitBinding.inflate(layoutInflater)
}
@@ -25,31 +38,73 @@ class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
}
override fun initialize() {
showWaitingDialog("加载中……")
lifecycleScope.launch {
withContext(Dispatchers.Default) {
//val count = FaceApi().queryFaceCount()
//if (count == 0) {
if (SpTool.firstGetFace) {
netViewModelV2.getUserFaceCache { status, msg ->
SpTool.firstGetFace = false
getDeviceConfig()
}
} else {
getDeviceConfig()
}
// 等待框设为不拦截触摸,确保连点手势可达
showWaitingDialog("加载中……")?.window?.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
// 首次启动先全量拉取人脸缓存,后续走增量同步
if (SpTool.getFirstGetFace()) {
netViewModelV2.getUserFaceCache { status, msg ->
SpTool.setFirstGetFace(false)
getDeviceConfig()
}
} else {
getDeviceConfig()
}
}
/**
* 连续点击 3 次(500ms 内)触发环境切换弹窗
*/
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
if (ev.action == MotionEvent.ACTION_DOWN) {
tapCount++
tapHandler.removeCallbacks(resetTapRunnable)
tapHandler.postDelayed(resetTapRunnable, 500)
if (tapCount >= 3) {
tapCount = 0
tapHandler.removeCallbacks(resetTapRunnable)
showEnvironmentDialog()
return true
}
}
return super.dispatchTouchEvent(ev)
}
/**
* 弹出环境选择弹窗
*/
private fun showEnvironmentDialog() {
EnvironmentSelectDialog(this) { env ->
switchEnvironment(env)
}.show()
}
/**
* 切换环境:持久化 + 清空本地人脸库 + 重置增量同步状态
*/
private fun switchEnvironment(env: Environment) {
SpTool.setBaseUrl(env.url)
lifecycleScope.launch(Dispatchers.IO) {
val faceDao = FaceDatabase.getInstance(this@DeviceInitActivity).faceDao()
faceDao.deleteAll()
faceDao.resetId()
SpTool.setLastFaceTimestamp(0L)
SpTool.setFirstGetFace(true)
withContext(Dispatchers.Main) {
ToastUtils.showToast("已切换到${env.name},重启后生效")
}
}
}
/**
* 获取设备配置(V2),并初始化虹软 SDK 激活参数
*/
private fun getDeviceConfig() {
netViewModelV2.getDeviceConfig(onSuccess = { deviceConfig ->
runOnUiThread {
if (deviceConfig == null) {
hideWaitingDialog()
ToastUtils.showToast("获取设备配置数据失败")
hideWaitingDialog()
goLoginActivity()
ToastUtils.showToast("获取设备配置失败,请连点屏幕3次切换环境")
// goLoginActivity()
return@runOnUiThread
}
GlobalData.appId = deviceConfig.arcsoftAppId ?: ""
@@ -62,7 +117,10 @@ class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
}
}, onFailure = { errMsg ->
runOnUiThread {
ToastUtils.showToast(errMsg ?: "接口异常")
hideWaitingDialog()
Timber.e("getDeviceConfig onFailure: $errMsg")
ToastUtils.showToast("服务连接失败,请连点屏幕3次切换环境")
// goLoginActivity()
}
})
}
@@ -1,152 +0,0 @@
//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,11 +1,10 @@
package com.sw.platecabinet.activity
import android.util.Log
import androidx.lifecycle.lifecycleScope
import com.sw.plate.utils.arcface.FaceApi
import com.sw.plate.utils.arcface.faceserver.FaceServer
import com.sw.platecabinet.databinding.ActivityInitBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.member.databinding.ActivityInitBinding
import com.sw.platecabinet.member.databinding.ItemTitleTimeBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -22,8 +21,7 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
}
override fun initialize() {
Log.d("InitActivity", "collectFace,initialize: ")
binding.takeButton.setOnClickListener {
binding.main.setOnClickListener {
finish()
}
// binding.takeButton.setOnClickListener {
@@ -41,8 +39,4 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
finish()
}
override fun onRightDoubleClick() {
PlcSettingActivity.start(this)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,62 @@
package com.sw.platecabinet.activity
import android.content.Intent
import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.member.databinding.ActivityLoginByPwdBinding
import com.sw.platecabinet.member.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.dialog.BalanceNotEnoughDialog
import com.sw.platecabinet.model.request.LoginParam
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.utils.KeyboardUtils
/**
* 密码登录
*/
class LoginByPwdActivity : BaseActivity<ActivityLoginByPwdBinding>() {
override fun inflateViewBinding(): ActivityLoginByPwdBinding {
return ActivityLoginByPwdBinding.inflate(layoutInflater)
}
override fun inflateTitleBinding(): ItemTitleTimeBinding {
return binding.includeHeader
}
override fun initialize() {
binding.btnLogin.setOnClickListener {
val phone = binding.etPhone.text.toString()
val pwd = binding.etPwd.text.toString()
if (phone.isEmpty() || pwd.isEmpty()) {
ToastUtils.showToast("手机或校验码不能为空")
return@setOnClickListener
}
val loginParam = LoginParam(
//equipmentId = equipmentId,
phone = phone,
password = pwd
)
viewModel.loginWithPwd(loginParam)
KeyboardUtils.hideKeyboard(this)
}
binding.tvFaceRec.setOnClickListener {
val intent = Intent(this, LoginByFaceActivity::class.java)
startActivity(intent)
}
}
override fun onResume() {
super.onResume()
viewModel.resetUserInfo()
}
override fun handleLoginSuccess(equipmentUserInfo: EquipmentUserInfo, isAdmin: Boolean) {
super.handleLoginSuccess(equipmentUserInfo, isAdmin)
// val cardBalance = equipmentUserInfo.cardBalance?:0.toDouble()
// val balanceIsNotEnough = cardBalance <= 0.toDouble()
if (equipmentUserInfo.isIntercept) {
return
}
LoginByFaceActivity.goInitActivity()
finish()
}
}
@@ -2,11 +2,15 @@ package com.sw.platecabinet.activity
import android.content.Context
import android.content.Intent
import com.sw.platecabinet.R
import com.sw.platecabinet.databinding.ActivityMainBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.member.R
import com.sw.platecabinet.member.databinding.ActivityMainBinding
import com.sw.platecabinet.member.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.fragment.BindPlateFragment
import com.sw.platecabinet.fragment.PlateCabinetFullFragment
import com.sw.platecabinet.fragment.PlateOpenFragment
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
typealias Callback = (String) -> Unit
@@ -30,49 +34,36 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
// 静态启动方法
companion object {
private const val PARAM_PAGE_TYPE = "pageType"
// private const val PARAM_EQUIPMENT_INFO = "equipmentUserInfo"
// private const val PARAM_IS_ADMIN = "isAdmin"
// fun start(
// context: Context,
// pageType: PageType,
// equipmentUserInfo: EquipmentUserInfo? = null,
// isAdmin: Boolean = false,
// block:()->Unit={}
// ) {
// val intent = Intent(context, MainActivity::class.java)
// intent.putExtra(PARAM_PAGE_TYPE, pageType.name)
// intent.putExtra(PARAM_EQUIPMENT_INFO, equipmentUserInfo)
// intent.putExtra(PARAM_IS_ADMIN, isAdmin)
// context.startActivity(intent)
// }
fun start(context: Context, pageType: PageType, isScanCode: Boolean = false) {
private const val PARAM_EQUIPMENT_INFO = "equipmentUserInfo"
private const val PARAM_IS_ADMIN = "isAdmin"
fun start(
context: Context,
pageType: PageType,
equipmentUserInfo: EquipmentUserInfo? = null,
isAdmin: Boolean = false
) {
val intent = Intent(context, MainActivity::class.java)
intent.putExtra(PARAM_PAGE_TYPE, pageType.name)
intent.putExtra(PlateOpenFragment.IS_SCAN_CODE, isScanCode)
intent.putExtra(PARAM_EQUIPMENT_INFO, equipmentUserInfo)
intent.putExtra(PARAM_IS_ADMIN, isAdmin)
context.startActivity(intent)
}
}
override fun initialize() {
val param = intent.getStringExtra(PARAM_PAGE_TYPE)
// val isAdmin = intent.getBooleanExtra(PARAM_IS_ADMIN, false)
val isScanCode = intent.getBooleanExtra(PlateOpenFragment.IS_SCAN_CODE, false)
// val equipmentUserInfo = intent.getParcelableExtra<EquipmentUserInfo>(PARAM_EQUIPMENT_INFO)
val isAdmin = intent.getBooleanExtra(PARAM_IS_ADMIN, false)
val equipmentUserInfo = intent.getParcelableExtra<EquipmentUserInfo>(PARAM_EQUIPMENT_INFO)
pageType = param?.let { PageType.valueOf(param) } ?: PageType.SETTING_LIST
fragmentHelper = FragmentHelper(supportFragmentManager, R.id.fragment_container)
val page = when (pageType) {
// PageType.SETTING_LIST -> SettingListFragment()
// PageType.BIND_PLATE -> BindPlateFragment.newInstance(equipmentUserInfo)
// PageType.PLATE_TIP -> PlateOpenFragment.newInstance(equipmentUserInfo, isAdmin)
PageType.PLATE_TIP -> PlateOpenFragment.newInstance(isScanCode)
// PageType.UNBIND_PLATE -> UnBindPlateFragment.newInstance(equipmentUserInfo)
PageType.SETTING_LIST -> SettingListFragment()
PageType.BIND_PLATE -> BindPlateFragment.newInstance(equipmentUserInfo)
PageType.PLATE_TIP -> PlateOpenFragment.newInstance(equipmentUserInfo, isAdmin)
PageType.UNBIND_PLATE -> UnBindPlateFragment.newInstance(equipmentUserInfo)
PageType.PLATE_CABINET_FULL -> PlateCabinetFullFragment()
else -> null
}
page?.let {
fragmentHelper.addFragment(it)
}
fragmentHelper.addFragment(page)
}
override fun onLeftDoubleClick() {
@@ -82,9 +73,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
}
override fun onRightDoubleClick() {
if (pageType == PageType.SETTING_LIST) {
// startActivity(Intent(this, FunActivity::class.java))
}
}
override fun handleScanKeyInfo(scanInfo: String) {
@@ -100,10 +89,9 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
override fun registerDataChange() {
super.registerDataChange()
}
}
/**
* 界面类型
*/
@@ -1,388 +0,0 @@
package com.sw.platecabinet.activity
import android.content.Context
import android.content.Intent
import android.view.inputmethod.InputMethodManager
import androidx.lifecycle.lifecycleScope
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.facedb.FaceDatabase
import com.sw.plate.utils.arcface.faceserver.FaceServer
import com.sw.platecabinet.databinding.ActivityPlcSettingBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.dialog.EnvSwitchDialog
import com.sw.platecabinet.ext.clickWithDebounce
import com.sw.platecabinet.utils.mego.PlcCallback
import com.sw.platecabinet.utils.mego.PlcHelper
import com.sw.platecabinet.utils.mego.PlcStatus
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* PLC 控制设置界面
* 通过右侧时间双击(onRightDoubleClick)打开,提供全套 PLC 控制操作入口
*/
class PlcSettingActivity : BaseActivity<ActivityPlcSettingBinding>() {
companion object {
/** Intent Extra 键:是否显示 tips 界面 */
private const val EXTRA_SHOW_TIPS = "extra_show_tips"
/**
* 启动 PlcSettingActivity
* @param showTips 是否显示 tips 界面(暂无餐盘提示)
*/
fun start(context: Context, showTips: Boolean = false) {
val intent = Intent(context, PlcSettingActivity::class.java)
intent.putExtra(EXTRA_SHOW_TIPS, showTips)
context.startActivity(intent)
}
}
override fun inflateViewBinding(): ActivityPlcSettingBinding {
return ActivityPlcSettingBinding.inflate(layoutInflater)
}
override fun inflateTitleBinding(): ItemTitleTimeBinding {
return binding.includeHeader
}
override fun initialize() {
// 根据传入参数控制 tips 界面显示
val showTips = intent.getBooleanExtra(EXTRA_SHOW_TIPS, false)
binding.layoutTips.visibility =
if (showTips) android.view.View.VISIBLE else android.view.View.GONE
initConnectButtons()
initOutPlateButton()
initPlatformButtons()
initPlatformResetButton()
initAlarmButton()
initParamButtons()
initStatusButton()
initFaceCount()
binding.btnSwitchEnv.setOnClickListener {
EnvSwitchDialog(context = this, onEnvChanged = {
showWaitingDialog("重置人脸数据中……")
lifecycleScope.launch {
withContext(Dispatchers.IO) {
FaceServer.getInstance().clearAllFaces()
}
}
netViewModelV2.getUserFaceCache(pageNo = 1, callback = { state, errMsg ->
hideWaitingDialog()
if (state) {
recognizeViewModel.refreshFaceList()
ToastUtils.showToast("人脸数据已更新")
} else {
ToastUtils.showToast(errMsg)
}
})
}).show()
}
binding.tvTips.clickWithDebounce {
binding.layoutTips.visibility = android.view.View.GONE
}
}
// ==================== 连接控制 ====================
/**
* 初始化连接控制区按钮:打开串口、打开自动模式
*/
private fun initConnectButtons() {
binding.btnOpenSerialPort.clickWithDebounce {
showWaitingDialog("正在打开串口……")
PlcHelper.getInstance(this).openSerialPort(object : PlcCallback<String> {
override fun onSuccess(result: String) {
hideWaitingDialog()
ToastUtils.showToast("打开串口成功:$result")
}
override fun onError(message: String) {
hideWaitingDialog()
ToastUtils.showToast("打开串口失败:$message")
}
})
}
binding.btnOpenAutoMode.clickWithDebounce {
showWaitingDialog("正在打开自动模式……")
PlcHelper.getInstance(this).openAutoMode(object : PlcCallback<String> {
override fun onSuccess(result: String) {
hideWaitingDialog()
ToastUtils.showToast("打开自动模式成功:$result")
}
override fun onError(message: String) {
hideWaitingDialog()
ToastUtils.showToast("打开自动模式失败:$message")
}
})
}
}
// ==================== 出盘操作 ====================
/**
* 初始化出盘操作按钮
*/
private fun initOutPlateButton() {
binding.btnOutPlate.clickWithDebounce {
showWaitingDialog("正在出盘……")
PlcHelper.getInstance(this).outPlate(object : PlcCallback<String> {
override fun onSuccess(result: String) {
hideWaitingDialog()
ToastUtils.showToast("出盘成功:$result")
}
override fun onError(message: String) {
hideWaitingDialog()
ToastUtils.showToast("出盘失败:$message")
}
})
}
}
// ==================== 平台控制 ====================
/**
* 初始化平台控制区按钮:平台下降、平台上升、返回营业
*/
private fun initPlatformButtons() {
binding.btnPlatformDown.clickWithDebounce {
showWaitingDialog("正在执行平台下降……")
PlcHelper.getInstance(this).platformDown(object : PlcCallback<String> {
override fun onSuccess(result: String) {
hideWaitingDialog()
ToastUtils.showToast("平台下降成功:$result")
}
override fun onError(message: String) {
hideWaitingDialog()
ToastUtils.showToast("平台下降失败:$message")
}
})
}
binding.btnPlatformUp.clickWithDebounce {
showWaitingDialog("正在执行平台上升……")
PlcHelper.getInstance(this).platformUp(object : PlcCallback<String> {
override fun onSuccess(result: String) {
hideWaitingDialog()
ToastUtils.showToast("平台上升成功:$result")
}
override fun onError(message: String) {
hideWaitingDialog()
ToastUtils.showToast("平台上升失败:$message")
}
})
}
binding.btnBackToBusiness.clickWithDebounce {
showWaitingDialog("正在返回营业状态……")
PlcHelper.getInstance(this).backToBusiness(object : PlcCallback<String> {
override fun onSuccess(result: String) {
hideWaitingDialog()
ToastUtils.showToast("返回营业成功:$result")
finish()
}
override fun onError(message: String) {
hideWaitingDialog()
ToastUtils.showToast("返回营业失败:$message")
}
})
}
}
// ==================== 故障处理 ====================
/**
* 初始化平台复位按钮:先执行平台上升,成功后自动执行返回营业
*/
private fun initPlatformResetButton() {
binding.btnPlatformReset.clickWithDebounce {
showWaitingDialog("正在复位平台……")
PlcHelper.getInstance(this).platformUp(object : PlcCallback<String> {
override fun onSuccess(result: String) {
// 平台上升完成,继续执行返回营业
PlcHelper.getInstance(this@PlcSettingActivity)
.backToBusiness(object : PlcCallback<String> {
override fun onSuccess(result: String) {
hideWaitingDialog()
ToastUtils.showToast("平台复位成功")
finish()
}
override fun onError(message: String) {
hideWaitingDialog()
ToastUtils.showToast("返回营业失败:$message")
}
})
}
override fun onError(message: String) {
hideWaitingDialog()
ToastUtils.showToast("平台上升失败:$message")
}
})
}
}
/**
* 初始化故障复位按钮
*/
private fun initAlarmButton() {
binding.btnAlarmReset.clickWithDebounce {
showWaitingDialog("正在故障复位……")
PlcHelper.getInstance(this).alarmReset(object : PlcCallback<String> {
override fun onSuccess(result: String) {
hideWaitingDialog()
ToastUtils.showToast("故障复位成功:$result")
}
override fun onError(message: String) {
hideWaitingDialog()
ToastUtils.showToast("故障复位失败:$message")
}
})
}
}
// ==================== 参数设置 ====================
/**
* 初始化参数设置按钮
* 读取参数类型和参数值输入框内容,调用 setParams
*/
private fun initParamButtons() {
binding.btnSetParams.clickWithDebounce {
val paramType = binding.etParamType.text.toString().trim()
val paramValueStr = binding.etParamValue.text.toString().trim()
if (paramType.isEmpty()) {
ToastUtils.showToast("请输入参数类型")
return@clickWithDebounce
}
if (paramValueStr.isEmpty()) {
ToastUtils.showToast("请输入参数值")
return@clickWithDebounce
}
val paramValue = paramValueStr.toIntOrNull()
if (paramValue == null) {
ToastUtils.showToast("参数值须为整数")
return@clickWithDebounce
}
// 隐藏键盘
hideKeyboard()
showWaitingDialog("正在设置参数……")
PlcHelper.getInstance(this).setParams(
paramType,
paramValue,
object : PlcCallback<String> {
override fun onSuccess(result: String) {
hideWaitingDialog()
ToastUtils.showToast("参数设置成功:$result")
}
override fun onError(message: String) {
hideWaitingDialog()
ToastUtils.showToast("参数设置失败:$message")
}
}
)
}
}
// ==================== 状态显示 ====================
/**
* 初始化状态刷新按钮,获取 PLC 状态并格式化展示
*/
private fun initStatusButton() {
binding.btnRefreshStatus.clickWithDebounce {
showWaitingDialog("正在获取状态……")
PlcHelper.getInstance(this).getStatus(object : PlcCallback<PlcStatus> {
override fun onSuccess(result: PlcStatus) {
hideWaitingDialog()
binding.tvStatusInfo.text = buildStatusText(result)
}
override fun onError(message: String) {
hideWaitingDialog()
binding.tvStatusInfo.text = "获取状态失败:$message"
ToastUtils.showToast("获取状态失败:$message")
}
})
}
}
/**
* 将 PlcStatus 格式化为多行可读文本
*/
private fun buildStatusText(status: PlcStatus): String {
return """
空盘状态:${if (status.isEmptyPlate) "是(无餐盘)" else "否(有餐盘)"}
平台下位:${if (status.isPlateformDown) "是" else "否"}
平台上位:${if (status.isPlateformUp) "是" else "否"}
取盘红外:${if (status.isTakePlateInfrared) "已触发" else "未触发"}
报警代码:${if (status.alarmCode == 0) "正常(0" else "异常(${status.alarmCode}"}
""".trimIndent()
}
// ==================== 人脸库统计 ====================
/**
* 初始化人脸库统计区:页面加载时查询并显示人脸总数,绑定刷新按钮
*/
private fun initFaceCount() {
refreshFaceCount()
binding.btnRefreshFaceCount.clickWithDebounce {
refreshFaceCount()
}
}
/**
* 从数据库查询人脸总数并更新 UI
*/
private fun refreshFaceCount() {
lifecycleScope.launch {
val count = withContext(Dispatchers.IO) {
FaceDatabase.getInstance(this@PlcSettingActivity).faceDao().faceCount
}
binding.tvFaceCount.text = "人脸总数:${count}"
}
}
// ==================== 生命周期 ====================
/**
* 左侧日期双击 → 关闭设置界面
*/
override fun onLeftDoubleClick() {
finish()
}
/**
* 右侧时间双击 → 不做操作,防止循环打开
*/
override fun onRightDoubleClick() {
// 空实现,避免在设置页再次触发跳转
}
// ==================== 工具方法 ====================
/**
* 隐藏软键盘
*/
private fun hideKeyboard() {
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(currentFocus?.windowToken, 0)
}
}
@@ -0,0 +1,97 @@
package com.sw.platecabinet.activity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.sw.platecabinet.member.R
import kotlin.math.ceil
class TestActivity : AppCompatActivity() {
private lateinit var parentRecyclerView: RecyclerView
private lateinit var pageAdapter: PageAdapter
private val itemsPerPage = 22 // 每行2个,每列11个,共22个
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test)
parentRecyclerView = findViewById(R.id.parentRecyclerView)
parentRecyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.HORIZONTAL, false)
// 假设有100个数据项
val allItems = (1..30).map { "Item $it" }
val pages = allItems.chunked(itemsPerPage)
pageAdapter = PageAdapter(pages)
parentRecyclerView.adapter = pageAdapter
}
private fun convertToColumnFirst(original: List<String>, cols: Int): List<String> {
val rows = ceil(original.size.toDouble() / cols).toInt()
return List(original.size) { pos ->
val row = pos % rows
val col = pos / rows
val originalPos = col + row * cols
original.getOrElse(originalPos) { "" }
}
}
inner class PageAdapter(private val pages: List<List<String>>) :
RecyclerView.Adapter<PageAdapter.PageViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.page_item_layout, parent, false)
return PageViewHolder(view)
}
override fun onBindViewHolder(holder: PageViewHolder, position: Int) {
holder.bind(pages[position])
}
override fun getItemCount(): Int = pages.size
inner class PageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val childRecyclerView: RecyclerView =
itemView.findViewById(R.id.childRecyclerView)
fun bind(items: List<String>) {
childRecyclerView.layoutManager = GridLayoutManager(itemView.context, 2)
childRecyclerView.adapter = ChildAdapter(items)
}
}
}
inner class ChildAdapter(private val items: List<String>) :
RecyclerView.Adapter<ChildAdapter.ItemViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_bind_view, parent, false)
return ItemViewHolder(view)
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
holder.bind(position, items[position])
}
override fun getItemCount(): Int = items.size
inner class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val llRoot: LinearLayout = itemView.findViewById(R.id.ll_root)
private val tvNum: TextView = itemView.findViewById(R.id.tv_num)
fun bind(position: Int, item: String) {
tvNum.text = item
llRoot.setBackgroundResource(if (position % 3 == 0) R.drawable.grid_item_bind else R.drawable.grid_item_unbind)
}
}
}
}
@@ -0,0 +1,57 @@
package com.sw.platecabinet.activity
import android.graphics.Color
import android.view.Gravity
import android.view.ViewGroup
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.WindowCompat
import com.sw.platecabinet.GlobalKey
import com.sw.platecabinet.member.databinding.ActivityUnbindDialogBinding
import com.sw.platecabinet.member.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.ext.dp
import com.sw.platecabinet.model.response.EquipmentUserInfo
/**
* 解绑确认弹窗界面
*/
class UnBindDialogActivity : BaseActivity<ActivityUnbindDialogBinding>() {
private var equipmentUserInfo: EquipmentUserInfo? = null
override fun inflateViewBinding(): ActivityUnbindDialogBinding {
return ActivityUnbindDialogBinding.inflate(layoutInflater)
}
override fun inflateTitleBinding(): ItemTitleTimeBinding? {
return null
}
override fun initialize() {
window?.apply {
WindowCompat.setDecorFitsSystemWindows(this, false)
setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
attributes = attributes.apply {
y = 314.dp
width = (context.resources.displayMetrics.widthPixels * 0.8).toInt()
height = ViewGroup.LayoutParams.MATCH_PARENT
gravity = Gravity.CENTER
}
}
equipmentUserInfo =
intent.getParcelableExtra<EquipmentUserInfo>(GlobalKey.PARAM_EQUIPMENT_INFO)
if (equipmentUserInfo == null) return
binding.tvTipInfo.text = "即将解除【${equipmentUserInfo!!.name}】的餐盘"
binding.tvCancel.setOnClickListener {
finish()
}
binding.tvConfirm.setOnClickListener {
setResult(RESULT_OK)
finish()
}
}
override fun finish() {
super.finish()
overridePendingTransition(0, 0) // 确保退出也无动画
}
}
@@ -6,7 +6,7 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.sw.platecabinet.R
import com.sw.platecabinet.member.R
/**
* 通用分页适配器(支持ViewBinding
@@ -1,34 +0,0 @@
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()
}
}
}
@@ -1,36 +1,36 @@
//package com.sw.platecabinet.dialog
//
//import android.app.Dialog
//import android.graphics.drawable.ColorDrawable
//import android.os.Bundle
//import android.view.Window
//import androidx.fragment.app.FragmentActivity
//import com.sw.platecabinet.databinding.DialogBalanceNotEnoughBinding
//import com.sw.platecabinet.ext.dp
//
//open class BalanceNotEnoughDialog(
// private var activity: FragmentActivity
//) : Dialog(activity) {
//
// private lateinit var binding: DialogBalanceNotEnoughBinding
//
// override fun onCreate(savedInstanceState: Bundle?) {
// super.onCreate(savedInstanceState)
// requestWindowFeature(Window.FEATURE_NO_TITLE)
// binding = DialogBalanceNotEnoughBinding.inflate(layoutInflater)
// setContentView(binding.root)
// window?.run {
// attributes = attributes.apply {
// width = 480.dp
// }
// setBackgroundDrawable(ColorDrawable())
// setCancelable(false)
// }
// addListener()
// }
//
// private fun addListener() {
// binding.tvConfirm.setOnClickListener { dismiss() }
// }
//
//}
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.member.databinding.DialogBalanceNotEnoughBinding
import com.sw.platecabinet.ext.dp
open class BalanceNotEnoughDialog(
private var activity: FragmentActivity
) : Dialog(activity) {
private lateinit var binding: DialogBalanceNotEnoughBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
binding = DialogBalanceNotEnoughBinding.inflate(layoutInflater)
setContentView(binding.root)
window?.run {
attributes = attributes.apply {
width = 480.dp
}
setBackgroundDrawable(ColorDrawable())
setCancelable(false)
}
addListener()
}
private fun addListener() {
binding.tvConfirm.setOnClickListener { dismiss() }
}
}
@@ -1,38 +0,0 @@
package com.sw.platecabinet.dialog
import android.app.Dialog
import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.Gravity
import android.view.View
import com.sw.platecabinet.R
import com.sw.platecabinet.ext.dp
import com.sw.platecabinet.ext.hideKeyboard
abstract class BaseDialog(
context: Context,
var defWidth: Int = 400.dp,
var defHeight: Int = 300.dp
) : Dialog(context, R.style.MyDialog) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val rootView = getRootView()
setContentView(rootView)
setCancelable(false)
setCanceledOnTouchOutside(false)
window?.apply {
setLayout(defWidth, defHeight)
setBackgroundDrawable(ColorDrawable())
setGravity(Gravity.CENTER)
}
rootView.setOnClickListener { it.hideKeyboard() }
initView()
}
abstract fun getRootView(): View
abstract fun initView()
}
@@ -1,48 +0,0 @@
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()
}
}
}
@@ -1,69 +0,0 @@
package com.sw.platecabinet.dialog
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.databinding.DialogEnvSwitchBinding
import com.sw.platecabinet.ext.dp
import com.sw.platecabinet.utils.SpTool
/**
* 环境切换弹窗
*
* 提供 TEST / UAT / PROD 三套环境的切换功能,
* 初始化时自动根据 [GlobalData.appBaseUrl] 选中当前环境,
* 确认后更新 [GlobalData.appBaseUrl] 并通过 [onEnvChanged] 回调通知外部。
*
* @param context 上下文
* @param onEnvChanged 确认切换后的回调,参数为新的 baseUrl
*/
class EnvSwitchDialog(
context: Context,
private val onEnvChanged: (newBaseUrl: String) -> Unit = {}
) : BaseDialog(
context,
defWidth = 600.dp,
defHeight = WindowManager.LayoutParams.WRAP_CONTENT
) {
private lateinit var binding: DialogEnvSwitchBinding
override fun getRootView(): View {
binding = DialogEnvSwitchBinding.inflate(LayoutInflater.from(context))
return binding.root
}
override fun initView() {
// 根据当前 appBaseUrl 预选对应 RadioButton,不匹配则不选
when (GlobalData.appBaseUrl) {
GlobalData.LOCAL_BASE_URL -> binding.rbTest.isChecked = true
GlobalData.TEST_BASE_URL -> binding.rbUat.isChecked = true
GlobalData.PROD_BASE_URL -> binding.rbProd.isChecked = true
}
// 取消按钮
binding.btnCancel.setOnClickListener { dismiss() }
// 确认按钮
binding.btnConfirm.setOnClickListener {
// 未选中任何选项时提示用户
if (binding.rgEnv.checkedRadioButtonId == -1) {
ToastUtils.showToast("请选择环境")
return@setOnClickListener
}
val newUrl = when (binding.rgEnv.checkedRadioButtonId) {
binding.rbTest.id -> GlobalData.LOCAL_BASE_URL
binding.rbUat.id -> GlobalData.TEST_BASE_URL
binding.rbProd.id -> GlobalData.PROD_BASE_URL
else -> return@setOnClickListener
}
GlobalData.appBaseUrl = newUrl
SpTool.baseUrl = newUrl
onEnvChanged(newUrl)
dismiss()
}
}
}
@@ -0,0 +1,81 @@
package com.sw.platecabinet.dialog
import android.app.Dialog
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.Gravity
import android.view.View
import android.view.Window
import android.widget.LinearLayout
import android.widget.RadioButton
import androidx.fragment.app.FragmentActivity
import com.sw.platecabinet.ENVIRONMENTS
import com.sw.platecabinet.Environment
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.ext.dp
import com.sw.platecabinet.member.databinding.DialogEnvironmentSelectBinding
/**
* 业务环境选择弹窗
*/
class EnvironmentSelectDialog(
private val activity: FragmentActivity,
private val onConfirm: (Environment) -> Unit
) : Dialog(activity) {
private lateinit var binding: DialogEnvironmentSelectBinding
private var selected: Environment? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
binding = DialogEnvironmentSelectBinding.inflate(layoutInflater)
setContentView(binding.root)
window?.run {
attributes = attributes.apply { width = 480.dp }
setBackgroundDrawable(ColorDrawable())
setCancelable(true)
}
renderEnvironments()
addListener()
}
/**
* 渲染环境选项,高亮当前环境
*/
private fun renderEnvironments() {
val current = GlobalData.appBaseUrl
ENVIRONMENTS.forEach { env ->
val rb = RadioButton(activity).apply {
id = View.generateViewId()
text = env.name
gravity = Gravity.CENTER_VERTICAL
textSize = 18f
setTextColor(Color.WHITE)
buttonTintList = ColorStateList.valueOf(Color.WHITE)
isChecked = env.url == current
setOnClickListener { selected = env }
}
binding.rgEnvironments.addView(
rb,
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply { topMargin = 8.dp }
)
}
selected = ENVIRONMENTS.firstOrNull { it.url == current }
}
/**
* 确认按钮:回调所选环境并关闭弹窗
*/
private fun addListener() {
binding.tvConfirm.setOnClickListener {
selected?.let(onConfirm)
dismiss()
}
}
}
@@ -1,38 +1,38 @@
//package com.sw.platecabinet.dialog
//
//import android.app.Dialog
//import android.graphics.drawable.ColorDrawable
//import android.os.Bundle
//import android.view.Window
//import androidx.fragment.app.FragmentActivity
//import com.sw.platecabinet.databinding.DialogUserBindRemindBinding
//import com.sw.platecabinet.ext.dp
//
//open class UserBindRemindDialog(
// private var activity: FragmentActivity,
// var content:String
//) : Dialog(activity) {
//
// private lateinit var binding: DialogUserBindRemindBinding
//
// override fun onCreate(savedInstanceState: Bundle?) {
// super.onCreate(savedInstanceState)
// requestWindowFeature(Window.FEATURE_NO_TITLE)
// binding = DialogUserBindRemindBinding.inflate(layoutInflater)
// setContentView(binding.root)
// window?.run {
// attributes = attributes.apply {
// width = 480.dp
// }
// setBackgroundDrawable(ColorDrawable())
// setCancelable(false)
// }
// binding.tvBindContent.text = content
// addListener()
// }
//
// private fun addListener() {
// binding.tvConfirm.setOnClickListener { dismiss() }
// }
//
//}
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.member.databinding.DialogUserBindRemindBinding
import com.sw.platecabinet.ext.dp
open class UserBindRemindDialog(
private var activity: FragmentActivity,
var content:String
) : Dialog(activity) {
private lateinit var binding: DialogUserBindRemindBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
binding = DialogUserBindRemindBinding.inflate(layoutInflater)
setContentView(binding.root)
window?.run {
attributes = attributes.apply {
width = 480.dp
}
setBackgroundDrawable(ColorDrawable())
setCancelable(false)
}
binding.tvBindContent.text = content
addListener()
}
private fun addListener() {
binding.tvConfirm.setOnClickListener { dismiss() }
}
}
@@ -1,10 +1,8 @@
package com.sw.platecabinet.ext
import android.content.Context
import android.content.res.Resources
import android.util.TypedValue
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.annotation.Dimension
/**
@@ -65,12 +63,4 @@ fun View.invisible() {
fun View.gone() {
visibility = View.GONE
}
/**
* 隐藏软键盘
*/
fun View.hideKeyboard() {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(windowToken, 0)
}
@@ -4,11 +4,6 @@ import android.os.Handler
import android.os.Looper
import android.view.View
import android.widget.TextView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/**
* 设置 TextView 的双击和单击事件监听器
@@ -35,70 +30,4 @@ fun TextView.setClickListeners(
clickCount = 0 // 重置计数
}, doubleClickInterval)
}
}
fun View.clickWithCoroutines(
doubleClickInterval: Long = 300,
onDoubleClick: (View) -> Unit = {},
onSingleClick: (View) -> Unit = {}
) {
var pendingItem: View? = null
var lastClickTime: Long = 0
var clickJob: Job? = null
setOnClickListener { view ->
val currentTime = System.currentTimeMillis()
// 如果是同一控件且在双击间隔内,则判定为双击
if (pendingItem === view && currentTime - lastClickTime < doubleClickInterval) {
// 取消之前的单击任务
clickJob?.cancel()
onDoubleClick(view)
pendingItem = null
} else {
// 取消之前的单击任务
clickJob?.cancel()
// 启动新的单击任务
clickJob = CoroutineScope(Dispatchers.Main).launch {
delay(doubleClickInterval)
onSingleClick(view)
}
pendingItem = view
lastClickTime = currentTime
}
}
}
//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 // 恢复可点击
}
}
}
}
@@ -8,16 +8,21 @@ import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import com.sw.platecabinet.R
import com.sw.platecabinet.member.R
import com.sw.platecabinet.view.CustomLoadingDialog
import com.sw.platecabinet.viewmodel.SettingViewModel
import kotlinx.coroutines.launch
abstract class BaseFragment<VB : ViewBinding>(
private val bindingInflater: (inflater: LayoutInflater, parent: ViewGroup?, attachToParent: Boolean) -> VB
) : Fragment() {
protected var _binding: VB? = null
private var _binding: VB? = null
protected val binding get() = _binding!!
private var mDialogWaiting: CustomLoadingDialog? = null
protected val viewModel by viewModels<SettingViewModel>()
override fun onCreateView(
inflater: LayoutInflater,
@@ -34,7 +39,15 @@ abstract class BaseFragment<VB : ViewBinding>(
* 注册数据变化监听
*/
open fun registerDataChange() {
lifecycleScope.launch {
viewModel.showLoading.collect {
if (it) {
showWaitingDialog("")
} else {
hideWaitingDialog()
}
}
}
}
abstract fun initialize()
@@ -1,317 +1,317 @@
//package com.sw.platecabinet.fragment
//
//import android.annotation.SuppressLint
//import android.os.Bundle
//import android.text.Editable
//import android.text.TextUtils
//import android.text.TextWatcher
//import android.view.inputmethod.EditorInfo
//import androidx.fragment.app.viewModels
//import androidx.lifecycle.lifecycleScope
//import androidx.recyclerview.widget.GridLayoutManager
//import androidx.recyclerview.widget.LinearLayoutManager
//import androidx.recyclerview.widget.RecyclerView
//import com.sw.plate.utils.ToastUtils
//import com.sw.plate.utils.comn.SerialApi
//import com.sw.plate.utils.comn.SerialPortManager
//import com.sw.platecabinet.R
//import com.sw.platecabinet.activity.MainActivity
//import com.sw.platecabinet.activity.PageType
//import com.sw.platecabinet.adapter.GenericItemAdapter
//import com.sw.platecabinet.adapter.GridSpacingItemDecoration
//import com.sw.platecabinet.adapter.dpToPx
//import com.sw.platecabinet.databinding.FragmentBindPlateBinding
//import com.sw.platecabinet.databinding.ItemSearchUserInfoBinding
//import com.sw.platecabinet.dialog.UserBindRemindDialog
//import com.sw.platecabinet.ext.maskName
//import com.sw.platecabinet.ext.maskPhone
//import com.sw.platecabinet.model.ErrorInfo
//import com.sw.platecabinet.model.request.BindParam
//import com.sw.platecabinet.model.response.EquipmentUserInfo
//import com.sw.platecabinet.model.response.SearchResult
//import com.sw.platecabinet.utils.Debouncer
//import com.sw.platecabinet.utils.KeyboardUtils
//import com.sw.platecabinet.viewmodel.SettingViewModel
//import com.sw.platecabinet.viewmodel.UserViewModel
//import kotlinx.coroutines.launch
//import timber.log.Timber
//
///**
// * 餐盘柜绑定
// */
//class BindPlateFragment : BaseFragment<FragmentBindPlateBinding>(
// FragmentBindPlateBinding::inflate
//) {
// private lateinit var adapter: GenericItemAdapter<SearchResult.Member, ItemSearchUserInfoBinding>
// internal val KEY_ACTION_TYPE = "actionType"
// private var info: EquipmentUserInfo? = null
// private var checkedItem: SearchResult.Member? = null
// private var lastText = ""
// private val debouncer = Debouncer(2000)
//
// private val userViewModel by viewModels<UserViewModel>()
//
// companion object {
// @JvmStatic
// fun newInstance(userInfo: EquipmentUserInfo?) =
// BindPlateFragment().apply {
// arguments = Bundle().apply {
// putParcelable(KEY_ACTION_TYPE, userInfo)
// }
// }
// }
//
// @SuppressLint("SetTextI18n")
// override fun initialize() {
// arguments?.let { args ->
// info = args.getParcelable(KEY_ACTION_TYPE)
// info?.let {
// binding.tvNum.text = "${it.equipmentName}-${it.equipmentBoxCode}"
// if (!TextUtils.isEmpty(it.plateNumber)) {
//// binding.etCode.text = Editable.Factory.getInstance().newEditable(it.plateNumber)
// binding.etCode.apply {
// setText(it.plateNumber)
// setSelection(length())
// }
// }
// }
// }
// initView()
// initListener()
// }
//
// private fun initView() {
// adapter = createAdapter()
// binding.recyclerview.layoutManager = GridLayoutManager(context, 2)
// // 添加间距装饰(12dp)
// binding.recyclerview.addItemDecoration(
// GridSpacingItemDecoration(
// spanCount = 2,
// spacing = requireContext().dpToPx(6),
// includeEdge = false // 包含边缘间距
// )
// )
// binding.recyclerview.adapter = adapter
// binding.recyclerview.addOnScrollListener(object : RecyclerView.OnScrollListener() {
// override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
// super.onScrolled(recyclerView, dx, dy)
// val layoutManager = recyclerView.layoutManager as LinearLayoutManager
// val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
// val totalItems = layoutManager.itemCount
//
// if (!viewModel.isLoading && viewModel.canLoadMore
// && lastVisibleItem >= totalItems - 3
// ) {
// viewModel.getSearchMemberList(
// param = lastText,
// pageNum = viewModel.currentPage
// )
// }
// }
// })
// (activity as MainActivity).registerKeyEventInfo {
//// binding.etCode.text.clear()
//// binding.etCode.postDelayed({
//// binding.etCode.text.clear()
//// binding.etCode.text = Editable.Factory.getInstance().newEditable(it)
// activity?.runOnUiThread {
// if (binding.etUserInfo.isFocused) {
// return@runOnUiThread
// }
// binding.etCode.apply {
// if (it.length > 6) {
// setText(it.substring(0, 6))
// } else {
// setText(it)
// }
// if (length() > 0) {
// setSelection(length())
// }
// }
// binding.etUserInfo.requestFocus()
// }
//// }, 50)
// }
// }
//
// private fun initListener() {
// binding.etCode.addTextChangedListener(object : TextWatcher {
// override fun beforeTextChanged(
// s: CharSequence?,
// start: Int,
// count: Int,
// after: Int
// ) {
// Timber.d("beforeTextChanged s = ${s.toString()}, start = $start, count = $count, after = $after")
// }
//
// override fun onTextChanged(
// s: CharSequence?,
// start: Int,
// before: Int,
// count: Int
// ) {
// Timber.d("onTextChanged s = ${s.toString()}, start = $start, count = $count, before = $before")
// }
//
// override fun afterTextChanged(s: Editable?) {
// Timber.d("afterTextChanged s = ${s.toString()}")
// }
// })
// binding.etUserInfo.setOnEditorActionListener { _, actionId, keyEvent ->
// if (actionId == EditorInfo.IME_ACTION_SEARCH) {
// val currentText = binding.etUserInfo.text.toString()
// val containsNewlines = currentText.contains('\n') || currentText.contains('\r')
// if (containsNewlines) {
// Timber.d("非用户主动搜索")
// true
// }
// debouncer.debounce {
// // 执行搜索操作
// viewModel.getSearchMemberList(param = currentText, pageNum = 1)
// // 隐藏键盘
// KeyboardUtils.hideKeyboard(requireActivity())
// }
// true // 表示已处理该事件
// } else {
// false // 未处理其他动作
// }
// }
// binding.ivSearch.setOnClickListener {
// debouncer.debounce {
// // 执行搜索操作
// viewModel.getSearchMemberList(
// param = binding.etUserInfo.text.toString(),
// pageNum = 1
// )
// // 隐藏键盘
// KeyboardUtils.hideKeyboard(requireActivity())
// }
// }
// binding.llBind.setOnClickListener {
// if (equipmentBoxCode.isNullOrBlank().not()) {
// showBindDialog()
// return@setOnClickListener
// }
// if (info == null || info!!.equipmentCode?.isEmpty() == true || info!!.equipmentBoxCode?.isEmpty() == true) {
// ToastUtils.showToast("传入的设备信息异常")
// return@setOnClickListener
// }
// val plateNumber = binding.etCode.text.toString()
// if (plateNumber.isEmpty()) {
// ToastUtils.showToast("请输入餐盘号")
// return@setOnClickListener
// }
// if (checkedItem == null) {
// ToastUtils.showToast("请选择要绑定的会员")
// return@setOnClickListener
// }
// if (checkedItem!!.id == null) {
// ToastUtils.showToast("选中的会员信息异常")
// return@setOnClickListener
// }
// val bindParam = BindParam(
// equipmentId = info?.equipmentId,
// equipmentCode = info?.equipmentCode,
// equipmentBoxCode = info?.equipmentBoxCode,
// faceId = checkedItem?.faceId,
// plateNumber = plateNumber
// )
// viewModel.bindPlate(bindParam) { pair ->
// updateBindState(pair)
// }
// KeyboardUtils.hideKeyboard(requireActivity())
// }
// binding.tvBack.setOnClickListener { activity?.finish() }
// }
//
// /**
// * 监听数据变化
// */
// override fun registerDataChange() {
// super.registerDataChange()
package com.sw.platecabinet.fragment
import android.annotation.SuppressLint
import android.os.Bundle
import android.text.Editable
import android.text.TextUtils
import android.text.TextWatcher
import android.view.inputmethod.EditorInfo
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.comn.SerialApi
import com.sw.plate.utils.comn.SerialPortManager
import com.sw.platecabinet.member.R
import com.sw.platecabinet.activity.MainActivity
import com.sw.platecabinet.activity.PageType
import com.sw.platecabinet.adapter.GenericItemAdapter
import com.sw.platecabinet.adapter.GridSpacingItemDecoration
import com.sw.platecabinet.adapter.dpToPx
import com.sw.platecabinet.member.databinding.FragmentBindPlateBinding
import com.sw.platecabinet.member.databinding.ItemSearchUserInfoBinding
import com.sw.platecabinet.dialog.UserBindRemindDialog
import com.sw.platecabinet.ext.maskName
import com.sw.platecabinet.ext.maskPhone
import com.sw.platecabinet.model.ErrorInfo
import com.sw.platecabinet.model.request.BindParam
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.SearchResult
import com.sw.platecabinet.utils.Debouncer
import com.sw.platecabinet.utils.KeyboardUtils
import com.sw.platecabinet.viewmodel.SettingViewModel
import com.sw.platecabinet.viewmodel.UserViewModel
import kotlinx.coroutines.launch
import timber.log.Timber
/**
* 餐盘柜绑定
*/
class BindPlateFragment : BaseFragment<FragmentBindPlateBinding>(
FragmentBindPlateBinding::inflate
) {
private lateinit var adapter: GenericItemAdapter<SearchResult.Member, ItemSearchUserInfoBinding>
internal val KEY_ACTION_TYPE = "actionType"
private var info: EquipmentUserInfo? = null
private var checkedItem: SearchResult.Member? = null
private var lastText = ""
private val debouncer = Debouncer(2000)
private val userViewModel by viewModels<UserViewModel>()
companion object {
@JvmStatic
fun newInstance(userInfo: EquipmentUserInfo?) =
BindPlateFragment().apply {
arguments = Bundle().apply {
putParcelable(KEY_ACTION_TYPE, userInfo)
}
}
}
@SuppressLint("SetTextI18n")
override fun initialize() {
arguments?.let { args ->
info = args.getParcelable(KEY_ACTION_TYPE)
info?.let {
binding.tvNum.text = "${it.equipmentName}-${it.equipmentBoxCode}"
if (!TextUtils.isEmpty(it.plateNumber)) {
// binding.etCode.text = Editable.Factory.getInstance().newEditable(it.plateNumber)
binding.etCode.apply {
setText(it.plateNumber)
setSelection(length())
}
}
}
}
initView()
initListener()
}
private fun initView() {
adapter = createAdapter()
binding.recyclerview.layoutManager = GridLayoutManager(context, 2)
// 添加间距装饰(12dp)
binding.recyclerview.addItemDecoration(
GridSpacingItemDecoration(
spanCount = 2,
spacing = requireContext().dpToPx(6),
includeEdge = false // 包含边缘间距
)
)
binding.recyclerview.adapter = adapter
binding.recyclerview.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
val totalItems = layoutManager.itemCount
if (!viewModel.isLoading && viewModel.canLoadMore
&& lastVisibleItem >= totalItems - 3
) {
viewModel.getSearchMemberList(
param = lastText,
pageNum = viewModel.currentPage
)
}
}
})
(activity as MainActivity).registerKeyEventInfo {
// binding.etCode.text.clear()
// binding.etCode.postDelayed({
// binding.etCode.text.clear()
// binding.etCode.text = Editable.Factory.getInstance().newEditable(it)
activity?.runOnUiThread {
if (binding.etUserInfo.isFocused) {
return@runOnUiThread
}
binding.etCode.apply {
if (it.length > 6) {
setText(it.substring(0, 6))
} else {
setText(it)
}
if (length() > 0) {
setSelection(length())
}
}
binding.etUserInfo.requestFocus()
}
// }, 50)
}
}
private fun initListener() {
binding.etCode.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) {
Timber.d("beforeTextChanged s = ${s.toString()}, start = $start, count = $count, after = $after")
}
override fun onTextChanged(
s: CharSequence?,
start: Int,
before: Int,
count: Int
) {
Timber.d("onTextChanged s = ${s.toString()}, start = $start, count = $count, before = $before")
}
override fun afterTextChanged(s: Editable?) {
Timber.d("afterTextChanged s = ${s.toString()}")
}
})
binding.etUserInfo.setOnEditorActionListener { _, actionId, keyEvent ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
val currentText = binding.etUserInfo.text.toString()
val containsNewlines = currentText.contains('\n') || currentText.contains('\r')
if (containsNewlines) {
Timber.d("非用户主动搜索")
true
}
debouncer.debounce {
// 执行搜索操作
viewModel.getSearchMemberList(param = currentText, pageNum = 1)
// 隐藏键盘
KeyboardUtils.hideKeyboard(requireActivity())
}
true // 表示已处理该事件
} else {
false // 未处理其他动作
}
}
binding.ivSearch.setOnClickListener {
debouncer.debounce {
// 执行搜索操作
viewModel.getSearchMemberList(
param = binding.etUserInfo.text.toString(),
pageNum = 1
)
// 隐藏键盘
KeyboardUtils.hideKeyboard(requireActivity())
}
}
binding.llBind.setOnClickListener {
if (equipmentBoxCode.isNullOrBlank().not()) {
showBindDialog()
return@setOnClickListener
}
if (info == null || info!!.equipmentCode?.isEmpty() == true || info!!.equipmentBoxCode?.isEmpty() == true) {
ToastUtils.showToast("传入的设备信息异常")
return@setOnClickListener
}
val plateNumber = binding.etCode.text.toString()
if (plateNumber.isEmpty()) {
ToastUtils.showToast("请输入餐盘号")
return@setOnClickListener
}
if (checkedItem == null) {
ToastUtils.showToast("请选择要绑定的会员")
return@setOnClickListener
}
if (checkedItem!!.id == null) {
ToastUtils.showToast("选中的会员信息异常")
return@setOnClickListener
}
val bindParam = BindParam(
equipmentId = info?.equipmentId,
equipmentCode = info?.equipmentCode,
equipmentBoxCode = info?.equipmentBoxCode,
faceId = checkedItem?.faceId,
plateNumber = plateNumber
)
viewModel.bindPlate(bindParam) { pair ->
updateBindState(pair)
}
KeyboardUtils.hideKeyboard(requireActivity())
}
binding.tvBack.setOnClickListener { activity?.finish() }
}
/**
* 监听数据变化
*/
override fun registerDataChange() {
super.registerDataChange()
lifecycleScope.launch {
viewModel.searchMemberList.collect {
adapter.updateData(it)
}
}
// lifecycleScope.launch {
// viewModel.searchMemberList.collect {
// adapter.updateData(it)
// viewModel.bindStateChange.collect {
// updateBindState(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 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("绑定失败")
}
})
// }
// )
// }
//
// 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)
// }
// }
//}
} else {
ToastUtils.showToast(errorInfo.msg)
}
}
}
@@ -2,7 +2,7 @@ package com.sw.platecabinet.fragment
import android.os.CountDownTimer
import com.sw.platecabinet.Constants
import com.sw.platecabinet.databinding.FragmentPlateCabinetFullBinding
import com.sw.platecabinet.member.databinding.FragmentPlateCabinetFullBinding
import timber.log.Timber
/**
@@ -11,7 +11,6 @@ import timber.log.Timber
class PlateCabinetFullFragment :
BaseFragment<FragmentPlateCabinetFullBinding>(FragmentPlateCabinetFullBinding::inflate) {
private var totalTimeInMillis: Long = Constants.AUTO_CLOSE_TIME * 1000
private var countDownTimer: CountDownTimer? = null
override fun initialize() {
Timber.d("initialize")
@@ -19,22 +18,15 @@ class PlateCabinetFullFragment :
}
fun initCountTime() {
countDownTimer = object : CountDownTimer(totalTimeInMillis, 1000) {
object : CountDownTimer(totalTimeInMillis, 1000) {
override fun onTick(millisUntilFinished: Long) {
_binding?.tvAutoClose?.text = "${(millisUntilFinished / 1000).toInt() + 1}秒后返回主屏"
binding.tvAutoClose.text = "${(millisUntilFinished / 1000).toInt() + 1}秒后返回主屏"
}
override fun onFinish() {
activity?.finish()
}
}
countDownTimer?.start()
}
override fun onDestroyView() {
countDownTimer?.cancel()
countDownTimer = null
super.onDestroyView()
}.start()
}
}
@@ -4,9 +4,10 @@ import android.content.Intent
import android.os.Bundle
import android.os.CountDownTimer
import com.sw.platecabinet.Constants
import com.sw.platecabinet.R
import com.sw.platecabinet.member.R
import com.sw.platecabinet.activity.InitActivity
import com.sw.platecabinet.databinding.FragmentPlateOpenBinding
import com.sw.platecabinet.member.databinding.FragmentPlateOpenBinding
import com.sw.platecabinet.model.response.EquipmentUserInfo
/**
* 餐盘柜提示界面
@@ -14,85 +15,62 @@ import com.sw.platecabinet.databinding.FragmentPlateOpenBinding
class PlateOpenFragment :
BaseFragment<FragmentPlateOpenBinding>(FragmentPlateOpenBinding::inflate) {
private var totalTimeInMillis: Long = Constants.AUTO_CLOSE_TIME * 1000
// internal val KEY_ACTION_TYPE = "actionType"
// internal val PARAM_IS_ADMIN = "isAdmin"
// private var info: EquipmentUserInfo? = null
// private var isAdmin: Boolean = false
internal val KEY_ACTION_TYPE = "actionType"
internal val PARAM_IS_ADMIN = "isAdmin"
private var info: EquipmentUserInfo? = null
private var isAdmin: Boolean = false
private var isScanCode = false
private var countDownTimer: CountDownTimer? = null
companion object {
const val IS_SCAN_CODE = "isScanCode"
// @JvmStatic
// fun newInstance(userInfo: EquipmentUserInfo?, isAdmin: Boolean) =
// PlateOpenFragment().apply {
// arguments = Bundle().apply {
//// putParcelable(KEY_ACTION_TYPE, userInfo)
//// putBoolean(PARAM_IS_ADMIN, isAdmin)
// }
// }
@JvmStatic
fun newInstance(isScanCode: Boolean) =
fun newInstance(userInfo: EquipmentUserInfo?, isAdmin: Boolean) =
PlateOpenFragment().apply {
arguments = Bundle().apply {
putBoolean(IS_SCAN_CODE, isScanCode)
putParcelable(KEY_ACTION_TYPE, userInfo)
putBoolean(PARAM_IS_ADMIN, isAdmin)
}
}
}
override fun initialize() {
arguments?.let {
// info = it.getParcelable(KEY_ACTION_TYPE)
// isAdmin = it.getBoolean(PARAM_IS_ADMIN)
// info?.let {
// if (!it.hasEquipmentBox()) {
// binding.ivType.setImageResource(R.drawable.ic_tip_warn)
// binding.tvTitle.text = "暂无餐盘信息"
// binding.tvSubTitle.text = "请联系管理员"
// binding.tvTitle.setTextColor(requireContext().getColor(R.color.tip_title_fail))
// }
//// else if (it.isOtherEquipment()) {
//// binding.ivType.setImageResource(R.drawable.ic_tip_warn)
//// binding.tvTitle.text = "暂无餐盘信息"
//// binding.tvSubTitle.text = "您的餐盘绑定在${it.equipmentName ?: " "}号柜上"
//// binding.tvTitle.setTextColor(requireContext().getColor(R.color.tip_title_fail))
//// }
// else {
// binding.ivType.setImageResource(R.drawable.ic_tip_success)
// binding.tvTitle.text = "柜门开启"
// binding.tvSubTitle.text = "餐盘取出后请关闭柜门"
// binding.tvTitle.setTextColor(requireContext().getColor(R.color.tip_title_success))
// }
// }
isScanCode = it.getBoolean(IS_SCAN_CODE, false)
binding.ivType.setImageResource(R.drawable.ic_tip_success)
binding.tvTitle.text = "柜门开启"
binding.tvSubTitle.text = if (isScanCode) "餐盘放入后请关闭柜门" else "餐盘取出后请关闭柜门"
binding.tvTitle.setTextColor(requireContext().getColor(R.color.tip_title_success))
info = it.getParcelable(KEY_ACTION_TYPE)
isAdmin = it.getBoolean(PARAM_IS_ADMIN)
info?.let {
if (!it.hasEquipmentBox()) {
binding.ivType.setImageResource(R.drawable.ic_tip_warn)
binding.tvTitle.text = "暂无餐盘信息"
binding.tvSubTitle.text = "请联系管理员"
binding.tvTitle.setTextColor(requireContext().getColor(R.color.tip_title_fail))
} else if (it.isOtherEquipment()) {
binding.ivType.setImageResource(R.drawable.ic_tip_warn)
binding.tvTitle.text = "暂无餐盘信息"
binding.tvSubTitle.text = "您的餐盘绑定在${it.equipmentName ?: " "}号柜上"
binding.tvTitle.setTextColor(requireContext().getColor(R.color.tip_title_fail))
} else {
binding.ivType.setImageResource(R.drawable.ic_tip_success)
binding.tvTitle.text = "柜门开启"
binding.tvSubTitle.text = "餐盘取出后请关闭柜门"
binding.tvTitle.setTextColor(requireContext().getColor(R.color.tip_title_success))
}
}
}
initCountTime()
}
fun initCountTime() {
countDownTimer = object : CountDownTimer(totalTimeInMillis, 1000) {
object : CountDownTimer(totalTimeInMillis, 1000) {
override fun onTick(millisUntilFinished: Long) {
_binding?.tvAutoClose?.text = ((millisUntilFinished / 1000).toInt() + 1).toString()
binding.tvAutoClose.text = ((millisUntilFinished / 1000).toInt() + 1).toString()
}
override fun onFinish() {
val ctx = context ?: return
activity?.finish()
ctx.startActivity(Intent(ctx, InitActivity::class.java))
if (!isAdmin) {
val intent = Intent(context, InitActivity::class.java)
startActivity(intent)
}
}
}
countDownTimer?.start()
}
override fun onDestroyView() {
countDownTimer?.cancel()
countDownTimer = null
super.onDestroyView()
}.start()
}
}
@@ -1,241 +1,237 @@
//package com.sw.platecabinet.fragment
//
//import android.view.View
//import androidx.core.view.isVisible
//import androidx.lifecycle.lifecycleScope
//import androidx.recyclerview.widget.GridLayoutManager
//import androidx.recyclerview.widget.LinearLayoutManager
//import androidx.recyclerview.widget.RecyclerView
//import com.sw.inbound.utils.DateTimeUtils
//import com.sw.plate.utils.ToastUtils
//import com.sw.plate.utils.comn.SerialApi
//import com.sw.plate.utils.comn.SerialPortManager
//import com.sw.platecabinet.GlobalData
//import com.sw.platecabinet.MyApp
//import com.sw.platecabinet.R
//import com.sw.platecabinet.activity.BaseActivity
//import com.sw.platecabinet.activity.MainActivity
//import com.sw.platecabinet.activity.PageType
//import com.sw.platecabinet.adapter.GenericItemAdapter
//import com.sw.platecabinet.adapter.GenericPageAdapter
//import com.sw.platecabinet.adapter.GridSpacingItemDecoration
//import com.sw.platecabinet.adapter.dpToPx
//import com.sw.platecabinet.databinding.FragmentSettingListBinding
//import com.sw.platecabinet.databinding.ItemBindViewBinding
//import com.sw.platecabinet.ext.formatNumber
//import com.sw.platecabinet.ext.maskName
//import com.sw.platecabinet.model.response.EquipmentUserInfo
//import com.sw.platecabinet.utils.ListArrangementUtil
//import com.sw.platecabinet.utils.PlateUtils
//import com.sw.platecabinet.utils.SpTool
//import kotlinx.coroutines.Dispatchers
//import kotlinx.coroutines.launch
//import kotlinx.coroutines.withContext
//import timber.log.Timber
//
///**
// * 餐盘柜绑定的用户列表界面
// */
//class SettingListFragment :
// BaseFragment<FragmentSettingListBinding>(FragmentSettingListBinding::inflate) {
//
// /**
// * 需要横向滚动的adapter
// */
// private var pageAdapter: GenericPageAdapter<EquipmentUserInfo, ItemBindViewBinding>? = null
//
// /**
// * 垂直滚动的adapter
// */
// private var itemAdapter: GenericItemAdapter<EquipmentUserInfo, ItemBindViewBinding>? = null
//
// override fun registerDataChange() {
// super.registerDataChange()
//// lifecycleScope.launch {
//// viewModel.equipmentList.collect { it ->
//// Timber.d("registerDateChange updateAdapter it = $it")
//// updateAdapter(it)
//// }
//// }
// }
//
// override fun initialize() {
// binding.mainRecyclerView.apply {
// if (GlobalData.arrayCross > 2) {
// layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
// pageAdapter = createPageAdapter()
// adapter = pageAdapter
// } else {
// layoutManager = GridLayoutManager(context, 2)
// // 添加间距装饰(12dp)
// addItemDecoration(
// GridSpacingItemDecoration(
// spanCount = 2,
// spacing = requireContext().dpToPx(12),
// includeEdge = true // 包含边缘间距
// )
// )
// itemAdapter = createItemAdapter()
// adapter = itemAdapter
package com.sw.platecabinet.fragment
import android.view.View
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.sw.inbound.utils.DateTimeUtils
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.comn.SerialApi
import com.sw.plate.utils.comn.SerialPortManager
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.member.R
import com.sw.platecabinet.activity.MainActivity
import com.sw.platecabinet.activity.PageType
import com.sw.platecabinet.adapter.GenericItemAdapter
import com.sw.platecabinet.adapter.GenericPageAdapter
import com.sw.platecabinet.adapter.GridSpacingItemDecoration
import com.sw.platecabinet.adapter.dpToPx
import com.sw.platecabinet.member.databinding.FragmentSettingListBinding
import com.sw.platecabinet.member.databinding.ItemBindViewBinding
import com.sw.platecabinet.ext.formatNumber
import com.sw.platecabinet.ext.maskName
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.utils.ListArrangementUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
/**
* 餐盘柜绑定的用户列表界面
*/
class SettingListFragment :
BaseFragment<FragmentSettingListBinding>(FragmentSettingListBinding::inflate) {
/**
* 需要横向滚动的adapter
*/
private var pageAdapter: GenericPageAdapter<EquipmentUserInfo, ItemBindViewBinding>? = null
/**
* 垂直滚动的adapter
*/
private var itemAdapter: GenericItemAdapter<EquipmentUserInfo, ItemBindViewBinding>? = null
override fun registerDataChange() {
super.registerDataChange()
// lifecycleScope.launch {
// viewModel.equipmentList.collect { it ->
// Timber.d("registerDateChange updateAdapter it = $it")
// updateAdapter(it)
// }
// }
//
// }
//
// override fun onResume() {
// super.onResume()
//// viewModel.getEquipmentList{
//// updateAdapter(it)
//// }
// MyApp.plateList?.let {
// updateAdapter(it)
// }
// }
//
//// private fun getPlateData(block: (List<EquipmentUserInfo>) -> Unit) {
//// MyApp.plateList?.let {
//// block(it)
//// }
////// viewModel.getEquipmentList { items ->
////// block(items)
////// }
//// }
//
// private fun createItemAdapter(): GenericItemAdapter<EquipmentUserInfo, ItemBindViewBinding> {
// return GenericItemAdapter(
// items = emptyList(),
// bindingInflater = ItemBindViewBinding::inflate,
// bindCallback = { item, position ->
// setItemInfo(item, position)
// }
// )
// }
//
// private fun createPageAdapter(): GenericPageAdapter<EquipmentUserInfo, ItemBindViewBinding> {
// return GenericPageAdapter(
// pages = emptyList(),
// pageLayoutId = R.layout.page_item_layout,
// itemBindingInflater = ItemBindViewBinding::inflate,
// itemBindCallback = { item, position ->
// setItemInfo(item, position)
// }
// )
// }
//
// private fun ItemBindViewBinding.setItemInfo(
// item: EquipmentUserInfo,
// position: Int
// ) {
//// if (item.isPlaceholder()) {
//// this.llRoot.isVisible = false
//// return
//// }
// this.llRoot.isVisible = true
// this.tvNum.text = formatNumber(item.equipmentBoxCode ?: "")
// if (item.plateNumber.isNullOrBlank().not()) {
//// val date = DateTimeUtils.parseDateTime(item.updateTime)
//// val timeInMillis = date?.time ?: 0L
//// val isOldTime = DateTimeUtils.isMoreThanHoursFromNow(timeInMillis)
//
// this.llRoot.setBackgroundResource(R.drawable.grid_item_bind)
// this.tvNum.setTextColor(resources.getColor(R.color.bind_4E535D))
//// this.tvLastTime.setTextColor(
//// if (isOldTime) resources.getColor(R.color.bind_time_old) else resources.getColor(
//// R.color.bind_585868
//// )
//// )
// this.llOpen.setBackgroundResource(R.drawable.grid_button_bind)
// this.tvOpen.setTextColor(resources.getColor(R.color.bind_FFCC99))
// this.llBindInfo.visibility = View.VISIBLE
// this.tvUnbind.visibility = View.GONE
//
//// this.tvName.text = item.name.maskName()
// this.tvName.text = item.plateNumber
//
//// this.tvLastTime.text = DateTimeUtils.getTimeAgo(date)
// } else {
// this.llRoot.setBackgroundResource(R.drawable.grid_item_unbind)
// this.tvNum.setTextColor(resources.getColor(R.color.bind_FFCC99))
// this.llOpen.setBackgroundResource(R.drawable.grid_button_unbind)
// this.tvOpen.setTextColor(resources.getColor(R.color.unbind_32283C))
// this.llBindInfo.visibility = View.GONE
// this.tvUnbind.visibility = View.VISIBLE
//// this.tvUnbind.text = "未绑定"
// }
//// this.llRoot.setOnClickListener {
////// Timber.d("itemClick llRoot ${item.name}, position = $position")
//// if (item.isBound()) {
//// MainActivity.start(
//// requireContext(),
//// pageType = PageType.UNBIND_PLATE,
//// equipmentUserInfo = item
//// )
//// } else {
//// MainActivity.start(
//// requireContext(),
//// pageType = PageType.BIND_PLATE,
//// equipmentUserInfo = item
//// )
//// }
//// }
// this.llOpen.setOnClickListener {
//// item.faceId = null
// PlateUtils.open(
// fragmentActivity = requireActivity(),
// code = item.equipmentBoxCode,
// isScanCode = false,
// isSetting = true
// )
// }
// }
//
// fun updateAdapter(items: List<EquipmentUserInfo>) {
// val itemsPerPage = 2 * GlobalData.arrayVertical // 每行2个,每列11个,共22个
// if (GlobalData.arrayCross > 2) {
// lifecycleScope.launch(Dispatchers.Default) {
// // 多recyclerview
// var pages = if (items.size > itemsPerPage) {
// if (GlobalData.arrayMode == 0) {
// val pageList = items.chunked(itemsPerPage)
// pageList.mapIndexed { index, it ->
// var itemList: MutableList<EquipmentUserInfo> = it.toMutableList()
// if (index == pageList.size - 1) {
// // 补全剩余item
// itemList = (itemList + List(itemsPerPage - itemList.size) {
// EquipmentUserInfo()
// }) as MutableList<EquipmentUserInfo>
// }
// ListArrangementUtil.convertToColumnFirst(
// itemList,
// GlobalData.arrayVertical
// )
// }
// } else {
// val newItems =
// ListArrangementUtil.horizontalSortPageItem(items, GlobalData.arrayCross)
// newItems.chunked(itemsPerPage)
// }
// } else {
// listOf(
// items
// )
// }
// withContext(Dispatchers.Main) {
// pageAdapter?.updatePages(pages)
// }
// }
// } else {
// // 单 recyclerview
// lifecycleScope.launch(Dispatchers.Default) {
// var pages = items
// if (GlobalData.arrayMode == 0) {
// pages = ListArrangementUtil.verticalSortItem(pages)
// }
// withContext(Dispatchers.Main) {
// itemAdapter?.updateData(pages)
// }
// }
// }
// binding.mainRecyclerView.scrollToPosition(0)
// }
//}
}
override fun initialize() {
binding.mainRecyclerView.apply {
if (GlobalData.arrayCross > 2) {
layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
pageAdapter = createPageAdapter()
adapter = pageAdapter
} else {
layoutManager = GridLayoutManager(context, 2)
// 添加间距装饰(12dp)
addItemDecoration(
GridSpacingItemDecoration(
spanCount = 2,
spacing = requireContext().dpToPx(12),
includeEdge = true // 包含边缘间距
)
)
itemAdapter = createItemAdapter()
adapter = itemAdapter
}
}
}
override fun onResume() {
super.onResume()
viewModel.getEquipmentList{
updateAdapter(it)
}
}
private fun createItemAdapter(): GenericItemAdapter<EquipmentUserInfo, ItemBindViewBinding> {
return GenericItemAdapter(
items = emptyList(),
bindingInflater = ItemBindViewBinding::inflate,
bindCallback = { item, position ->
setItemInfo(item, position)
}
)
}
private fun createPageAdapter(): GenericPageAdapter<EquipmentUserInfo, ItemBindViewBinding> {
return GenericPageAdapter(
pages = emptyList(),
pageLayoutId = R.layout.page_item_layout,
itemBindingInflater = ItemBindViewBinding::inflate,
itemBindCallback = { item, position ->
setItemInfo(item, position)
}
)
}
private fun ItemBindViewBinding.setItemInfo(
item: EquipmentUserInfo,
position: Int
) {
if (item.isPlaceholder() == true) {
this.llRoot.isVisible = false
return
}
this.llRoot.isVisible = true
this.tvNum.text = formatNumber(item.equipmentBoxCode ?: "")
if (item.isBound()) {
val date = DateTimeUtils.parseDateTime(item.updateTime)
val timeInMillis = date?.time ?: 0L
val isOldTime = DateTimeUtils.isMoreThanHoursFromNow(timeInMillis)
this.llRoot.setBackgroundResource(R.drawable.grid_item_bind)
this.tvNum.setTextColor(resources.getColor(R.color.bind_4E535D))
this.tvLastTime.setTextColor(
if (isOldTime) resources.getColor(R.color.bind_time_old) else resources.getColor(
R.color.bind_585868
)
)
this.llOpen.setBackgroundResource(R.drawable.grid_button_bind)
this.tvOpen.setTextColor(resources.getColor(R.color.bind_FFCC99))
this.llBindInfo.visibility = View.VISIBLE
this.tvUnbind.visibility = View.GONE
this.tvName.text = item.name.maskName()
this.tvLastTime.text = DateTimeUtils.getTimeAgo(date)
} else {
this.llRoot.setBackgroundResource(R.drawable.grid_item_unbind)
this.tvNum.setTextColor(resources.getColor(R.color.bind_FFCC99))
this.llOpen.setBackgroundResource(R.drawable.grid_button_unbind)
this.tvOpen.setTextColor(resources.getColor(R.color.unbind_32283C))
this.llBindInfo.visibility = View.GONE
this.tvUnbind.visibility = View.VISIBLE
}
this.llRoot.setOnClickListener {
Timber.d("itemClick llRoot ${item.name}, position = $position")
if (item.isBound()) {
MainActivity.start(
requireContext(),
pageType = PageType.UNBIND_PLATE,
equipmentUserInfo = item
)
} else {
MainActivity.start(
requireContext(),
pageType = PageType.BIND_PLATE,
equipmentUserInfo = item
)
}
}
this.llOpen.setOnClickListener {
Timber.d("itemClick llOpen equipmentBoxCode = ${item.equipmentBoxCode}")
val equipmentBoxCode: Int = item.equipmentBoxCode?.toInt() ?: -1
if (equipmentBoxCode < 0) {
ToastUtils.showToast("餐盘柜编号错误")
return@setOnClickListener
}
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("柜门打开失败")
}
})
}
}
fun updateAdapter(items: List<EquipmentUserInfo>) {
val itemsPerPage = 2 * GlobalData.arrayVertical // 每行2个,每列11个,共22个
if (GlobalData.arrayCross > 2) {
lifecycleScope.launch(Dispatchers.Default) {
// 多recyclerview
var pages = if (items.size > itemsPerPage) {
if (GlobalData.arrayMode == 0) {
val pageList = items.chunked(itemsPerPage)
pageList.mapIndexed { index, it ->
var itemList: MutableList<EquipmentUserInfo> = it.toMutableList()
if (index == pageList.size - 1) {
// 补全剩余item
itemList = (itemList + List(itemsPerPage - itemList.size) {
EquipmentUserInfo()
}) as MutableList<EquipmentUserInfo>
}
ListArrangementUtil.convertToColumnFirst(
itemList,
GlobalData.arrayVertical
)
}
} else {
val newItems =
ListArrangementUtil.horizontalSortPageItem(items, GlobalData.arrayCross)
newItems.chunked(itemsPerPage)
}
} else {
listOf(
items
)
}
withContext(Dispatchers.Main) {
pageAdapter?.updatePages(pages)
}
}
} else {
// 单 recyclerview
lifecycleScope.launch(Dispatchers.Default) {
var pages = items
if (GlobalData.arrayMode == 0) {
pages = ListArrangementUtil.verticalSortItem(pages)
}
withContext(Dispatchers.Main) {
itemAdapter?.updateData(pages)
}
}
}
binding.mainRecyclerView.scrollToPosition(0)
}
}
@@ -1,95 +1,95 @@
//package com.sw.platecabinet.fragment
//
//import android.app.Activity
//import android.content.Intent
//import android.os.Bundle
//import androidx.activity.result.contract.ActivityResultContracts
//import androidx.lifecycle.lifecycleScope
//import com.sw.inbound.utils.DateTimeUtils
//import com.sw.plate.utils.ToastUtils
//import com.sw.platecabinet.GlobalKey
//import com.sw.platecabinet.activity.UnBindDialogActivity
//import com.sw.platecabinet.databinding.FragmentUnbindPlateBinding
//import com.sw.platecabinet.ext.maskName
//import com.sw.platecabinet.ext.maskPhone
//import com.sw.platecabinet.model.ErrorInfo
//import com.sw.platecabinet.model.response.EquipmentUserInfo
//import kotlinx.coroutines.launch
//
///**
// * 餐盘柜绑定
// */
//class UnBindPlateFragment private constructor() : BaseFragment<FragmentUnbindPlateBinding>(
// FragmentUnbindPlateBinding::inflate
//) {
// internal val ARG_PARAM1 = "param1"
// private var info: EquipmentUserInfo? = null
//
// companion object {
// @JvmStatic
// fun newInstance(param1: EquipmentUserInfo?) =
// UnBindPlateFragment().apply {
// arguments = Bundle().apply {
// putParcelable(ARG_PARAM1, param1)
// }
// }
// }
//
// private val dialogLauncher = registerForActivityResult(
// ActivityResultContracts.StartActivityForResult()
// ) { result ->
// if (result.resultCode == Activity.RESULT_OK) {
// viewModel.unbindPlate(info?.id) { pair ->
// updateBindState(pair)
package com.sw.platecabinet.fragment
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.lifecycleScope
import com.sw.inbound.utils.DateTimeUtils
import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.GlobalKey
import com.sw.platecabinet.activity.UnBindDialogActivity
import com.sw.platecabinet.member.databinding.FragmentUnbindPlateBinding
import com.sw.platecabinet.ext.maskName
import com.sw.platecabinet.ext.maskPhone
import com.sw.platecabinet.model.ErrorInfo
import com.sw.platecabinet.model.response.EquipmentUserInfo
import kotlinx.coroutines.launch
/**
* 餐盘柜绑定
*/
class UnBindPlateFragment private constructor() : BaseFragment<FragmentUnbindPlateBinding>(
FragmentUnbindPlateBinding::inflate
) {
internal val ARG_PARAM1 = "param1"
private var info: EquipmentUserInfo? = null
companion object {
@JvmStatic
fun newInstance(param1: EquipmentUserInfo?) =
UnBindPlateFragment().apply {
arguments = Bundle().apply {
putParcelable(ARG_PARAM1, param1)
}
}
}
private val dialogLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
viewModel.unbindPlate(info?.id) { pair ->
updateBindState(pair)
}
}
}
override fun initialize() {
arguments?.let { args ->
info = args.getParcelable(ARG_PARAM1)
info?.let {
binding.tvNum.text = "${it.equipmentName}-${it.equipmentBoxCode}"
binding.tvPlateNumberValue.text = it.plateNumber
binding.tvNameValue.text = it.name.maskName()
binding.tvPhoneValue.text = it.phone.maskPhone()
val timeInfo = DateTimeUtils.parseDateTime(it.updateTime)
val timeAgo = DateTimeUtils.getTimeAgo(timeInfo)
binding.tvUpdateTimeValue.text = timeAgo
}
}
binding.llUnbind.setOnClickListener {
if (info == null) return@setOnClickListener
val intent = Intent(requireContext(), UnBindDialogActivity::class.java).apply {
putExtra(GlobalKey.PARAM_EQUIPMENT_INFO, info)
}
dialogLauncher.launch(intent)
requireActivity().overridePendingTransition(0, 0)
}
binding.tvBack.setOnClickListener {
activity?.finish()
}
}
override fun registerDataChange() {
super.registerDataChange()
// lifecycleScope.launch {
// viewModel.bindStateChange.collect {
// updateBindState(it)
// }
// }
// }
//
// override fun initialize() {
// arguments?.let { args ->
// info = args.getParcelable(ARG_PARAM1)
// info?.let {
// binding.tvNum.text = "${it.equipmentName}-${it.equipmentBoxCode}"
// binding.tvPlateNumberValue.text = it.plateNumber
// binding.tvNameValue.text = it.name.maskName()
// binding.tvPhoneValue.text = it.phone.maskPhone()
// val timeInfo = DateTimeUtils.parseDateTime(it.updateTime)
// val timeAgo = DateTimeUtils.getTimeAgo(timeInfo)
// binding.tvUpdateTimeValue.text = timeAgo
// }
// }
// binding.llUnbind.setOnClickListener {
// if (info == null) return@setOnClickListener
// val intent = Intent(requireContext(), UnBindDialogActivity::class.java).apply {
// putExtra(GlobalKey.PARAM_EQUIPMENT_INFO, info)
// }
// dialogLauncher.launch(intent)
// requireActivity().overridePendingTransition(0, 0)
// }
// binding.tvBack.setOnClickListener {
// activity?.finish()
// }
// }
//
// override fun registerDataChange() {
// super.registerDataChange()
//// lifecycleScope.launch {
//// viewModel.bindStateChange.collect {
//// updateBindState(it)
//// }
//// }
// }
// private fun updateBindState(pair: Pair<Boolean?, ErrorInfo>){
// if (pair.first == null) return // 绑定状态过滤
// val errorInfo = pair.second
// if (errorInfo.isSuccess()) {
// ToastUtils.showToast("解绑成功")
//// activity?.finish()
// // 关闭宿主 Activity
// requireActivity().finish()
// requireActivity().overridePendingTransition(0, 0)
// } else {
// ToastUtils.showToast(errorInfo.msg)
// }
// }
//}
}
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)
}
}
}
@@ -1,19 +1,19 @@
package com.sw.platecabinet.model
import com.google.gson.annotations.SerializedName
import com.sw.platecabinet.model.response.EquipmentUserInfo
/**
* 错误信息 code == 200 成功
*/
//data class ErrorInfo(
// val code: String = "00000",
// val msg: String = "",
// val equipmentUserInfo: EquipmentUserInfo? = null
//) {
// fun isSuccess(): Boolean {
// return code == "00000"
// }
//}
data class ErrorInfo(
val code: String = "00000",
val msg: String = "",
val equipmentUserInfo: EquipmentUserInfo? = null
) {
fun isSuccess(): Boolean {
return code == "00000"
}
}
data class DeviceConfig(
var arcsoftAppId: String? = null,
@@ -21,6 +21,9 @@ data class DeviceConfig(
var arcsoftActiveKey: String? = null
)
/**
* V2 设备配置(人脸 5.0 体系)
*/
data class DeviceConfigV2(
/** 业务服务器 BASE URL */
val appPackageUrl: String? = null,
@@ -38,10 +41,8 @@ data class DeviceConfigV2(
val clientServerIp: String? = null,
/** 智慧食堂服务器 IP(MQTT 端口) */
val zhstServerIp: String? = null
)
data class CodeMsg(
val code: String? = "",
val msg: String? = ""
@@ -1,72 +1,72 @@
//package com.sw.platecabinet.model.response
//
//
//import android.os.Parcelable
//import com.google.gson.annotations.SerializedName
//import kotlinx.parcelize.Parcelize
//
//@Parcelize
//data class EquipmentInfo(
// @SerializedName("appPackageLocalUrl")
// val appPackageLocalUrl: String? = "",
// @SerializedName("appPackageUrl")
// val appPackageUrl: String? = "",
// @SerializedName("arcsoftActiveKey")
// val arcsoftActiveKey: String? = "",
// @SerializedName("arcsoftAppId")
// val arcsoftAppId: String? = "",
// @SerializedName("arcsoftSdkKey")
// val arcsoftSdkKey: String? = "",
// @SerializedName("arrayCross")
// val arrayCross: Int? = 0,
// @SerializedName("arrayMode")
// val arrayMode: String? = "",
// @SerializedName("arrayVertical")
// val arrayVertical: Int? = 0,
// @SerializedName("canteenId")
// val canteenId: String? = "",
// @SerializedName("canteenName")
// val canteenName: String? = "",
// @SerializedName("clientServerIp")
// val clientServerIp: String? = "",
// @SerializedName("createBy")
// val createBy: String? = "",
// @SerializedName("createTime")
// val createTime: String? = "",
// @SerializedName("customerName")
// val customerName: String? = "",
// @SerializedName("equipmentCode")
// val equipmentCode: String? = "",
// @SerializedName("equipmentName")
// val equipmentName: String? = "",
// @SerializedName("equipmentName_dictText")
// val equipmentNameDictText: String? = "",
// @SerializedName("id")
// val id: String? = "",
// @SerializedName("isSync")
// val isSync: Int? = 0,
// @SerializedName("mqName")
// val mqName: String? = "",
// @SerializedName("mqPassword")
// val mqPassword: String? = "",
// @SerializedName("orgCode")
// val orgCode: String? = "",
// @SerializedName("owningTrack")
// val owningTrack: Int? = 0,
// @SerializedName("owningTrackOrder")
// val owningTrackOrder: Int? = 0,
// @SerializedName("screenHtml")
// val screenHtml: String? = "",
// @SerializedName("serviceRequestAddress")
// val serviceRequestAddress: String? = "",
// @SerializedName("status")
// val status: Int? = 0,
// @SerializedName("sysOrgCode")
// val sysOrgCode: String? = "",
// @SerializedName("updateBy")
// val updateBy: String? = "",
// @SerializedName("updateTime")
// val updateTime: String? = "",
// @SerializedName("zhstServerIp")
// val zhstServerIp: String? = ""
//) : Parcelable
package com.sw.platecabinet.model.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
data class EquipmentInfo(
@SerializedName("appPackageLocalUrl")
val appPackageLocalUrl: String? = "",
@SerializedName("appPackageUrl")
val appPackageUrl: String? = "",
@SerializedName("arcsoftActiveKey")
val arcsoftActiveKey: String? = "",
@SerializedName("arcsoftAppId")
val arcsoftAppId: String? = "",
@SerializedName("arcsoftSdkKey")
val arcsoftSdkKey: String? = "",
@SerializedName("arrayCross")
val arrayCross: Int? = 0,
@SerializedName("arrayMode")
val arrayMode: String? = "",
@SerializedName("arrayVertical")
val arrayVertical: Int? = 0,
@SerializedName("canteenId")
val canteenId: String? = "",
@SerializedName("canteenName")
val canteenName: String? = "",
@SerializedName("clientServerIp")
val clientServerIp: String? = "",
@SerializedName("createBy")
val createBy: String? = "",
@SerializedName("createTime")
val createTime: String? = "",
@SerializedName("customerName")
val customerName: String? = "",
@SerializedName("equipmentCode")
val equipmentCode: String? = "",
@SerializedName("equipmentName")
val equipmentName: String? = "",
@SerializedName("equipmentName_dictText")
val equipmentNameDictText: String? = "",
@SerializedName("id")
val id: String? = "",
@SerializedName("isSync")
val isSync: Int? = 0,
@SerializedName("mqName")
val mqName: String? = "",
@SerializedName("mqPassword")
val mqPassword: String? = "",
@SerializedName("orgCode")
val orgCode: String? = "",
@SerializedName("owningTrack")
val owningTrack: Int? = 0,
@SerializedName("owningTrackOrder")
val owningTrackOrder: Int? = 0,
@SerializedName("screenHtml")
val screenHtml: String? = "",
@SerializedName("serviceRequestAddress")
val serviceRequestAddress: String? = "",
@SerializedName("status")
val status: Int? = 0,
@SerializedName("sysOrgCode")
val sysOrgCode: String? = "",
@SerializedName("updateBy")
val updateBy: String? = "",
@SerializedName("updateTime")
val updateTime: String? = "",
@SerializedName("zhstServerIp")
val zhstServerIp: String? = ""
) : Parcelable
@@ -1,90 +1,90 @@
//package com.sw.platecabinet.model.response
//
//import android.os.Parcelable
//import android.text.TextUtils
////import com.google.gson.annotations.SerializedName
//import com.sw.platecabinet.GlobalData
//import kotlinx.parcelize.Parcelize
//
///**
// * 设备绑定用户信息
// */
//@Parcelize
//data class EquipmentUserInfo(
//// /**
//// * 主键
//// */
//// val id: Long? = 0,
//// /**
//// *
//// * 设备Id
//// */
//// val equipmentId: String? = "",
//// /**
//// * 设备编号
//// */
//// var equipmentCode: String? = "",
// /**
// * 设备盒子编号
// */
// var equipmentBoxCode: String? = "",
// /**
// * 会员Id
// */
// var faceId: String? = "",
// /**
// * 餐盘编号
// */
// var plateNumber: String? = "",
//// /**
//// * 设备名称
//// */
//// val equipmentName: String? = "",
//
//// val orderNo: String? = "",
// /**
// *
// * 更新时间
// */
// var updateTime: String? = "",
//
//// val name: String? = "",
//// val phone: String? = "",
//// val faceUrl: String? = "",
//// val mealTime: String? = "",
//// val mealTimeInterval: String? = "",
//// val openTime: String? = "",
//// val openTimeInterval: String? = "",
//// val eatCount: Int? = 0,
////
//// var cardBalance: Double? = 0.toDouble(),
//
//// /**
//// * 用于cardBalance后续拦截判断
//// */
//// var showBalanceNotEnoughDialog: Boolean = false,
//// var isIntercept: Boolean = false
//) : Parcelable {
//
//// /**
//// * 是否已绑定
//// */
//// fun isBound(): Boolean {
//// return faceId.isNullOrBlank().not()
//// }
//
//// /**
//// * 是否是其他设备
//// */
//// fun isOtherEquipment(): Boolean {
//// return equipmentCode != null && equipmentCode != "" && GlobalData.globalEquipmentCode != equipmentCode
//// }
//
//// fun hasEquipmentBox(): Boolean {
//// return !TextUtils.isEmpty(equipmentBoxCode)
//// }
////
//// fun isPlaceholder(): Boolean {
//// return equipmentBoxCode.isNullOrBlank()
//// }
//}
package com.sw.platecabinet.model.response
import android.os.Parcelable
import android.text.TextUtils
//import com.google.gson.annotations.SerializedName
import com.sw.platecabinet.GlobalData
import kotlinx.parcelize.Parcelize
/**
* 设备绑定用户信息
*/
@Parcelize
data class EquipmentUserInfo(
/**
* 主键
*/
val id: Long? = 0,
/**
*
* 设备Id
*/
val equipmentId: String? = "",
/**
* 设备编号
*/
var equipmentCode: String? = "",
/**
* 设备盒子编号
*/
var equipmentBoxCode: String? = "",
/**
* 会员Id
*/
val faceId: String? = "",
/**
* 餐盘编号
*/
var plateNumber: String? = "",
/**
* 设备名称
*/
val equipmentName: String? = "",
val orderNo: String? = "",
/**
*
* 更新时间
*/
val updateTime: String? = "",
val name: String? = "",
val phone: String? = "",
val faceUrl: String? = "",
val mealTime: String? = "",
val mealTimeInterval: String? = "",
val openTime: String? = "",
val openTimeInterval: String? = "",
val eatCount: Int? = 0,
var cardBalance: Double? = 0.toDouble(),
/**
* 用于cardBalance后续拦截判断
*/
var showBalanceNotEnoughDialog: Boolean = false,
var isIntercept: Boolean = false
) : Parcelable {
/**
* 是否已绑定
*/
fun isBound(): Boolean {
return faceId != null
}
/**
* 是否是其他设备
*/
fun isOtherEquipment(): Boolean {
return equipmentCode != null && equipmentCode != "" && GlobalData.globalEquipmentCode != equipmentCode
}
fun hasEquipmentBox(): Boolean {
return !TextUtils.isEmpty(equipmentBoxCode)
}
fun isPlaceholder(): Boolean {
return equipmentBoxCode == null || equipmentBoxCode == ""
}
}
@@ -1,85 +1,85 @@
//package com.sw.platecabinet.model.response
//
//
//import android.os.Parcelable
//import com.google.gson.annotations.SerializedName
//import kotlinx.parcelize.Parcelize
//
///**
// * 搜索会员信息结果
// */
//@Parcelize
//data class SearchResult(
// @SerializedName("endRow")
// val endRow: Int? = 0,
// @SerializedName("hasNextPage")
// val hasNextPage: Boolean? = false,
// @SerializedName("hasPreviousPage")
// val hasPreviousPage: Boolean? = false,
// @SerializedName("isFirstPage")
// val isFirstPage: Boolean? = false,
// @SerializedName("isLastPage")
// val isLastPage: Boolean? = false,
// @SerializedName("list")
// val list: List<Member>? = listOf(),
// @SerializedName("navigateFirstPage")
// val navigateFirstPage: Int? = 0,
// @SerializedName("navigateLastPage")
// val navigateLastPage: Int? = 0,
// @SerializedName("navigatePages")
// val navigatePages: Int? = 0,
// @SerializedName("navigatepageNums")
// val navigatepageNums: List<Int?>? = listOf(),
// @SerializedName("nextPage")
// val nextPage: Int? = 0,
// @SerializedName("pageNum")
// val pageNum: Int? = 0,
// @SerializedName("pageSize")
// val pageSize: Int? = 0,
// @SerializedName("pages")
// val pages: Int? = 0,
// @SerializedName("prePage")
// val prePage: Int? = 0,
// @SerializedName("size")
// val size: Int? = 0,
// @SerializedName("startRow")
// val startRow: Int? = 0,
// @SerializedName("total")
// val total: Int? = 0
//) : Parcelable {
// @Parcelize
// data class Member(
//// @SerializedName("cardBalance")
//// val cardBalance: String? = "",
//// @SerializedName("cardCode")
//// val cardCode: String? = "",
//// @SerializedName("consumptionCount")
//// val consumptionCount: Int? = 0,
//// @SerializedName("consumptionTotal")
//// val consumptionTotal: String? = "",
//// @SerializedName("createTime")
//// val createTime: String? = "",
//// @SerializedName("firstTopUpTime")
//// val firstTopUpTime: String? = "",
//// @SerializedName("id")
//// val id: Int? = 0,
//// @SerializedName("integralBalance")
//// val integralBalance: Int? = 0,
//// @SerializedName("lastConsumptionTime")
//// val lastConsumptionTime: String? = "",
//// @SerializedName("memberFrom")
//// val memberFrom: Int? = 0,
//// @SerializedName("name")
//// val name: String? = "",
//// @SerializedName("phone")
//// val phone: String? = "",
//// @SerializedName("rewardBalance")
//// val rewardBalance: String? = "",
//// @SerializedName("topUpBalance")
//// val topUpBalance: String? = ""
// val id: String? = "",
// val faceId: String? = "",
package com.sw.platecabinet.model.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
/**
* 搜索会员信息结果
*/
@Parcelize
data class SearchResult(
@SerializedName("endRow")
val endRow: Int? = 0,
@SerializedName("hasNextPage")
val hasNextPage: Boolean? = false,
@SerializedName("hasPreviousPage")
val hasPreviousPage: Boolean? = false,
@SerializedName("isFirstPage")
val isFirstPage: Boolean? = false,
@SerializedName("isLastPage")
val isLastPage: Boolean? = false,
@SerializedName("list")
val list: List<Member>? = listOf(),
@SerializedName("navigateFirstPage")
val navigateFirstPage: Int? = 0,
@SerializedName("navigateLastPage")
val navigateLastPage: Int? = 0,
@SerializedName("navigatePages")
val navigatePages: Int? = 0,
@SerializedName("navigatepageNums")
val navigatepageNums: List<Int?>? = listOf(),
@SerializedName("nextPage")
val nextPage: Int? = 0,
@SerializedName("pageNum")
val pageNum: Int? = 0,
@SerializedName("pageSize")
val pageSize: Int? = 0,
@SerializedName("pages")
val pages: Int? = 0,
@SerializedName("prePage")
val prePage: Int? = 0,
@SerializedName("size")
val size: Int? = 0,
@SerializedName("startRow")
val startRow: Int? = 0,
@SerializedName("total")
val total: Int? = 0
) : Parcelable {
@Parcelize
data class Member(
// @SerializedName("cardBalance")
// val cardBalance: String? = "",
// @SerializedName("cardCode")
// val cardCode: String? = "",
// @SerializedName("consumptionCount")
// val consumptionCount: Int? = 0,
// @SerializedName("consumptionTotal")
// val consumptionTotal: String? = "",
// @SerializedName("createTime")
// val createTime: String? = "",
// @SerializedName("firstTopUpTime")
// val firstTopUpTime: String? = "",
// @SerializedName("id")
// val id: Int? = 0,
// @SerializedName("integralBalance")
// val integralBalance: Int? = 0,
// @SerializedName("lastConsumptionTime")
// val lastConsumptionTime: String? = "",
// @SerializedName("memberFrom")
// val memberFrom: Int? = 0,
// @SerializedName("name")
// val name: String? = "",
// @SerializedName("phone")
// val phone: String? = "",
// ) : Parcelable
//}
// @SerializedName("rewardBalance")
// val rewardBalance: String? = "",
// @SerializedName("topUpBalance")
// val topUpBalance: String? = ""
val id: String? = "",
val faceId: String? = "",
val name: String? = "",
val phone: String? = "",
) : Parcelable
}
@@ -29,7 +29,7 @@ object ApiClient {
private val retrofit by lazy {
Retrofit.Builder()
.baseUrl(GlobalData.appBaseUrl)
.baseUrl(resolveBaseUrl())
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
@@ -42,4 +42,13 @@ object ApiClient {
val apiServiceV2: ApiServiceV2 by lazy {
retrofit.create(ApiServiceV2::class.java)
}
/**
* 规范化 Retrofit 要求的 baseUrl:非空且以 / 结尾。
* 所有接口走 @Url 全路径,此值不参与实际拼接,只需合法。
*/
private fun resolveBaseUrl(): String {
val url = GlobalData.appBaseUrl.ifBlank { "http://localhost/" }
return if (url.endsWith("/")) url else "$url/"
}
}
@@ -2,35 +2,179 @@ package com.sw.platecabinet.network.api
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.model.DeviceConfig
import com.sw.platecabinet.model.request.BindParam
import com.sw.platecabinet.model.request.EquipmentParam
import com.sw.platecabinet.model.request.LoginParam
import com.sw.platecabinet.model.request.SearchParam
import com.sw.platecabinet.model.response.ApiResponse
import com.sw.platecabinet.model.response.EquipmentInfo
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.SearchResult
import com.sw.platecabinet.model.response.UserFaceModel
import 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.Header
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.Query
import retrofit2.http.Url
interface ApiService {
// /**
// * device获取token
// */
// @GET("sys/getEquipmentToken")
// suspend fun getDeviceToken(
// @Query("qrcodeId") qrcodeId: String,
// @Query("appVersion") appVersion: String = GlobalData.appVersion
// ): ApiResponse<String>
//
// /**
// *获取配置信息
// */
// @GET("equipment/stEquipment/queryByEquipmentCode")
// suspend fun getDeviceInfo(
// @Query("equipmentCode") equipmentCode: String,
// @Query("appVersion") appVersion: String = GlobalData.appVersion,
// @Header("X-Access-Token") token: String
// ): ApiResponse<EquipmentInfo>
//
// /**
// * 生成token
// */
// @GET//("/shuwei-zhct/scales/generateToken")
// suspend fun generateToken(
// @Url url: String = "${GlobalData.appBaseUrl}/shuwei-zhct/scales/generateToken",
// @Query("deviceId") deviceId: String,
// @Query("appVersion") appVersion: String,
// @Query("equipmentCode") equipmentCode: String
// ): ApiResponse<String>
/**
* 获取人脸缓存数据
* 获取人脸数据
*/
// @GET//("/shuwei-zhct/scales/getUserFaceCache")
// suspend fun getUserFaceCache(
// @Url url: String = "${GlobalData.appBaseUrl}/shuwei-zhct/scales/getUserFaceCache",
// @Query("appVersion") appVersion: String,
// @Query("equipmentCode") equipmentCode: String
// ): ApiResponse<List<UserFaceModel>>
@POST
suspend fun getUserFaceCache(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/faceFeature/list",
@Body param: Map<String, Int>
): ApiResponse<List<UserFaceModel>>
// /**
// * 餐盘用户信息获取
// */
// @POST//("/shuwei-zhct/swEquipmentRelUser/equipmentBoxLogin")
// suspend fun equipmentBoxLogin(
// @Url url: String = "${GlobalData.appBaseUrl}/shuwei-zhct/swEquipmentRelUser/equipmentBoxLogin",
// @Body param: LoginParam
// ): ApiResponse<EquipmentUserInfo>
/**
* 餐盘用户信息获取
*/
@POST//("/shuwei-zhct/swEquipmentRelUser/equipmentBoxLogin")
suspend fun equipmentBoxLogin(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/sideboard/app/getPlateBoxUserInfo",
@Body param: LoginParam
): ApiResponse<EquipmentUserInfo>
/**
* 餐盘用户信息列表查询
*/
// @POST//("/shuwei-zhct/swEquipmentRelUser/list")
// suspend fun getEquipmentList(
// @Url url: String = "${GlobalData.appBaseUrl}/shuwei-zhct/swEquipmentRelUser/list",
// @Body param: EquipmentParam
// ): ApiResponse<List<EquipmentUserInfo>>
@GET
suspend fun getEquipmentList(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/sideboard/app/getYxMemberRefPlateByEquipmentCode",
): ApiResponse<List<EquipmentUserInfo>>
// /**
// * 餐盘用户信息绑定解绑
// */
// @POST//("/shuwei-zhct/swEquipmentRelUser/addOrEdit")
// suspend fun bindEquipment(
// @Url url: String = "${GlobalData.appBaseUrl}/shuwei-zhct/swEquipmentRelUser/addOrEdit",
// @Body param: BindParam
// ): ApiResponse<EquipmentUserInfo?>
/**
* 餐盘绑定
*/
@POST
suspend fun plateBind(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/sideboard/app/plateBinding",
@Body param: BindParam
): ApiResponse<EquipmentUserInfo?>
/**
* 餐盘解绑
*/
@POST
@FormUrlEncoded
suspend fun plateUnbind(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/sideboard/app/plateUnbind",
@Field("id") id: Long?
): ApiResponse<Any?>
/**
* 用户信息模糊搜索
*/
@POST//("/shuwei-user/swclientUserInfoShop/selectList")
suspend fun searchUser(
// @Url url: String = "${GlobalData.appBaseUrl}/shuwei-user/swclientUserInfoShop/selectList",
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/getUserInfoByNameOrPhone",
@Body param: SearchParam
): ApiResponse<List<SearchResult.Member>?>
// /**
// * 通过餐盘号获取信息
// */
// @POST//("/shuwei-zhct/swEquipmentRelUser/findByPlateNumber")
// suspend fun findByPlateNumber(
// @Url url: String = "${GlobalData.appBaseUrl}/shuwei-zhct/swEquipmentRelUser/findByPlateNumber",
// @Body param: BindParam
// ): ApiResponse<EquipmentUserInfo>
/**
* 通过餐盘号获取信息
*/
@GET
suspend fun findByPlateNumber(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/sideboard/app/getMemberRefPlateByPlateNumber",
@Query("plateNumber") plateNumber: String
): ApiResponse<EquipmentUserInfo>
/**
* 获取人脸增量数据
*
* @param timeDate 时间戳(毫秒级)
*/
// @GET//("/shuwei-zhct/scales/getHeartbeatInterface")
// suspend fun getHeartbeatInterface(
// @Url url: String = "${GlobalData.appBaseUrl}/shuwei-zhct/scales/getHeartbeatInterface",
// @Query("timeDate") timeDate: Long
// ): ApiResponse<List<UserFaceModel>>
/**
* 获取人脸增量数据
*/
@POST
suspend fun getFaceIncrementList(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/faceFeature/increment/list",
@Body param: Map<String, Long>
@Url url:String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/faceFeature/increment/list",
@Body param:Map<String, Long>
): ApiResponse<List<UserFaceModel>?>
/**
@@ -41,25 +185,5 @@ interface ApiService {
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/getYxEquipmentByEquipmentCode"
): ApiResponse<DeviceConfig?>
/**
* 添加人脸数据
*/
@POST
@FormUrlEncoded
suspend fun addUserFace(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/addUserByFace",
@Field("featureChar") faceData: String,
@Field("url") imageUr: String
): ApiResponse<UserFaceModel?>
/**
* 上传图片
*/
@Multipart
@POST
suspend fun uploadImage(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/upload",
@Part file: MultipartBody.Part,
): ApiResponse<String?>
}
@@ -1,104 +1,92 @@
//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
//
//import android.content.Context
//import android.util.Log
//import androidx.work.CoroutineWorker
//import androidx.work.WorkerParameters
//import com.arcsoft.face.FaceEngine
//import com.sw.inbound.utils.GsonUtils
//import com.sw.plate.App
//import com.sw.plate.utils.Base64
//import com.sw.plate.utils.PrefUtils
//import com.sw.plate.utils.arcface.FaceApi
//import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
//import com.sw.plate.utils.arcface.faceserver.FaceServer
//import com.sw.platecabinet.network.ApiClient
//import com.sw.platecabinet.repository.RemoteRepository
//import kotlinx.coroutines.Dispatchers
//import kotlinx.coroutines.withContext
//import org.greenrobot.eventbus.EventBus
//import timber.log.Timber
// 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)
//
//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()
// EventBus.getDefault().post(insertEntity)
//// callback(insertEntity)
// }
// } catch (e: Exception) {
// e.printStackTrace()
// }
// }
// }
// }
//
// 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
//
//import androidx.lifecycle.LifecycleOwner
//import androidx.work.ExistingWorkPolicy
//import androidx.work.OneTimeWorkRequestBuilder
//import androidx.work.WorkInfo
//import androidx.work.WorkManager
//import com.sw.plate.App
//import java.util.concurrent.TimeUnit
//
//object TaskManager {
// private const val TASK_NAME = "SmartPlateCabinetTask"
//// private var owner: LifecycleOwner?=null
//// fun setOwner(owner: LifecycleOwner) {
//// this.owner = owner
//// }
// fun startTask() {
// val nextRequest = OneTimeWorkRequestBuilder<HeartBeatTask>()
// .setInitialDelay(1, TimeUnit.MINUTES)
//// .setInputData(inputData)
// .build()
// WorkManager.getInstance(App.getContext())
//// .apply{
//// getWorkInfoByIdLiveData(nextRequest.id)
//// .observe(owner) { workInfo ->
//// if (workInfo?.state == WorkInfo.State.SUCCEEDED) {
//// val result = workInfo.outputData.getString("RESULT_KEY")
//// // 使用返回的结果
//// }
//// }
//// }
// .enqueueUniqueWork(TASK_NAME, ExistingWorkPolicy.REPLACE, nextRequest)
package com.sw.platecabinet.network.task
import androidx.lifecycle.LifecycleOwner
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.sw.plate.App
import java.util.concurrent.TimeUnit
object TaskManager {
private const val TASK_NAME = "SmartPlateCabinetTask"
// private var owner: LifecycleOwner?=null
// fun setOwner(owner: LifecycleOwner) {
// this.owner = owner
// }
//
// fun cancelTask() {
// WorkManager.getInstance(App.getContext())
// .cancelUniqueWork(TASK_NAME)
// }
//
//}
fun startTask() {
val nextRequest = OneTimeWorkRequestBuilder<HeartBeatTask>()
.setInitialDelay(1, TimeUnit.MINUTES)
// .setInputData(inputData)
.build()
WorkManager.getInstance(App.getContext())
// .apply{
// getWorkInfoByIdLiveData(nextRequest.id)
// .observe(owner) { workInfo ->
// if (workInfo?.state == WorkInfo.State.SUCCEEDED) {
// val result = workInfo.outputData.getString("RESULT_KEY")
// // 使用返回的结果
// }
// }
// }
.enqueueUniqueWork(TASK_NAME, ExistingWorkPolicy.REPLACE, nextRequest)
}
fun cancelTask() {
WorkManager.getInstance(App.getContext())
.cancelUniqueWork(TASK_NAME)
}
}
@@ -1,11 +1,14 @@
package com.sw.platecabinet.repository
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.response.ApiResponse
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.SearchResult
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.network.api.ApiService
import com.sw.platecabinet.utils.FileUtils
import java.io.File
/**
* 远程数据处理
@@ -14,6 +17,42 @@ class RemoteRepository constructor(
private val apiService: ApiService
) : BaseRepository() {
// /**
// * 生成token
// */
// suspend fun getDeviceToken(qrcodeId: String): ApiResponse<String> {
// return safeApiCall {
// apiService.getDeviceToken(
// qrcodeId
// )
// }
// }
//
// /**
// * 获取设备信息
// */
// suspend fun getDeviceInfo(equipmentCode: String, token: String): ApiResponse<EquipmentInfo> {
// return safeApiCall {
// apiService.getDeviceInfo(
// equipmentCode,
// token = token
// )
// }
// }
//
// /**
// * 生成token
// */
// suspend fun generateToken(deviceId: String): ApiResponse<String> {
// return safeApiCall {
// apiService.generateToken(
// deviceId = deviceId,
// appVersion = GlobalData.appVersion,
// equipmentCode = GlobalData.globalEquipmentCode
// )
// }
// }
/**
* 获取人脸数据
*/
@@ -31,6 +70,57 @@ class RemoteRepository constructor(
}
}
/**
* 登录
*/
suspend fun equipmentBoxLogin(param: LoginParam): ApiResponse<EquipmentUserInfo> {
return safeApiCall { apiService.equipmentBoxLogin(param = param) }
}
/**
* 获取设备绑定用户列表
*/
suspend fun getEquipmentList(): ApiResponse<List<EquipmentUserInfo>> {
return safeApiCall { apiService.getEquipmentList() }
}
// /**
// * 餐盘用户信息绑定解绑
// */
// suspend fun bindEquipment(param: BindParam): ApiResponse<EquipmentUserInfo?> {
// return safeApiCall { apiService.bindEquipment(param = param) }
// }
suspend fun plateBind(param: BindParam): ApiResponse<EquipmentUserInfo?> {
return safeApiCall { apiService.plateBind(param = param) }
}
suspend fun plateUnbind(id: Long?): ApiResponse<Any?> {
return safeApiCall { apiService.plateUnbind(id = id) }
}
/**
* 用户信息模糊搜索
*/
suspend fun searchUser(param: SearchParam): ApiResponse<List<SearchResult.Member>?> {
return safeApiCall { apiService.searchUser(param = param) }
}
/**
* 通过餐盘编号获取绑定信息
*/
suspend fun findByPlateNumber(plateNumber: String): ApiResponse<EquipmentUserInfo> {
return safeApiCall { apiService.findByPlateNumber(plateNumber = plateNumber) }
}
/**
* 获取人脸增量数据
*
* @param timeDate 时间戳(毫秒级)
*/
// suspend fun getHeartbeatInterface(timeDate: Long): ApiResponse<List<UserFaceModel>> {
// return safeApiCall { apiService.getHeartbeatInterface(timeDate = timeDate) }
// }
suspend fun getFaceIncrementList(
pageNum: Long,
@@ -51,19 +141,4 @@ class RemoteRepository constructor(
suspend fun getDeviceConfig(): ApiResponse<DeviceConfig?> {
return safeApiCall { apiService.getDeviceConfig() }
}
suspend fun addUserFace(faceData: String, imageUr: String): ApiResponse<UserFaceModel?> {
return safeApiCall { apiService.addUserFace(faceData = faceData, imageUr = imageUr) }
}
suspend fun uploadImage(file: File): ApiResponse<String?> {
return safeApiCall {
val part = FileUtils.genRequestPart(file)
if (part == null) {
ApiResponse(code = "-1", msg = "解析图片失败")
} else {
apiService.uploadImage(file = part)
}
}
}
}
@@ -22,7 +22,7 @@ class RemoteRepositoryV2 constructor(
*/
suspend fun getUserFaceCache(
pageNum: Int,
pageSize: Int = 100,
pageSize: Int = 500,
): ApiResponse<List<UserFaceModelV2>> {
return safeApiCall {
apiService.getUserFaceCache(
@@ -1,300 +0,0 @@
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();
}
}
}
@@ -1,35 +0,0 @@
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));
}
}
@@ -1,21 +0,0 @@
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")
}
}
@@ -1,330 +0,0 @@
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);
}
}
@@ -1,33 +0,0 @@
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()}")
}
}
@@ -1,47 +0,0 @@
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();
}
}
@@ -1,113 +0,0 @@
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();
}
}
@@ -1,59 +0,0 @@
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
}
}
}
@@ -1,79 +0,0 @@
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,11 +115,7 @@ class CrashHandler private constructor(private val context: Context) :
collectDeviceInfo(this)
// 收集应用日志
try {
append("\n\n").append(collectLogs())
} catch (e: OutOfMemoryError) {
append("collectLogs发生OutOfMemoryError:${e.message}")
}
append("\n\n").append(collectLogs())
// 收集线程和异常信息
append("\n\n========== Thread & Exception Info ==========\n")
@@ -154,7 +150,7 @@ class CrashHandler private constructor(private val context: Context) :
// 设备信息
sb.append("Vendor: ${Build.MANUFACTURER}\n")
sb.append("Model: ${Build.MODEL}\n")
sb.append("CPU ABI: ${Build.SUPPORTED_ABIS.firstOrNull() ?: "unknown"}\n")
sb.append("CPU ABI: ${Build.SUPPORTED_ABIS[0]}\n")
// 其他信息
sb.append("Locale: ${Locale.getDefault()}\n")
@@ -28,10 +28,10 @@ object DateTimeUtils {
}
/**
* 获取带时间的完整中文格式(示例:2025年06月11日 星期三 14:30
* 获取带时间的完整中文格式(示例:2025年6月11日 星期三 14:30
*/
fun getChineseDateTimeString(date: Date = Date()): String {
return SimpleDateFormat("yyyy年MM月dd日 EEEE HH:mm", Locale.CHINA).format(date)
return SimpleDateFormat("yyyy年Md日 EEEE HH:mm:ss", Locale.CHINA).format(date)
}
// 使用线程安全的日期格式化(避免 SimpleDateFormat 的线程安全问题)
@@ -52,7 +52,7 @@ object DateTimeUtils {
*/
fun realTimeChineseDateFlow(intervalMillis: Long = 1000) = flow {
while (true) {
emit(getChineseDateTimeString())
emit(getChineseDateTimePair())
delay(intervalMillis)
}
}
@@ -1,11 +1,6 @@
package com.sw.platecabinet.utils
import timber.log.Timber
class Debouncer(private val delayMillis: Long) {
companion object {
const val TAG = "Debouncer"
}
private var lastActionTime = 0L
/**
@@ -13,15 +8,13 @@ class Debouncer(private val delayMillis: Long) {
* @param action 要执行的操作
* @return Boolean 是否执行了操作 (true=已执行, false=被防抖)
*/
fun debounce(tag:String = TAG, action: () -> Unit): Boolean {
fun debounce(action: () -> Unit): Boolean {
val currentTime = System.currentTimeMillis()
if (currentTime - lastActionTime >= delayMillis) {
Timber.tag(tag).d("collectFace,recognizeUserId防止重复操作:执行action回调")
lastActionTime = currentTime
action()
return true
}
Timber.tag(tag).d("collectFace,recognizeUserId防止重复操作-----------------------")
return false
}
@@ -1,33 +0,0 @@
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,7 +15,6 @@ class IntervalExecutor {
fun startIntervalTask(delayMillis: Long, action: suspend () -> Unit): Job {
return CoroutineScope(Dispatchers.Default).launch {
while (isActive) {
ensureActive()
action()
delay(delayMillis)
}
@@ -37,7 +36,6 @@ class IntervalExecutor {
return CoroutineScope(Dispatchers.Default).launch {
delay(initialDelay)
while (isActive) {
ensureActive()
action()
delay(delayMillis)
}
@@ -46,23 +44,23 @@ class IntervalExecutor {
}
// 使用示例
//fun main() = runBlocking {
// val executor = IntervalExecutor()
//
// // 示例1:每隔10秒执行一次
// val job1 = executor.startIntervalTask(10000) {
// println("定时任务执行: ${System.currentTimeMillis()}")
// // 这里可以执行你的业务逻辑
// }
//
// // 示例2:先延迟5秒,然后每隔3秒执行一次
// val job2 = executor.startIntervalTaskWithInitialDelay(5000, 3000) {
// println("带初始延迟的定时任务: ${System.currentTimeMillis()}")
// }
//
// // 运行30秒后取消任务
// delay(30000)
// job1.cancel()
// job2.cancel()
// println("所有定时任务已取消")
//}
fun main() = runBlocking {
val executor = IntervalExecutor()
// 示例1:每隔10秒执行一次
val job1 = executor.startIntervalTask(10000) {
println("定时任务执行: ${System.currentTimeMillis()}")
// 这里可以执行你的业务逻辑
}
// 示例2:先延迟5秒,然后每隔3秒执行一次
val job2 = executor.startIntervalTaskWithInitialDelay(5000, 3000) {
println("带初始延迟的定时任务: ${System.currentTimeMillis()}")
}
// 运行30秒后取消任务
delay(30000)
job1.cancel()
job2.cancel()
println("所有定时任务已取消")
}
@@ -1,57 +0,0 @@
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);
}
}
@@ -1,88 +1,88 @@
//package com.sw.platecabinet.utils
//
//import com.sw.platecabinet.model.response.EquipmentUserInfo
//import timber.log.Timber
//import kotlin.math.ceil
//
///**
// * 列表排序工具类
// */
//object ListArrangementUtil {
//
// /**
// * page item水平排序
// */
// fun horizontalSortPageItem(
// list: List<EquipmentUserInfo>,
// columns: Int,
// defaultValue: EquipmentUserInfo = EquipmentUserInfo()
// ): List<EquipmentUserInfo> {
// Timber.d("horizontalSortPageItem")
// require(columns > 0) { "Columns must be positive" }
//
// val filledList = list.padToMultiple(columns, defaultValue)
// val chunks = filledList.chunked(columns)
//
// return (0 until columns)
// .windowed(2, 2, partialWindows = true)
// .flatMap { group ->
// when (group.size) {
// 2 -> chunks.flatMap { listOf(it[group[0]], it[group[1]]) }
// 1 -> chunks.flatMap {
// listOf(it[group[0]], EquipmentUserInfo(equipmentBoxCode = null))
// }.dropLast(1)
// else -> error("Unexpected group size")
// }
// }
// }
//
// /**
// * item 垂直排序
// */
// fun verticalSortItem(list: List<EquipmentUserInfo>): List<EquipmentUserInfo> {
// Timber.d("verticalSortItem")
// val users = list.padToEvenSize()
// val half = users.size / 2
//
// return users.take(half)
// .zip(users.drop(half)) { a, b -> listOf(a, b) }
// .flatten()
// }
//
// /**
// * page item垂直排序
// */
// fun convertToColumnFirst(
// original: List<EquipmentUserInfo>,
// cols: Int
// ): List<EquipmentUserInfo> {
// Timber.d("convertToColumnFirst")
// require(cols > 0) { "Columns must be positive" }
//
// val rows = ceil(original.size.toDouble() / cols).toInt()
// return List(original.size) { pos ->
// original.getOrNull(pos % rows * cols + pos / rows) ?: EquipmentUserInfo()
// }
// }
//
// // 提取的扩展函数
// private fun List<EquipmentUserInfo>.padToMultiple(
// multiple: Int,
// defaultValue: EquipmentUserInfo
// ): List<EquipmentUserInfo> {
// return if (size % multiple != 0) {
// this + List(multiple - (size % multiple)) { defaultValue }
// } else {
// this
// }
// }
//
// private fun List<EquipmentUserInfo>.padToEvenSize(): List<EquipmentUserInfo> {
// return if (size % 2 != 0) {
// Timber.d("列表长度必须是偶数")
// this + EquipmentUserInfo()
// } else {
// this
// }
// }
//}
package com.sw.platecabinet.utils
import com.sw.platecabinet.model.response.EquipmentUserInfo
import timber.log.Timber
import kotlin.math.ceil
/**
* 列表排序工具类
*/
object ListArrangementUtil {
/**
* page item水平排序
*/
fun horizontalSortPageItem(
list: List<EquipmentUserInfo>,
columns: Int,
defaultValue: EquipmentUserInfo = EquipmentUserInfo()
): List<EquipmentUserInfo> {
Timber.d("horizontalSortPageItem")
require(columns > 0) { "Columns must be positive" }
val filledList = list.padToMultiple(columns, defaultValue)
val chunks = filledList.chunked(columns)
return (0 until columns)
.windowed(2, 2, partialWindows = true)
.flatMap { group ->
when (group.size) {
2 -> chunks.flatMap { listOf(it[group[0]], it[group[1]]) }
1 -> chunks.flatMap {
listOf(it[group[0]], EquipmentUserInfo(equipmentBoxCode = null))
}.dropLast(1)
else -> error("Unexpected group size")
}
}
}
/**
* item 垂直排序
*/
fun verticalSortItem(list: List<EquipmentUserInfo>): List<EquipmentUserInfo> {
Timber.d("verticalSortItem")
val users = list.padToEvenSize()
val half = users.size / 2
return users.take(half)
.zip(users.drop(half)) { a, b -> listOf(a, b) }
.flatten()
}
/**
* page item垂直排序
*/
fun convertToColumnFirst(
original: List<EquipmentUserInfo>,
cols: Int
): List<EquipmentUserInfo> {
Timber.d("convertToColumnFirst")
require(cols > 0) { "Columns must be positive" }
val rows = ceil(original.size.toDouble() / cols).toInt()
return List(original.size) { pos ->
original.getOrNull(pos % rows * cols + pos / rows) ?: EquipmentUserInfo()
}
}
// 提取的扩展函数
private fun List<EquipmentUserInfo>.padToMultiple(
multiple: Int,
defaultValue: EquipmentUserInfo
): List<EquipmentUserInfo> {
return if (size % multiple != 0) {
this + List(multiple - (size % multiple)) { defaultValue }
} else {
this
}
}
private fun List<EquipmentUserInfo>.padToEvenSize(): List<EquipmentUserInfo> {
return if (size % 2 != 0) {
Timber.d("列表长度必须是偶数")
this + EquipmentUserInfo()
} else {
this
}
}
}
@@ -1,698 +0,0 @@
package com.sw.platecabinet.utils;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import com.sw.plate.App;
import com.sw.plate.utils.AppUtil;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringReader;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class LogFileUtil {
private static boolean isSaveLog = true;
// 使用内部存储,不会被系统缓存清理策略影响
public static final String FILE_STR_PATH = new File(App.getContext().getFilesDir(), "logs").getAbsolutePath();
// 单线程执行器,保证日志串行写入,避免多线程并发竞争
private static final ExecutorService LOG_EXECUTOR = Executors.newSingleThreadExecutor();
public static final int SIZETYPE_B = 1;//获取文件大小单位为B的double值
public static final int SIZETYPE_KB = 2;//获取文件大小单位为KB的double值
public static final int SIZETYPE_MB = 3;//获取文件大小单位为MB的double值
public static final int SIZETYPE_GB = 4;//获取文件大小单位为GB的double值
private static String urlNull = "原文件路径不存在";
private static String isFile = "原文件不是文件";
private static String canRead = "原文件不能读";
private static String copyFalse = "备份失败!";
private static String cFromFile = "创建原文件出错:";
private static String ctoFile = "创建备份文件出错:";
/**
* 写入文件
* FileUtil.byteWriteFile(getExternalFilesDir("123"),"test.jpg",nv21);
*
* @param filePath
* @param fileName
* @param bytes
*/
public static void byteWriteFile(File filePath, String fileName, byte[] bytes) {
BufferedOutputStream bout = null;
try {
File file = new File(filePath, fileName);
if (file.exists() == false) {
if (file.getParentFile().exists() == false) {
file.getParentFile().mkdirs();
}
file.createNewFile();
}
bout = new BufferedOutputStream(new FileOutputStream(file, false));
bout.write(bytes);
bout.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (bout != null) {
try {
bout.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 获取文件名
*
* @param path 路径
* @return 文件名
*/
public static String getFileName(String path) {
int index = path.lastIndexOf("/");
return path.substring(index + 1);
}
/**
* 根据文件路径获取文件
*
* @param path 路径
* @return
*/
public static File getFileByPath(String path) {
File file = new File(path);
if (file.exists()) {
return new File(path);
} else {
return null;
}
}
/**
* 重命名文件
*
* @param filePath 文件路径
* @param newName 新名称
* @return {@code true}: 重命名成功<br>{@code false}: 重命名失败
*/
public static boolean rename(String filePath, String newName) {
return rename(getFileByPath(filePath), newName);
}
/**
* 重命名文件
*
* @param file 文件
* @param newName 新名称
* @return {@code true}: 重命名成功<br>{@code false}: 重命名失败
*/
public static boolean rename(File file, String newName) {
// 文件为空返回false
if (file == null) return false;
// 文件不存在返回false
if (!file.exists()) return false;
// 新的文件名为空返回false
if (AppUtil.isEmpty(newName)) return false;
// 如果文件名没有改变返回true
if (newName.equals(file.getName())) return true;
File newFile = new File(file.getParent() + File.separator + newName);
// 如果重命名的文件已存在返回false
return !newFile.exists()
&& file.renameTo(newFile);
}
/**
* 判断文件是否存在
*
* @param path 文件的路径,含文件后缀和文件名
* @return 是否存在
*/
public static boolean isFileExists(String path) {
File file = new File(path);
if (file.exists()) {
return true;
} else {
return false;
}
}
/**
* 判断文件是否存在
*
* @return 是否存在
*/
public static boolean isFileExists(File file) {
if (file.exists()) {
return true;
} else {
return false;
}
}
/**
* 删除文件夹及文件夹下所有内容
*
* @param path 文件夹路径
* @return 返回是否删除成功
*/
public static boolean deleteFiles(String path) {
if (getFileByPath(path) != null) {
return deleteFiles(getFileByPath(path));
} else {
return false;
}
}
/**
* 删除文件夹及文件夹下所有内容
*
* @param file 文件
* @return 返回是否删除成功
*/
public static boolean deleteFiles(File file) {
try {
if (file.exists()) { // 判断文件是否存在
if (file.isFile()) { // 判断是否是文件
file.delete(); // delete()方法
} else if (file.isDirectory()) { // 否则如果它是一个目录
File files[] = file.listFiles(); // 声明目录下所有的文件 files[];
for (int i = 0; i < files.length; i++) { // 遍历目录下所有的文件
deleteFiles(files[i].getPath()); // 把每个文件 用这个方法进行迭代
}
file.delete();//删除目录
}
//file.delete();
return true;
} else {
return false;
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 获取文件的Uri
*
* @param path 文件的路径
* @return 文件的Uri
*/
public static Uri getUriFromFile(String path) {
File file = new File(path);
return Uri.fromFile(file);
}
/**
* 获取文件指定文件的指定单位的大小
*
* @param filePath 文件路径
* @param sizeType 获取大小的类型1为B、2为KB、3为MB、4为GB
* @return double值的大小
*/
public static double getFileOrFilesSize(String filePath, int sizeType) {
File file = new File(filePath);
long blockSize = 0;
try {
if (file.isDirectory()) {
blockSize = getFileSizes(file);
} else {
blockSize = getFileSize(file);
}
} catch (Exception e) {
e.printStackTrace();
L.i("获取文件大小", "获取失败!");
}
return FormetFileSize(blockSize, sizeType);
}
/**
* 调用此方法自动计算指定文件或指定文件夹的大小
*
* @param filePath 文件路径
* @return 计算好的带B、KB、MB、GB的字符串
*/
public static String getAutoFileOrFilesSize(String filePath) {
File file = new File(filePath);
long blockSize = 0;
try {
if (file.isDirectory()) {
blockSize = getFileSizes(file);
} else {
blockSize = getFileSize(file);
}
} catch (Exception e) {
e.printStackTrace();
L.i("获取文件大小", "获取失败!");
}
return FormetFileSize(blockSize);
}
/**
* 获取指定文件大小
*
* @param file
* @return
* @throws Exception
*/
private static long getFileSize(File file) throws Exception {
long size = 0;
if (file.exists()) {
FileInputStream fis = null;
fis = new FileInputStream(file);
size = fis.available();
} else {
// file.createNewFile();
L.i("获取文件大小", "文件不存在!");
}
return size;
}
/**
* 获取目录下指定文件名的文件包括子目录
* <p>大小写忽略</p>
*
* @param dirPath 目录路径
* @param fileName 文件名
* @return 文件链表
*/
public static List<File> searchFileInDir(String dirPath, String fileName) {
return searchFileInDir(getFileByPath(dirPath), fileName);
}
/**
* 获取目录下指定文件名的文件包括子目录
* <p>大小写忽略</p>
*
* @param dir 目录
* @param fileName 文件名
* @return 文件链表
*/
public static List<File> searchFileInDir(File dir, String fileName) {
if (dir == null || !dir.isDirectory()) return null;
List<File> list = new ArrayList<>();
File[] files = dir.listFiles();
if (files != null && files.length != 0) {
for (File file : files) {
if (file.getName().toUpperCase().equals(fileName.toUpperCase())) {
list.add(file);
}
if (file.isDirectory()) {
list.addAll(searchFileInDir(file, fileName));
}
}
}
return list;
}
/**
* 获取指定文件夹
*
* @param f
* @return
* @throws Exception
*/
private static long getFileSizes(File f) throws Exception {
long size = 0;
File flist[] = f.listFiles();
for (int i = 0; i < flist.length; i++) {
if (flist[i].isDirectory()) {
size = size + getFileSizes(flist[i]);
} else {
size = size + getFileSize(flist[i]);
}
}
return size;
}
/**
* 转换文件大小
*
* @param fileS
* @return
*/
private static String FormetFileSize(long fileS) {
DecimalFormat df = new DecimalFormat("#.00");
String fileSizeString = "";
String wrongSize = "0B";
if (fileS == 0) {
return wrongSize;
}
if (fileS < 1024) {
fileSizeString = df.format((double) fileS) + "B";
} else if (fileS < 1048576) {
fileSizeString = df.format((double) fileS / 1024) + "KB";
} else if (fileS < 1073741824) {
fileSizeString = df.format((double) fileS / 1048576) + "MB";
} else {
fileSizeString = df.format((double) fileS / 1073741824) + "GB";
}
return fileSizeString;
}
/**
* 转换文件大小,指定转换的类型
*
* @param fileS
* @param sizeType
* @return
*/
private static double FormetFileSize(long fileS, int sizeType) {
DecimalFormat df = new DecimalFormat("#.00");
double fileSizeLong = 0;
switch (sizeType) {
case SIZETYPE_B:
fileSizeLong = Double.valueOf(df.format((double) fileS));
break;
case SIZETYPE_KB:
fileSizeLong = Double.valueOf(df.format((double) fileS / 1024));
break;
case SIZETYPE_MB:
fileSizeLong = Double.valueOf(df.format((double) fileS / 1048576));
break;
case SIZETYPE_GB:
fileSizeLong = Double.valueOf(df.format((double) fileS / 1073741824));
break;
default:
break;
}
return fileSizeLong;
}
/**
* 复制文件
*
* @param fromFilePath 旧文件地址和名称
* @param toFilePath 新文件地址和名称
* @return 返回备份文件的信息,ok是成功,其它就是错误
*/
public static File copyFile(String fromFilePath, String toFilePath) {
File fromFile = null;
File toFile = null;
try {
fromFile = new File(fromFilePath);
} catch (Exception e) {
L.i(cFromFile + e.getMessage());
return null;
}
try {
toFile = new File(toFilePath);
if (toFile.isDirectory()) {
toFile = new File(toFilePath.endsWith("/") ? toFilePath + fromFile.getName() : toFilePath + "/" + fromFile.getName());
}
} catch (Exception e) {
L.i(ctoFile + e.getMessage());
return null;
}
if (!fromFile.exists()) {
L.i(urlNull);
return null;
}
if (!fromFile.isFile()) {
L.i(isFile);
return null;
}
if (!fromFile.canRead()) {
L.i(canRead);
return null;
}
// 复制到的路径如果不存在就创建
if (!toFile.getParentFile().exists()) {
toFile.getParentFile().mkdirs();
}
if (toFile.exists()) {
toFile.delete();
}
if (!toFile.canWrite()) {
//return notWrite;
}
try {
FileInputStream fosfrom = new FileInputStream(
fromFile);
FileOutputStream fosto = new FileOutputStream(toFile);
byte bt[] = new byte[1024];
int c;
while ((c = fosfrom.read(bt)) > 0) {
fosto.write(bt, 0, c); // 将内容写到新文件当中
}
//关闭数据流
fosfrom.close();
fosto.close();
} catch (Exception e) {
e.printStackTrace();
L.i(copyFalse + e.getMessage());
return null;
}
return toFile;
}
/**
* 创建文件
*
* @param path 文件路径
* @return 创建的文件
*/
public static synchronized File createNewFile(String path) {
File file = new File(path);
String parentPath = path.substring(0, path.lastIndexOf("/") + 1);
File parentFile = new File(parentPath);
if (!parentFile.exists()) {
parentFile.mkdirs();
}
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
return file;
}
/***
* 根据文件后缀回去MIME类型
****/
private static String getMIMEType(File file) {
String type = "*/*";
String fName = file.getName();
//获取后缀名前的分隔符"."在fName中的位置。
int dotIndex = fName.lastIndexOf(".");
if (dotIndex < 0) {
return type;
}
/* 获取文件的后缀名*/
String end = fName.substring(dotIndex, fName.length()).toLowerCase();
if (end == "") return type;
//在MIME和文件类型的匹配表中找到对应的MIME类型。
for (int i = 0; i < MIME_MapTable.length; i++) { //MIME_MapTable??在这里你一定有疑问,这个MIME_MapTable是什么?
if (end.equals(MIME_MapTable[i][0]))
type = MIME_MapTable[i][1];
}
return type;
}
/**
* 获取全路径中的文件拓展名
*
* @param file 文件
* @return 文件拓展名
*/
public static String getFileExtension(File file) {
if (file == null) return null;
return getFileExtension(file.getPath());
}
/**
* 获取全路径中的文件拓展名
*
* @param filePath 文件路径
* @return 文件拓展名
*/
public static String getFileExtension(String filePath) {
if (AppUtil.isEmpty(filePath)) return filePath;
int lastPoi = filePath.lastIndexOf('.');
int lastSep = filePath.lastIndexOf(File.separator);
if (lastPoi == -1 || lastSep >= lastPoi) return "";
return filePath.substring(lastPoi + 1);
}
/**
* 调用系统应用打开文件
*
* @param context 上下文
* @param file 文件
*/
public static void openFile(Context context, File file) {
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//设置intent的Action属性
intent.setAction(Intent.ACTION_VIEW);
//获取文件file的MIME类型
String type = getMIMEType(file);
//设置intent的data和Type属性。
intent.setDataAndType(Uri.fromFile(file), type);
//跳转
try {
context.startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
L.i("找不到打开此文件的应用!");
}
}
/**
* 保存文本文件
* FileUtil.saveStrFile("测试", "log.txt", FILE_STR_PATH, true);
*
* @param res 写入内容
* @param fileName 文件名
* @param filePath 路径
* @param append true新增 false替换
* @return
*/
public static boolean saveStrFile(String res, String fileName, String filePath, boolean append) {
boolean flag = true;
BufferedReader bufferedReader = null;
BufferedWriter bufferedWriter = null;
try {
File file = new File(filePath, fileName);
if (!file.exists()) {
file.getParentFile().mkdirs();
file.createNewFile();
}
bufferedReader = new BufferedReader(new StringReader(res));
bufferedWriter = new BufferedWriter(new FileWriter(file, append));
char buf[] = new char[1024]; //字符缓冲区
int len;
while ((len = bufferedReader.read(buf)) != -1) {
bufferedWriter.write(buf, 0, len);
}
bufferedWriter.flush();
bufferedReader.close();
bufferedWriter.close();
} catch (IOException e) {
e.printStackTrace();
flag = false;
return flag;
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return flag;
}
public static void saveLog(String... logs) {
if (!isSaveLog) {
return;
}
// 提交到单线程队列,串行执行,避免并发写入竞争
LOG_EXECUTOR.execute(() -> {
StringBuilder sb = new StringBuilder();
for (String log : logs) {
sb.append("===")
.append((AppUtil.formatDateGetCurrentTime()))
.append("===")
.append("\n")
.append(log)
.append("\n");
}
String fileName = "log" + AppUtil.formatDateGetDay(System.currentTimeMillis()) + ".txt";
LogFileUtil.saveStrFile(sb.toString(), fileName, FILE_STR_PATH, true);
});
}
private static final String[][] MIME_MapTable = {
// {后缀名,MIME类型}
{".3gp", "video/3gpp"},
{".apk", "application/vnd.android.package-archive"},
{".asf", "video/x-ms-asf"},
{".avi", "video/x-msvideo"},
{".bin", "application/octet-stream"},
{".bmp", "image/bmp"},
{".c", "text/plain"},
{".class", "application/octet-stream"},
{".conf", "text/plain"},
{".cpp", "text/plain"},
{".doc", "application/msword"},
{".docx",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"},
{".xls", "application/vnd.ms-excel"},
{".xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
{".exe", "application/octet-stream"},
{".gif", "image/gif"},
{".gtar", "application/x-gtar"},
{".gz", "application/x-gzip"},
{".h", "text/plain"},
{".htm", "text/html"},
{".html", "text/html"},
{".jar", "application/java-archive"},
{".java", "text/plain"},
{".jpeg", "image/jpeg"},
{".jpg", "image/jpeg"},
{".js", "application/x-javascript"},
{".log", "text/plain"},
{".m3u", "audio/x-mpegurl"},
{".m4a", "audio/mp4a-latm"},
{".m4b", "audio/mp4a-latm"},
{".m4p", "audio/mp4a-latm"},
{".m4u", "video/vnd.mpegurl"},
{".m4v", "video/x-m4v"},
{".mov", "video/quicktime"},
{".mp2", "audio/x-mpeg"},
{".mp3", "audio/x-mpeg"},
{".mp4", "video/mp4"},
{".mpc", "application/vnd.mpohun.certificate"},
{".mpe", "video/mpeg"},
{".mpeg", "video/mpeg"},
{".mpg", "video/mpeg"},
{".mpg4", "video/mp4"},
{".mpga", "audio/mpeg"},
{".msg", "application/vnd.ms-outlook"},
{".ogg", "audio/ogg"},
{".pdf", "application/pdf"},
{".png", "image/png"},
{".pps", "application/vnd.ms-powerpoint"},
{".ppt", "application/vnd.ms-powerpoint"},
{".pptx",
"application/vnd.openxmlformats-officedocument.presentationml.presentation"},
{".prop", "text/plain"}, {".rc", "text/plain"},
{".rmvb", "audio/x-pn-realaudio"}, {".rtf", "application/rtf"},
{".sh", "text/plain"}, {".tar", "application/x-tar"},
{".tgz", "application/x-compressed"}, {".txt", "text/plain"},
{".wav", "audio/x-wav"}, {".wma", "audio/x-ms-wma"},
{".wmv", "audio/x-ms-wmv"},
{".wps", "application/vnd.ms-works"}, {".xml", "text/plain"},
{".z", "application/x-compress"},
{".zip", "application/x-zip-compressed"}, {"", "*/*"}};
}
@@ -77,7 +77,11 @@ class PermissionHelper private constructor(
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
context.startActivity(intent)
if (context is Activity) {
context.startActivity(intent)
} else if (context is Fragment) {
context.startActivity(intent)
}
}
}
@@ -1,66 +0,0 @@
package com.sw.platecabinet.utils
import androidx.fragment.app.FragmentActivity
import com.sw.platecabinet.activity.BaseActivity
object PlateUtils {
fun open(
fragmentActivity: FragmentActivity,
code: String?,
isScanCode: Boolean,
isSetting: Boolean = false,
callback: () -> Unit = {}
) {
val activity = fragmentActivity as BaseActivity<*>
activity.window.decorView.postDelayed({
callback()
}, 1500)
// activity.showWaitingDialog("请稍等")
// Timber.d("itemClick llOpen equipmentBoxCode = ${code}")
// val equipmentBoxCode: Int = code?.toInt() ?: -1
// if (equipmentBoxCode < 0) {
// ToastUtils.showToast("餐盘柜编号错误")
// return
// }
// activity.showWaitingDialog("请稍等")
// SerialApi.openPlate(equipmentBoxCode, object : SerialPortManager.SendCallback {
// override fun onSuccess() {
// activity.hideWaitingDialog()
// if (isSetting.not()) {
// MainActivity.start(
// activity,
// pageType = PageType.PLATE_TIP,
// isScanCode = isScanCode
// )
// } else {
// ToastUtils.showToast("柜门打开成功")
// callback()
// }
// }
//
// override fun onFail(e: Exception?) {
// activity.hideWaitingDialog()
// ToastUtils.showToast("柜门打开失败")
// callback()
// }
// })
}
// fun openDirect(boxCode: String?) {
// val boxNumber = boxCode?.toIntOrNull() ?: -1
// if (boxNumber < 0) return
// SerialApi.openPlate(boxNumber, object : SerialPortManager.SendCallback {
// override fun onSuccess() {
//
// }
//
// override fun onFail(e: Exception?) {
//
// }
// })
// }
}
@@ -1,15 +0,0 @@
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()
}
}
}
}
@@ -1,427 +0,0 @@
package com.sw.platecabinet.utils;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Build;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class SoundPoolUtil {
private static final String TAG = SoundPoolUtil.class.getSimpleName();
private static SoundPoolUtil mSound;
private SoundPool mSoundPool;
private boolean isLoadC = false;
private Map<String, Integer> idCache;
private List<Integer> sidCache;
public SoundPoolUtil() {
idCache = new HashMap<>();
sidCache = new ArrayList<>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
AudioAttributes aab = new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build();
mSoundPool = new SoundPool.Builder()
.setMaxStreams(10)
.setAudioAttributes(aab)
.build();
} else {
mSoundPool = new SoundPool(10, AudioManager.STREAM_MUSIC, 8);
}
// mSoundPool = new SoundPool(60, AudioManager.USE_DEFAULT_STREAM_TYPE, 7);
mSoundPool.setOnLoadCompleteListener(new MyOnLoadCompleteListener());
}
public static SoundPoolUtil getInstance() {
synchronized (SoundPoolUtil.class) {
if (mSound == null) {
mSound = new SoundPoolUtil();
}
}
return mSound;
}
private int loadCompleteSize = 0;
private LoadCompletion completionListener;
public void setCompletionListener(LoadCompletion listener) {
this.completionListener = listener;
}
private class MyOnLoadCompleteListener implements SoundPool.OnLoadCompleteListener {
@Override
public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {
loadCompleteSize++;
L.e("loadSize" + loadSize + "===" + loadCompleteSize);
if (loadSize == loadCompleteSize) {
isLoadC = true;
if (completionListener != null) {
completionListener.onCompletion();
}
}
}
}
/**
* 加载指定资源
*
* @param name
* @param path
*/
public void loadR(String name, String path) {
if (checkSoundPool()) {
if (!idCache.containsKey(name)) {
idCache.put(name, mSoundPool.load(path, 1));
}
}
}
private int loadSize = 0;
/**
* 加载指定路径列表的资源
*
* @param map
*/
public void loadR(Map<String, String> map) {
loadSize = map.size();
Set<Map.Entry<String, String>> entries = map.entrySet();
for (Map.Entry<String, String> entry : entries) {
String key = entry.getKey();
if (checkSoundPool()) {
if (!idCache.containsKey(key)) {
idCache.put(key, mSoundPool.load(entry.getValue(), 1));
}
}
}
}
/**
* 加载指定AssetFileDescriptor的资源
*
* @param name
* @param afd
*/
public void loadRF(String name, AssetFileDescriptor afd) {
if (checkSoundPool()) {
if (!idCache.containsKey(name)) {
idCache.put(name, mSoundPool.load(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength(), 1));
}
}
}
/**
* 加载指定AssetFileDescriptor列表的资源
*
* @param map
*/
public void loadRF(Map<String, AssetFileDescriptor> map) {
Set<Map.Entry<String, AssetFileDescriptor>> entries = map.entrySet();
for (Map.Entry<String, AssetFileDescriptor> entry : entries) {
String key = entry.getKey();
if (checkSoundPool()) {
if (!idCache.containsKey(key)) {
idCache.put(key, mSoundPool.load(entry.getValue().getFileDescriptor(), entry.getValue().getStartOffset(), entry.getValue().getLength(), 1));
}
}
}
}
/**
* 加载指定列表资源
*
* @param context
* @param map
*/
public void loadR(Context context, Map<String, Integer> map) {
loadSize = map.size();
Set<Map.Entry<String, Integer>> entries = map.entrySet();
for (Map.Entry<String, Integer> entry : entries) {
String key = entry.getKey();
if (checkSoundPool()) {
if (!idCache.containsKey(key)) {
idCache.put(key, mSoundPool.load(context, entry.getValue(), 1));
}
}
}
}
/**
* 加载单个音频
*
* @param context
* @param name
* @param res
*/
public void loadR(Context context, String name, int res) {
if (checkSoundPool()) {
if (!idCache.containsKey(name)) {
idCache.put(name, mSoundPool.load(context, res, 1));
}
}
}
/**
* 播放指定音频,并返用于停止、暂停、恢复的StreamId
*
* @param name
* @param times
* @return
*/
public int play(String name, int times) {
L.e(String.format("play %s times=%s", name, times));
return this.play(name, 1, 1, 1, times, 1);
}
/**
* 播放指定音频,并指定播放次数和频率
*
* @param name
* @param times
* @param rate
* @return
*/
public int play(String name, int times, int rate) {
return this.play(name, 1, 1, 1, times, rate);
}
/**
* 播放指定音频,并指定优先级和播放频率
*
* @param name
* @param property
* @param times
* @param rate
* @return
*/
public int play(String name, int property, int times, int rate) {
return this.play(name, 1, 1, property, times, rate);
}
/**
* 播放指定音频,并指定左右声道、优先级、播放次数、播放频率
*
* @param name
* @param leftVolume
* @param rightVolume
* @param property
* @param times
* @param rate
* @return
*/
public int play(String name, float leftVolume, float rightVolume, int property, int times, int rate) {
int streamId = -1;
if (checkSoundPool()) {
L.d(TAG, "play: " + name);
if (idCache.containsKey(name) && isLoadC) {
L.d(TAG, "name:" + idCache.get(name));
streamId = mSoundPool.play(idCache.get(name), leftVolume, rightVolume, property, times, rate);
L.d(TAG, "streadmId:" + streamId);
sidCache.add(streamId);
}
}
return streamId;
}
/**
* 播放指定列表的音频,并返回并返用于停止、暂停、恢复的StreamId列表
*
* @param names
* @param times
* @return
*/
public List<Integer> play(List<String> names, int times) {
return this.play(names, 1, 1, 1, times, 1);
}
/**
* 播放指定列表的音频,并返回并返用于停止、暂停、恢复的StreamId列表,指定次数和频率
*
* @param names
* @param times
* @param rate
* @return
*/
public List<Integer> play(List<String> names, int times, int rate) {
return this.play(names, 1, 1, 1, times, rate);
}
/**
* 播放指定列表的音频,并返回并返用于停止、暂停、恢复的StreamId列表,指定所有参数
*
* @param names
* @param leftVolume
* @param rightVolume
* @param property
* @param times
* @param rate
* @return
*/
public List<Integer> play(List<String> names, int leftVolume, int rightVolume, int property, int times, int rate) {
List<Integer> streamIds = new ArrayList<>();
if (checkSoundPool()) {
for (String name : names) {
if (idCache.containsKey(name) && isLoadC) {
int a = mSoundPool.play(idCache.get(name), leftVolume, rightVolume, property, times, rate);
streamIds.add(a);
sidCache.add(a);
}
}
}
return streamIds;
}
/**
* 停止指定id音频
*/
public void stop(int r) {
if (checkSoundPool()) {
mSoundPool.stop(r);
}
}
/**
* 停止指定列表音频
*/
public void stopAll() {
if (checkSoundPool()) {
for (int r : sidCache) {
mSoundPool.stop(r);
}
}
}
/**
* 暂停指定音效
*
* @param r
*/
public void pause(int r) {
if (checkSoundPool()) {
mSoundPool.pause(r);
}
}
/**
* 暂停指定列表音频
*
* @param list
*/
public void pause(List<Integer> list) {
if (checkSoundPool()) {
for (int r : list) {
mSoundPool.pause(r);
}
}
}
/**
* 暂停所有音效
*/
public void pauseAll() {
mSoundPool.autoPause();
}
/**
* 恢复指定音频播放
*
* @param r
*/
public void resume(int r) {
if (checkSoundPool()) {
mSoundPool.resume(r);
}
}
/**
* 恢复指定列表的音频
*
* @param list
*/
public void resume(List<Integer> list) {
if (checkSoundPool()) {
for (int r : list) {
mSoundPool.resume(r);
}
}
}
/**
* 恢复所有暂停的音频
*/
public void resumeAll() {
if (checkSoundPool()) {
mSoundPool.autoResume();
}
}
/**
* 卸载指定音频
*
* @param name
*/
public void unLoad(String name) {
if (checkSoundPool()) {
if (idCache.containsKey(name)) {
mSoundPool.unload(idCache.get(name));
idCache.remove(name);
}
}
}
/**
* 卸载指定列表的音频
*
* @param names
*/
public void unLoad(List<String> names) {
if (checkSoundPool()) {
for (String name : names) {
if (idCache.containsKey(name)) {
mSoundPool.unload(idCache.get(name));
idCache.remove(name);
}
}
}
}
/**
* 释放所有资源,如果想继续播放,需要重新加载资源
*/
public void release() {
if (checkSoundPool()) {
mSound = null;
mSoundPool.release();
idCache.clear();
}
}
private boolean checkSoundPool() {
if (mSoundPool != null) {
return true;
}
return false;
}
public interface LoadCompletion {
void onCompletion();
}
}
@@ -0,0 +1,42 @@
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);
}
}
@@ -1,55 +0,0 @@
package com.sw.platecabinet.utils
import com.sw.inbound.utils.SPUtil
import com.sw.inbound.utils.SPUtil.Companion.getInstance
import com.sw.platecabinet.GlobalKey
import com.sw.platecabinet.MyApp
object SpTool {
const val LAST_FACE_TIMESTAMP: String = "faceTimestamp"
const val PLATE_BOX_DATA: String = "plateBoxBind"
const val IS_FIRST_GET_FACE = "isFirstGetFace"
private var spUtil: SPUtil? = null
init {
spUtil = getInstance(MyApp.instance!!, "default_sp")
}
fun putString(key:String, value:String) {
spUtil?.put(key, value)
}
var lastFaceTimestamp: Long
get() = spUtil?.get<Long?>(SpTool.LAST_FACE_TIMESTAMP, 0L) ?: 0
set(timestamp) {
spUtil?.put(LAST_FACE_TIMESTAMP, timestamp)
}
// fun savePlateData(items: List<EquipmentUserInfo>) {
// val jsonData = Gson().toJson(items)
// putString(PLATE_BOX_DATA, jsonData)
// }
//
// fun getPlateData(): List<EquipmentUserInfo>? {
// val jsonData = spUtil?.get(PLATE_BOX_DATA, "")
// if (jsonData.isNullOrBlank()) {
// return null
// }
// return GsonUtils.fromJsonList(jsonData, EquipmentUserInfo::class.java)
// }
var firstGetFace: Boolean
get() = spUtil?.get(IS_FIRST_GET_FACE, true)?:true
set(value) {
spUtil?.boolean(IS_FIRST_GET_FACE, value)
}
var baseUrl: String
get() = spUtil?.get(GlobalKey.KEY_BASE_URL, "") ?: ""
set(value) {
spUtil?.put(GlobalKey.KEY_BASE_URL, value)
}
}
@@ -1,16 +0,0 @@
package com.sw.platecabinet.utils.mego
/**
* PLC 操作统一回调接口
*/
interface PlcCallback<T> {
/**
* 操作成功回调(主线程)
*/
fun onSuccess(result: T)
/**
* 操作失败回调(主线程)
*/
fun onError(message: String)
}
@@ -1,229 +0,0 @@
package com.sw.platecabinet.utils.mego
import android.content.Context
import android.os.Handler
import android.os.Looper
import com.megoai.plc_aar.SDK
import java.util.concurrent.atomic.AtomicBoolean
/**
* PLC 操作工具类(单例)
* 封装 SDK 所有接口,统一回调到主线程
*
* 注意:底层 PLC SDKFlutter MethodChannel 封装)的 Listener 只有 onMessage 一个回调,
* Flutter 侧 error()/notImplemented() 均为空实现,串口异常或引擎未就绪时不会回调。
* 因此本类对所有动作类、查询类接口统一加超时兜底:超时未响应则触发 onError,
* 保证上层回调链不会因“无响应”而永久挂起。
*/
class PlcHelper private constructor(context: Context) {
private val sdk: SDK = SDK.getInstance(context)
private val mainHandler = Handler(Looper.getMainLooper())
companion object {
@Volatile
private var instance: PlcHelper? = null
/**
* 获取单例实例
*/
fun getInstance(context: Context): PlcHelper {
return instance ?: synchronized(this) {
instance ?: PlcHelper(context.applicationContext).also { instance = it }
}
}
}
// ==================== 超时兜底封装 ====================
/**
* 包装 SDK 动作/查询调用:超时未响应时主动触发 onError。
*
* 背景:底层 SDK 的 Listener 仅在 Flutter 侧 success() 时回调 onMessage
* error()/notImplemented() 为空实现,串口异常时 Android 侧收不到任何回调,
* 会导致上层回调永久挂起。本方法用定时器兜底,并保证 onSuccess/onError 至多触发一次。
*
* @param timeoutMs 超时时长(毫秒)
* @param callback 上层回调(统一在主线程回调)
* @param action 实际发起 SDK 调用的逻辑,参数为经过防重入包装的“安全回调”,
* 内部所有成功/失败出口都应通过该回调触发
* @param <T> 回调结果类型
*/
private inline fun <T> withTimeout(
timeoutMs: Long,
callback: PlcCallback<T>,
crossinline action: (safeCallback: PlcCallback<T>) -> Unit
) {
// 标记回调是否已触发,保证 onSuccess/onError 至多执行一次
val fired = AtomicBoolean(false)
// 包装后的安全回调,所有出口都经过它,避免重复回调
val safeCallback = object : PlcCallback<T> {
override fun onSuccess(result: T) {
if (fired.compareAndSet(false, true)) callback.onSuccess(result)
}
override fun onError(message: String) {
if (fired.compareAndSet(false, true)) callback.onError(message)
}
}
// 超时定时器:到期未收到 SDK 回调则触发 onError
val timeoutRunnable = Runnable {
safeCallback.onError("PLC 操作超时(${timeoutMs}ms)未响应")
}
mainHandler.postDelayed(timeoutRunnable, timeoutMs)
// 发起实际 SDK 调用,所有成功/失败都通过 safeCallback 回调
action(object : PlcCallback<T> {
override fun onSuccess(result: T) {
// 成功后取消超时定时器
mainHandler.removeCallbacks(timeoutRunnable)
safeCallback.onSuccess(result)
}
override fun onError(message: String) {
// 失败同样取消超时定时器
mainHandler.removeCallbacks(timeoutRunnable)
safeCallback.onError(message)
}
})
}
// ==================== 连接与模式 ====================
/**
* 打开出盘串口(应用启动时调用)
*/
fun openSerialPort(callback: PlcCallback<String>) {
sdk.openPlcSerialPort { result ->
mainHandler.post { callback.onSuccess(result.toString()) }
}
}
/**
* 打开自动模式(应用启动时调用一次)
*/
fun openAutoMode(callback: PlcCallback<String>) {
withTimeout(5000, callback) { safe ->
sdk.actionOpenAutoMode { result ->
mainHandler.post { safe.onSuccess(result.toString()) }
}
}
}
/**
* 故障复位(出现 alarmCode 或补盘完成后调用)
*/
fun alarmReset(callback: PlcCallback<String>) {
withTimeout(5000, callback) { safe ->
sdk.actionAlarmReset { result ->
mainHandler.post { safe.onSuccess(result.toString()) }
}
}
}
// ==================== 出盘操作 ====================
/**
* 出盘(机械动作,超时 10s)
*/
fun outPlate(callback: PlcCallback<String>) {
withTimeout(10000, callback) { safe ->
sdk.actionOutPlate { result ->
mainHandler.post { safe.onSuccess(result.toString()) }
}
}
}
// ==================== 平台控制 ====================
/**
* 平台下降(开始补盘,超时 8s)
*/
fun platformDown(callback: PlcCallback<String>) {
withTimeout(8000, callback) { safe ->
sdk.actionPlatformDown { result ->
mainHandler.post { safe.onSuccess(result.toString()) }
}
}
}
/**
* 平台上升(补盘完成,超时 8s)
*/
fun platformUp(callback: PlcCallback<String>) {
withTimeout(8000, callback) { safe ->
sdk.actionPlatformDownStop { result ->
mainHandler.post { safe.onSuccess(result.toString()) }
}
}
}
/**
* 补盘完成返回营业(自动模式 + 故障复位,超时 5s)
*/
fun backToBusiness(callback: PlcCallback<String>) {
withTimeout(5000, callback) { safe ->
sdk.actionOpenAutoModeAndActionAlarmReset { result ->
mainHandler.post { safe.onSuccess(result.toString()) }
}
}
}
// ==================== 状态查询 ====================
/**
* 获取 PLC 状态(解析为 PlcStatus 对象,超时 3s
*/
fun getStatus(callback: PlcCallback<PlcStatus>) {
withTimeout(3000, callback) { safe ->
sdk.getPlcStatus { json ->
mainHandler.post {
if (json != null) {
safe.onSuccess(PlcStatus.Companion.fromJson(json.toString()))
} else {
safe.onError("获取状态失败")
}
}
}
}
}
/**
* 获取 PLC 状态(原始 JSON 字符串,超时 3s)
*/
fun getStatusJson(callback: PlcCallback<String>) {
withTimeout(3000, callback) { safe ->
sdk.getPlcStatus { json ->
mainHandler.post {
if (json != null) safe.onSuccess(json.toString())
else safe.onError("获取状态失败")
}
}
}
}
// ==================== 参数设置 ====================
/**
* 设置 PLC 参数(超时 3s)
* @param paramType 参数类型(如 "18FD" 表示主电机运行最长时间)
* @param paramValue 参数值(int,单位 0.1s
*/
fun setParams(paramType: String, paramValue: Int, callback: PlcCallback<String>) {
withTimeout(3000, callback) { safe ->
sdk.setPLCParams(
{ result -> mainHandler.post { safe.onSuccess(result.toString()) } },
paramType,
paramValue
)
}
}
// ==================== 生命周期 ====================
/**
* 释放资源,清除单例
*/
fun release() {
instance = null
}
}
@@ -1,34 +0,0 @@
package com.sw.platecabinet.utils.mego
import org.json.JSONObject
/**
* PLC 状态数据类
*/
data class PlcStatus(
val isEmptyPlate: Boolean = false, // 是否空盘
val isPlateformDown: Boolean = false, // 平台是否在下位
val isPlateformUp: Boolean = false, // 平台是否在上位
val isTakePlateInfrared: Boolean = false, // 取盘红外是否触发
val alarmCode: Int = 0 // 报警代码
) {
companion object {
/**
* 从 JSON 字符串解析状态
*/
fun fromJson(json: String): PlcStatus {
return try {
val obj = JSONObject(json)
PlcStatus(
isEmptyPlate = obj.optBoolean("isEmptyPlate", false),
isPlateformDown = obj.optBoolean("isPlateformDown", false),
isPlateformUp = obj.optBoolean("isPlateformUp", false),
isTakePlateInfrared = obj.optBoolean("isTakePlateInfrared", false),
alarmCode = obj.optInt("alarmCode", 0)
)
} catch (e: Exception) {
PlcStatus()
}
}
}
}
@@ -1,74 +1,75 @@
//package com.sw.platecabinet.viewmodel
//
//import androidx.lifecycle.ViewModel
//import androidx.lifecycle.viewModelScope
//import com.sw.plate.utils.ToastUtils
//import com.sw.platecabinet.model.response.ApiResponse
//import com.sw.platecabinet.network.ApiClient
//import com.sw.platecabinet.repository.RemoteRepository
//import kotlinx.coroutines.flow.MutableStateFlow
//import kotlinx.coroutines.flow.StateFlow
//import kotlinx.coroutines.launch
//import timber.log.Timber
//
//abstract class BaseViewModel() : ViewModel() {
// protected val repository = RemoteRepository(ApiClient.apiService)
//
// // 显示进度条
// private val _showLoading = MutableStateFlow<Boolean>(false)
// val showLoading: StateFlow<Boolean> = _showLoading
//
// /**
// * 带进度条的请求
// */
// protected fun launchWithLoading(block: suspend () -> Unit) {
// viewModelScope.launch {
// try {
// _showLoading.value = true
// block()
// } catch (e: Exception) {
// // 错误处理可被子类重写
// handleError(e)
// } finally {
// _showLoading.value = false
// }
// }
// }
//
// /**
// * 不带进度条的请求
// */
// protected fun launch(block: suspend () -> Unit) {
// viewModelScope.launch() {
// try {
// block()
// } catch (e: Exception) {
// // 错误处理可被子类重写
// handleError(e)
// }
// }
// }
//
// protected open fun parseResponse(response: ApiResponse<*>): Boolean {
// if (response.isSuccess()) {
// return true
// }
// Timber.d("msg = ${response.msg}, code = ${response.code}")
// ToastUtils.showToast("${response.msg}(${response.code})")
// return false
// }
//
//
// fun handleError(exception: Exception) {
// Timber.d("handleError ${exception.message}")
// }
//
//// fun parseEquipmentInfo(equipmentInfo: EquipmentInfo) {
////// GlobalData.appBaseUrl = equipmentInfo.appPackageUrl!!
////// GlobalData.appBaseUrl="https://vip.shuziweidao.com"
////// GlobalData.sdkKey = equipmentInfo.arcsoftSdkKey!!
////// GlobalData.appId = equipmentInfo.arcsoftAppId!!
////// GlobalData.activeKey = equipmentInfo.arcsoftActiveKey!!
////// GlobalData.restId = equipmentInfo.canteenId!!
//// }
//}
package com.sw.platecabinet.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.model.response.ApiResponse
import com.sw.platecabinet.model.response.EquipmentInfo
import com.sw.platecabinet.network.ApiClient
import com.sw.platecabinet.repository.RemoteRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
abstract class BaseViewModel() : ViewModel() {
protected val repository = RemoteRepository(ApiClient.apiService)
// 显示进度条
private val _showLoading = MutableStateFlow<Boolean>(false)
val showLoading: StateFlow<Boolean> = _showLoading
/**
* 带进度条的请求
*/
protected fun launchWithLoading(block: suspend () -> Unit) {
viewModelScope.launch {
try {
_showLoading.value = true
block()
} catch (e: Exception) {
// 错误处理可被子类重写
handleError(e)
} finally {
_showLoading.value = false
}
}
}
/**
* 不带进度条的请求
*/
protected fun launch(block: suspend () -> Unit) {
viewModelScope.launch() {
try {
block()
} catch (e: Exception) {
// 错误处理可被子类重写
handleError(e)
}
}
}
protected open fun parseResponse(response: ApiResponse<*>): Boolean {
if (response.isSuccess()) {
return true
}
Timber.d("msg = ${response.msg}, code = ${response.code}")
ToastUtils.showToast("${response.msg}(${response.code})")
return false
}
fun handleError(exception: Exception) {
Timber.d("handleError ${exception.message}")
}
fun parseEquipmentInfo(equipmentInfo: EquipmentInfo) {
// GlobalData.appBaseUrl = equipmentInfo.appPackageUrl!!
// GlobalData.appBaseUrl="https://vip.shuziweidao.com"
// GlobalData.sdkKey = equipmentInfo.arcsoftSdkKey!!
// GlobalData.appId = equipmentInfo.arcsoftAppId!!
// GlobalData.activeKey = equipmentInfo.arcsoftActiveKey!!
// GlobalData.restId = equipmentInfo.canteenId!!
}
}
@@ -106,7 +106,7 @@ class NetViewModelV2 : ViewModel() {
faceTimestamp = pageMaxTimestamp
}
}
SpTool.lastFaceTimestamp = faceTimestamp
SpTool.setLastFaceTimestamp(faceTimestamp)
callback(true, "查询完成")
}
}
@@ -208,9 +208,9 @@ class NetViewModelV2 : ViewModel() {
return@withContext
}
// 所有页拉取完毕,保存最终时间戳(只升不降)
val storedTimestamp = SpTool.lastFaceTimestamp
val storedTimestamp = SpTool.getLastFaceTimestamp()
if (faceTimestamp > storedTimestamp) {
SpTool.lastFaceTimestamp = faceTimestamp
SpTool.setLastFaceTimestamp(faceTimestamp)
}
try {
refreshFaceList()
@@ -1,62 +1,237 @@
//package com.sw.platecabinet.viewmodel
//
//import com.sw.platecabinet.model.response.UserFaceModel
//import kotlinx.coroutines.Dispatchers
//import kotlinx.coroutines.withContext
//import java.io.File
//
///**
// * 管理员操作viewmodel
// */
//class SettingViewModel : BaseViewModel() {
//
// companion object {
// const val PAGE_SIZE = 100
// }
//
// /**
// * 获取人脸数据
// */
// fun getFaceIncrementList(
// pageNo: Int = 1,
// pageSize: Int = PAGE_SIZE,
// timestamp: Long,
// block: (List<UserFaceModel>) -> Unit
// ) {
// launch {
// val response = repository.getFaceIncrementList(
// pageNum = pageNo.toLong(),
// pageSize = pageSize.toLong(),
// timestamp = timestamp
// )
package com.sw.platecabinet.viewmodel
import android.text.TextUtils
import com.sw.inbound.utils.DateTimeUtils
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.GlobalData.globalEquipmentCode
import com.sw.platecabinet.model.ErrorInfo
import com.sw.platecabinet.model.request.BindParam
import com.sw.platecabinet.model.request.EquipmentParam
import com.sw.platecabinet.model.request.SearchParam
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.SearchResult
import com.sw.platecabinet.model.response.UserFaceModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
/**
* 管理员操作viewmodel
*/
class SettingViewModel : BaseViewModel() {
// 设备绑定列表
// private val _equipmentList = MutableStateFlow<List<EquipmentUserInfo>>(emptyList())
// val equipmentList: StateFlow<List<EquipmentUserInfo>> = _equipmentList
private val _searchMemberList = MutableStateFlow<List<SearchResult.Member>>(emptyList())
val searchMemberList: StateFlow<List<SearchResult.Member>> = _searchMemberList
private val _searchUserInfo = MutableStateFlow<EquipmentUserInfo?>(null)
val searchUserInfo: StateFlow<EquipmentUserInfo?> = _searchUserInfo
var currentPage = 1
var isLoading = false
var canLoadMore = true
/**
* 绑定/解绑状态 <绑定/解绑, 错误信息,>
*/
// private val _bindStateChange = MutableStateFlow<Pair<Boolean?, ErrorInfo>>(null to ErrorInfo())
// val bindStateChange: StateFlow<Pair<Boolean?, ErrorInfo>> = _bindStateChange
/**
* 获取设备绑定用户信息列表
*/
// fun getEquipmentList(equipmentCode: String = globalEquipmentCode) {
// launchWithLoading {
// val param =
// EquipmentParam(appVersion = GlobalData.appVersion, equipmentCode = equipmentCode)
// val response = repository.getEquipmentList(param)
// if (parseResponse(response)) {
// withContext(Dispatchers.Default) {
// val list: List<UserFaceModel> = response.data ?: emptyList()
// block(list)
// _equipmentList.value = response.data ?: emptyList()
// }
// }
//// val list = mutableListOf<EquipmentUserInfo>()
//// for (i in 1 .. 30){
//// list.add(EquipmentUserInfo(id = i, equipmentBoxCode = i.toString()))
//// }
//// _equipmentList.value = list
// }
fun getEquipmentList(block: (List<EquipmentUserInfo>) -> Unit) {
launchWithLoading {
val response = repository.getEquipmentList()
if (parseResponse(response)) {
val list = response.data ?: emptyList()
block(list)
}
}
}
/**
* 搜索会员信息列表
*/
fun getSearchMemberList(param:String, pageNum: Int = 1, pageSize: Int = 50) {
if (TextUtils.isEmpty(param)) return
currentPage = pageNum
var name =""
var phone =""
val num = param.toIntOrNull()
if (num != null) {
phone = param
} else {
name = param
}
launch {
val searchParam = SearchParam(
// appVersion = GlobalData.appVersion,
name = name,
phone = phone,
pageNum = pageNum,
pageSize = pageSize
)
isLoading = true
val response = repository.searchUser(searchParam)
isLoading = false
if (parseResponse(response)) {
response.data?.let { list ->
if (pageNum == 1) {
_searchMemberList.value = list
} else {
_searchMemberList.value = _searchMemberList.value.plus(list)
}
if (list.size >= pageSize) currentPage++
}
}
}
}
// fun bindPlate(
// equipmentBoxCode: String,
// equipmentCode: String = globalEquipmentCode,
// memberId: Int,
// plateNumber: String
// ) {
// _bindStateChange.value = null to ErrorInfo()
// launchWithLoading {
// val bindParam = BindParam(
// appVersion = GlobalData.appVersion,
// equipmentBoxCode = equipmentBoxCode,
// equipmentCode = equipmentCode,
// memberId = memberId,
// plateNumber = plateNumber
// )
// val response = repository.bindEquipment(bindParam)
// if (parseResponse(response)) {
// var userInfo = response.data
// if (userInfo == null) {
// userInfo = EquipmentUserInfo()
// userInfo.equipmentBoxCode = equipmentBoxCode
// userInfo.plateNumber = plateNumber
// userInfo.equipmentCode = equipmentCode
// }
// _bindStateChange.value = true to ErrorInfo(equipmentUserInfo = userInfo)
// } else {
// _bindStateChange.value = true to ErrorInfo(response.code, response.msg.toString())
// }
// }
// }
//
// /**
// * 添加人脸数据
// */
// fun 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
// fun unbindPlate(
// equipmentBoxCode: String,
// equipmentCode: String = globalEquipmentCode
// ) {
// launchWithLoading {
// val bindParam = BindParam(
// appVersion = GlobalData.appVersion,
// equipmentBoxCode = equipmentBoxCode,
// equipmentCode = equipmentCode
// )
// val response = repository.bindEquipment(bindParam)
// if (parseResponse(response)) {
// _bindStateChange.value = false to ErrorInfo()
// } else {
// _bindStateChange.value = false to ErrorInfo(response.code, response.msg.toString())
// }
// val faceResp = repository.addUserFace(faceData, imageUrl)
// val status = parseResponse(faceResp)
// if (status.not()) {
// block(false, null)
// return@launch
// }
// block(true, faceResp.data)
// }
// }
//
//}
fun unbindPlate(id: Long?, block:(Pair<Boolean?, ErrorInfo>)-> Unit) {
block(null to ErrorInfo())
launchWithLoading {
val response = repository.plateUnbind(id)
if (parseResponse(response)) {
block(false to ErrorInfo())
} else {
block(false to ErrorInfo(response.code, response.msg.toString()))
}
}
}
fun bindPlate(bindParam: BindParam, block:(Pair<Boolean?, ErrorInfo>)-> Unit) {
launchWithLoading {
val response = repository.plateBind(bindParam)
if (parseResponse(response)) {
var userInfo = response.data
if (userInfo == null) {
userInfo = EquipmentUserInfo()
userInfo.equipmentBoxCode = bindParam.equipmentBoxCode
userInfo.plateNumber = bindParam.plateNumber
userInfo.equipmentCode = bindParam.equipmentCode
}
block(true to ErrorInfo(equipmentUserInfo = userInfo))
} else {
block(true to ErrorInfo(response.code, response.msg.toString()))
}
}
}
fun findByPlateNumber(
plateNumber: String,
equipmentCode: String = globalEquipmentCode
) {
_searchUserInfo.value = null
launchWithLoading {
// val bindParam = BindParam(plateNumber = plateNumber)
val response = repository.findByPlateNumber(plateNumber)
if (response.code == "00000") {
_searchUserInfo.value = response.data
} else {
_searchUserInfo.value = EquipmentUserInfo(
plateNumber = plateNumber,
equipmentCode = equipmentCode,
updateTime = DateTimeUtils.getDateTimeString()
)
}
}
}
val PAGE_SIZE = 100
/**
* 获取人脸数据
*/
fun getFaceIncrementList(
pageNo: Int = 1,
pageSize: Int = PAGE_SIZE,
timestamp: Long,
block: (List<UserFaceModel>) -> Unit
) {
launch {
val response = repository.getFaceIncrementList(
pageNum = pageNo.toLong(),
pageSize = pageSize.toLong(),
timestamp = timestamp
)
if (parseResponse(response)) {
withContext(Dispatchers.Default) {
val list: List<UserFaceModel> = response.data ?: emptyList()
block(list)
}
}
}
}
}
@@ -1,201 +1,181 @@
//package com.sw.platecabinet.viewmodel
//
//import androidx.lifecycle.viewModelScope
//import com.arcsoft.face.ErrorInfo
//import com.sw.inbound.utils.SPUtil
//import com.sw.plate.App
//import com.sw.plate.utils.Base64
//import com.sw.plate.utils.ToastUtils
//import com.sw.plate.utils.arcface.FaceApi
//import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
//import com.sw.platecabinet.GlobalData
//import com.sw.platecabinet.GlobalKey
//import com.sw.platecabinet.model.DeviceConfig
//import com.sw.platecabinet.model.response.UserFaceModel
//import com.sw.platecabinet.utils.SpTool
//import kotlinx.coroutines.Dispatchers
//import kotlinx.coroutines.launch
//import kotlinx.coroutines.withContext
//import timber.log.Timber
//import java.io.File
//
///**
// * 用户viewmodel
// */
//class UserViewModel : BaseViewModel() {
//
// companion object {
// const val PAGE_SIZE = 100
// }
//
// private val faceApi: FaceApi = FaceApi()
//
//
// fun getUserFace(
// pageNo: Int,
// pageSize: Int,
// callback: (List<UserFaceModel>) -> Unit
// ) {
package com.sw.platecabinet.viewmodel
import androidx.lifecycle.viewModelScope
import com.arcsoft.face.ErrorInfo
import com.sw.inbound.utils.SPUtil
import com.sw.plate.App
import com.sw.plate.utils.Base64
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.FaceApi
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.GlobalData.globalEquipmentCode
import com.sw.platecabinet.GlobalKey
import com.sw.platecabinet.model.DeviceConfig
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.utils.SpTool
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
/**
* 用户viewmodel
*/
class UserViewModel : BaseViewModel() {
companion object {
const val PAGE_SIZE = 100
}
// 通过人脸获取到的用户信息
private val _currentUserInfo = MutableStateFlow<EquipmentUserInfo?>(null)
val currentUserInfo: StateFlow<EquipmentUserInfo?> = _currentUserInfo
private val faceApi: FaceApi = FaceApi()
/**
* 获取token
*/
// fun generateToken(deviceId: String = "SWSN:88:12:AC:4E:D9:CC") {
// launchWithLoading {
// val response = repository.getUserFaceCache(pageNum = pageNo, pageSize = pageSize)
// val status = parseResponse(response)
// if (status) callback(response.data ?: emptyList())
// else emptyList<UserFaceModel>()
// }
// }
//
// /**
// * 获取人脸数据
// */
// fun getUserFaceCache(
// pageNo: Int = 1,
// pageSize: Int = PAGE_SIZE,
// callback: (Boolean, String?) -> Unit = { statue, msg -> }
// ) {
// var currentPageNo = pageNo
// launchWithLoading {
// val response = repository.getUserFaceCache(pageNum = pageNo, pageSize = pageSize)
// val status = parseResponse(response)
// if (status.not()) {
// callback(false, response.msg)
// return@launchWithLoading
// }
// // 获取成功一次后缓存状态
// SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true)
// val list: List<UserFaceModel> = response.data ?: emptyList()
// if (pageNo == 1 && list.isEmpty()) {
// callback(false, "暂无数据")
// return@launchWithLoading
// }
// val faceEntityList = list.map {
// FaceEntity(it.userId, null, Base64.decode(it.faceFeatureStr)).apply {
// userType = if (it.member == true) "1" else "2"
// }
// }
// withContext(Dispatchers.Default) {
// if (currentPageNo == 1) {
// //第一页数据删除本地数据库
// faceApi.clearFaceData()
// }
// faceApi.updateFaceData(pageNo, faceEntityList)
// if (list.size >= pageSize) {
// faceTimestamp = list.last().faceUpdateTimestamp ?: 0
// currentPageNo++
// getUserFaceCache(
// pageNo = currentPageNo,
// callback = callback
// )
// return@withContext
// }
// if (list.isNotEmpty()) {
// faceTimestamp = list.last().faceUpdateTimestamp ?: 0
// }
// SpTool.lastFaceTimestamp = faceTimestamp
// callback(true, "查询完成")
// }
// }
// }
//
// private var faceTimestamp = 0L
//
//
// /**
// * 激活人脸识别引擎
// */
// fun activeEngine() {
// Timber.d("activeEngine")
//
// faceApi.activeEngine(
// App.getContext(),
// GlobalData.appId,
// GlobalData.sdkKey,
// GlobalData.activeKey,
// object : FaceApi.ActiveCallback {
// override fun onSuccess(activeCode: Int) {
// Timber.d("activeEngine activeCode = $activeCode")
//// ThreadUtils.launch {
// viewModelScope.launch(Dispatchers.Main) {
// when (activeCode) {
// ErrorInfo.MOK -> {
// ToastUtils.showToast("激活引擎成功")
// }
//
// ErrorInfo.MERR_ASF_ALREADY_ACTIVATED -> {
//// ToastUtils.showToast("引擎已激活,无需再次激活")
// }
//
// else -> {
// ToastUtils.showToast("激活引擎失败($activeCode)")
// }
// }
// }
// }
//
// override fun onFail(e: Exception?) {
// viewModelScope.launch(Dispatchers.Main) {
// ToastUtils.showToast("激活引擎异常,${e?.message}")
// }
// }
// })
// }
//
//
// fun getDeviceConfig(block: (DeviceConfig?) -> Unit) {
//// Timber.tag(TAG).d("getDeviceConfig")
// launch {
// val response = repository.getDeviceConfig()
// val response = repository.generateToken(deviceId)
// if (parseResponse(response)) {
// block(response.data)
// } else {
// block(null)
// }
// }
// }
//
//
// /**
// * 获取人脸数据
// */
// fun getFaceIncrementList(
// pageNo: Int = 1,
// pageSize: Int = PAGE_SIZE,
// timestamp: Long,
// block: (List<UserFaceModel>) -> Unit
// ) {
// launch {
// val response = repository.getFaceIncrementList(
// pageNum = pageNo.toLong(),
// pageSize = pageSize.toLong(),
// timestamp = timestamp
// )
// if (parseResponse(response)) {
// withContext(Dispatchers.Default) {
// val list: List<UserFaceModel> = response.data ?: emptyList()
// block(list)
// // 缓存token
// SPUtil.getInstance().put(GlobalKey.KEY_TOKEN, response.data)
// // 首次运行获取人脸数据
// if (SPUtil.getInstance().get(GlobalKey.KEY_FIRST_RUN, false) != true) {
// getUserFaceCache()
// }
// }
// }
// }
//
// /**
// * 添加人脸数据
// */
// 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)
// }
// }
//
//}
/**
* 获取人脸数据
*/
fun getUserFaceCache(pageNo: Int = 1, pageSize: Int = PAGE_SIZE) {
var currentPageNo = pageNo
launchWithLoading {
val response = repository.getUserFaceCache(pageNum = pageNo, pageSize = pageSize)
if (parseResponse(response)) {
// 获取成功一次后缓存状态
SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true)
val list: List<UserFaceModel> = response.data ?: emptyList()
if (pageNo==1&&list.isEmpty()) {
return@launchWithLoading
}
val faceEntityList = list.map {
FaceEntity(it.userId, null, Base64.decode(it.faceFeatureStr))
}
withContext(Dispatchers.Default) {
faceApi.updateFaceData(pageNo, faceEntityList)
if (list.size >= pageSize) {
faceTimestamp = list.last().faceUpdateTimestamp?:0
currentPageNo++
getUserFaceCache(currentPageNo)
return@withContext
}
if (list.isNotEmpty()) {
faceTimestamp = list.last().faceUpdateTimestamp?:0
}
SpTool.setLastFaceTimestamp(faceTimestamp)
//activeEngine()
}
}
}
}
private var faceTimestamp = 0L
/**
* 激活人脸识别引擎
*/
fun activeEngine() {
Timber.d("activeEngine")
faceApi.activeEngine(
App.getContext(),
GlobalData.appId,
GlobalData.sdkKey,
GlobalData.activeKey,
object : FaceApi.ActiveCallback {
override fun onSuccess(activeCode: Int) {
Timber.d("activeEngine activeCode = $activeCode")
// ThreadUtils.launch {
viewModelScope.launch(Dispatchers.Main) {
when (activeCode) {
ErrorInfo.MOK -> {
ToastUtils.showToast("激活引擎成功")
}
ErrorInfo.MERR_ASF_ALREADY_ACTIVATED -> {
// ToastUtils.showToast("引擎已激活,无需再次激活")
}
else -> {
ToastUtils.showToast("激活引擎失败($activeCode)")
}
}
}
}
override fun onFail(e: Exception?) {
viewModelScope.launch(Dispatchers.Main) {
ToastUtils.showToast("激活引擎异常,${e?.message}")
}
}
})
}
/**
* 校验码登录
*/
fun loginWithPwd(loginParam: LoginParam) {
launchWithLoading {
val response = repository.equipmentBoxLogin(loginParam)
if (parseResponse(response)) {
_currentUserInfo.value = response.data
}
}
}
/**
* 通过用户id获取用户信息
*/
fun getUserInfoById(memberId: String?, action:(EquipmentUserInfo?)->Unit={}) {
_currentUserInfo.value = null
launchWithLoading {
val loginParam = LoginParam(faceId = memberId)
val response = repository.equipmentBoxLogin(loginParam)
if (parseResponse(response)) {
_currentUserInfo.value = response.data
action(response.data)
}
}
}
/**
* 重置用户信息
*/
fun resetUserInfo() {
Timber.d("resetUserInfo")
_currentUserInfo.value = null
}
fun getDeviceConfig(block: (DeviceConfig?) -> Unit) {
// Timber.tag(TAG).d("getDeviceConfig")
launch {
val response = repository.getDeviceConfig()
if (parseResponse(response)) {
block(response.data)
} else {
block(null)
}
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

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

Before

Width:  |  Height:  |  Size: 1.2 MiB

-11
View File
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#30000000">
<item>
<shape android:shape="rectangle">
<solid android:color="#5C77F7" />
<corners android:radius="8dp" />
</shape>
</item>
</ripple>
+8 -11
View File
@@ -1,21 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:width="1920px"
android:height="1080px">
<bitmap
android:scaleType="centerCrop"
android:src="@drawable/bg_collect_page3" />
android:width="600dp"
android:height="1024dp">
<bitmap android:src="@drawable/bg"/>
</item>
<item
android:width="506dp"
android:height="483dp"
android:width="337dp"
android:height="322dp"
android:gravity="top|center_horizontal"
android:top="150dp">
<bitmap
android:scaleType="fitCenter"
android:src="@drawable/img_init" />
android:top="112dp">
<bitmap android:src="@drawable/img_init" android:scaleType="fitCenter"/>
</item>
</layer-list>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/white"/>
<corners android:radius="12dp"/>
</shape>
+3 -3
View File
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="16dp" />
<corners android:radius="12dp" />
<stroke
android:width="3dp"
android:color="#FFC08A6F" />
android:width="2dp"
android:color="#FFCC99" />
</shape>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

@@ -1,5 +0,0 @@
<?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:orientation="vertical">
<!-- <ImageView-->
<!-- android:layout_width="337dp"-->
<!-- android:layout_height="322dp"-->
<!-- android:layout_gravity="center_horizontal"-->
<!-- android:layout_marginTop="112dp"-->
<!-- android:src="@drawable/img_init"-->
<!-- tools:ignore="ContentDescription"-->
<!-- android:visibility="invisible"/>-->
<ImageView
android:layout_width="337dp"
android:layout_height="322dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="112dp"
android:src="@drawable/img_init"
tools:ignore="ContentDescription"
android:visibility="invisible"/>
<TextView
android:id="@+id/btnInit"
-110
View File
@@ -1,110 +0,0 @@
<?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>
+22 -24
View File
@@ -4,7 +4,7 @@
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_collect_page3"
android:background="@drawable/bg"
android:orientation="vertical"
tools:context=".activity.InitActivity">
@@ -12,10 +12,10 @@
android:id="@+id/includeHeader"
layout="@layout/item_title_time" />
<Space
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
android:layout_weight="2"/>
<LinearLayout
android:layout_width="match_parent"
@@ -26,38 +26,36 @@
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="142dp"
android:src="@drawable/ic_init_text"
android:adjustViewBounds="true"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="60dp"
android:layout_height="345dp"
android:layout_marginTop="90dp"
android:src="@drawable/ic_init_img"
android:adjustViewBounds="true" />
<FrameLayout
android:id="@+id/takeButton"
<!-- <ImageView-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginTop="20dp"-->
<!-- android:src="@drawable/ic_init_press" />-->
<TextView android:id="@+id/takeButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="-35dp">
<TextView
android:layout_width="360dp"
android:layout_height="110dp"
android:text="点击取盘"
android:textColor="#FFCC99"
android:textSize="36sp"
android:layout_marginHorizontal="60dp"
android:layout_marginVertical="50dp"
android:paddingHorizontal="45dp"
android:gravity="center"
android:background="@drawable/btn_outline"
tools:ignore="HardcodedText" />
</FrameLayout>
android:layout_height="60dp"
android:text="点击取盘"
android:textColor="#FFCC99"
android:textSize="24sp"
android:paddingHorizontal="45dp"
android:gravity="center"
android:background="@drawable/btn_outline"
tools:ignore="HardcodedText" />
</LinearLayout>
<Space
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
+80 -106
View File
@@ -1,125 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg"
android:fitsSystemWindows="false"
tools:context=".activity.LoginByFaceActivity"
tools:ignore="HardcodedText,ContentDescription,PxUsage">
<FrameLayout
android:layout_width="776px"
android:layout_height="582px"
android:layout_gravity="center">
<TextureView
android:id="@+id/dual_camera_texture_preview_rgb"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.sw.plate.utils.arcface.FaceRectView
android:id="@+id/dual_camera_face_rect_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:src="@drawable/bg_face_area" />
android:orientation="vertical"
tools:context=".activity.LoginByFaceActivity">
<include
android:id="@+id/includeHeader"
layout="@layout/item_title_time" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="165dp"
android:layout_gravity="center_horizontal|top"
android:layout_marginTop="95dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:id="@+id/tv_tip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:maxLines="1"
android:text="请正视屏幕"
android:textColor="#FFF0C8B4"
android:textSize="48sp"
android:textStyle="bold" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:visibility="visible">
<TextView
android:id="@+id/tvSimilarValue"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="0dp"
android:maxLines="1"
android:textColor="@color/white"
android:textSize="12sp"
tools:text="0.6183次" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="0dp"
android:paddingTop="1dp">
<TextView
android:id="@+id/tv_to_init"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="0dp"
android:gravity="center"
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="#FFF0C8B4"
android:textSize="48sp"
tools:text="30" />
<TextureView
android:id="@+id/dual_camera_texture_preview_rgb"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible" />
<com.sw.plate.utils.arcface.FaceRectView
android:id="@+id/dual_camera_face_rect_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible" />
</FrameLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginTop="0dp"
android:background="@drawable/bg_face"
android:visibility="visible" />
<TextView
android:id="@+id/tv_tip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="129dp"
android:text="请正视屏幕"
android:textColor="#FFCC99"
android:textSize="36sp"
android:textStyle="bold" />
<LinearLayout
android:id="@+id/ll_to_pwd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="208dp"
android:layout_marginTop="730dp"
android:background="@drawable/btn_outline"
android:gravity="center"
android:paddingHorizontal="45dp"
android:paddingVertical="18dp"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="密码登录"
android:textColor="#FFCC99"
android:textSize="24sp" />
</LinearLayout>
<TextView
android:id="@+id/tv_to_init"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="185dp"
android:gravity="center"
android:padding="12dp"
android:textColor="#FFCC99"
android:textSize="36sp"
tools:text="30" />
</FrameLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/llCollectButton"
android:layout_width="wrap_content"
android:layout_height="265dp"
android:layout_gravity="center_horizontal|bottom"
android:gravity="center"
android:orientation="horizontal"
android:visibility="visible">
<TextView
android:id="@+id/btnRetry"
android:layout_width="360dp"
android:layout_height="110dp"
android:layout_marginHorizontal="20dp"
android:background="@drawable/btn_outline"
android:gravity="center"
android:text="再次重试"
android:textColor="#FFC08A6F"
android:textSize="36sp"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/btnCollect"
android:layout_width="360dp"
android:layout_height="110dp"
android:layout_marginHorizontal="20dp"
android:background="@drawable/btn_outline"
android:gravity="center"
android:text="新用户采集"
android:textColor="#FFC08A6F"
android:textSize="36sp"
tools:ignore="HardcodedText" />
</LinearLayout>
<ImageView
android:id="@+id/ivCollectPhoto"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="64dp"
android:adjustViewBounds="true"
android:visibility="gone"
tools:src="@drawable/logo_launcher" />
</FrameLayout>
</LinearLayout>

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