40 Commits
Author SHA1 Message Date
马增飞 582be4e9b7 绑定解绑功能调试;其它优化 2026-01-05 18:38:10 +08:00
马增飞 17d47a876f 增加配置接口;调试人脸识别;验证手机号密码登录和人脸登录功能; 2025-12-29 18:41:48 +08:00
马增飞 6c28260069 Merge remote-tracking branch 'refs/remotes/origin/main_新版_lvmeng_1222' into main_新版_lvmeng_1229 2025-12-29 09:57:05 +08:00
马增飞 7097bbf907 优化 2025-12-29 09:55:49 +08:00
lvmeng c046441d4e 优化 2025-12-26 14:16:01 +08:00
lvmeng 7a6c4012d1 餐盘柜接口改造 2025-12-26 14:05:41 +08:00
mazengfei 5e4dc4baa4 处理人脸数据不同步 2025-11-07 20:37:50 +08:00
mazengfei 4231c68cdc 添加方法 2025-11-07 15:18:15 +08:00
mazengfei 55ba11bf34 修改人脸同步逻辑 2025-11-07 10:55:48 +08:00
马增飞 abe594fb66 设备联调 2025-11-06 19:34:42 +08:00
lvmeng fb074c1aaa 修改开机初始化页面 2025-09-26 16:11:00 +08:00
lvmeng 73456046c5 增加初始化页面 2025-09-17 17:42:38 +08:00
lvmeng 7f56d4d1a1 修改待机也按钮名称 2025-09-17 15:45:00 +08:00
lvmeng 1658b6fe6f logo修改 2025-09-12 13:32:10 +08:00
lvmeng e9a366b52a 增加心跳处理人脸数据增减 2025-09-09 15:59:29 +08:00
lvmeng 3b492cb710 待机页面优化 2025-09-05 17:59:30 +08:00
lvmeng eaafeab1d8 增加余额不足判断 2025-09-05 15:29:43 +08:00
zxj 578a9aaced 添加注释 2025-08-22 16:28:20 +08:00
zxj cd375bfaf8 修复了登录成功后跳转界面错误问题 2025-08-21 15:50:05 +08:00
zxj 7a832ec40d 调整了初始化界面逻辑 2025-08-14 13:36:45 +08:00
zxj b83a442fe3 修改了配色,添加了解除绑定提示 2025-08-08 17:26:09 +08:00
zxj 2c10731b8d 修复了识别追踪信息功能 2025-08-08 10:50:11 +08:00
zxj 47f6e814a6 添加init界面,调整了认证后逻辑,调整了扫描枪扫描处理 2025-08-01 18:23:54 +08:00
zxj d20ee771b0 排序放入协程 2025-07-28 18:25:23 +08:00
zxj 6f18245c4f 密码登录后关闭密码界面 2025-07-28 18:05:06 +08:00
zxj 9a89fff9dd 工具类优化 2025-07-28 17:35:21 +08:00
zxj cc317117bc 修复了排序问题 2025-07-28 17:09:24 +08:00
zxj ca081f659b 初步实现功能 2025-07-28 15:47:55 +08:00
zxj 307fc70ae2 实现了垂直排序 2025-07-28 10:50:15 +08:00
zxj 8780f5d463 倒计时+1 2025-07-25 16:33:50 +08:00
zxj c0a628c51f 优化了键盘操作 2025-07-25 15:58:11 +08:00
zxj c2bae2f3e8 调整了餐盘柜3种情况处理 2025-07-25 13:35:17 +08:00
zxj c89efd2d6f 完善了功能 2025-07-25 10:36:30 +08:00
zxj 5b3773d277 调整了人脸识别界面 2025-07-24 18:02:42 +08:00
zxj 082011a939 调整了餐盘在不同设备提示 2025-07-24 08:40:36 +08:00
zxj 7fd2ccb960 调整了部分问题 2025-07-23 14:22:11 +08:00
zxj 5ad82eb224 添加了界面及逻辑 2025-07-22 15:48:12 +08:00
zxj 9d14b5499b 修改成java17 2025-07-18 17:42:05 +08:00
zxj 8e28e2f43c 填充了一些逻辑 2025-07-18 17:24:34 +08:00
zxj f97ef151c3 添加了界面及逻辑 2025-07-18 17:16:00 +08:00
144 changed files with 8121 additions and 225 deletions
Generated
+1
View File
@@ -0,0 +1 @@
SmartPlateCabinet
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>
+9 -1
View File
@@ -2,8 +2,16 @@
<project version="4"> <project version="4">
<component name="deploymentTargetSelector"> <component name="deploymentTargetSelector">
<selectionStates> <selectionStates>
<SelectionState runConfigName="SmartPlateCabinet.app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <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> </SelectionState>
</selectionStates> </selectionStates>
</component> </component>
+1 -1
View File
@@ -6,7 +6,7 @@
<GradleProjectSettings> <GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" /> <option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" /> <option name="gradleJvm" value="azul-17" />
<option name="modules"> <option name="modules">
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />
+2 -3
View File
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="zulu-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">
@@ -10,7 +9,7 @@
<component name="VisualizationToolProject"> <component name="VisualizationToolProject">
<option name="state"> <option name="state">
<ProjectState> <ProjectState>
<option name="scale" value="0.19390498954287422" /> <option name="scale" value="0.24414062499999997" />
</ProjectState> </ProjectState>
</option> </option>
</component> </component>
+23 -5
View File
@@ -6,6 +6,14 @@ plugins {
} }
android { android {
signingConfigs {
getByName("debug") {
storeFile = file("./swkey.jks")
storePassword = "123456"
keyAlias = "android"
keyPassword = "123456"
}
}
namespace = "com.sw.platecabinet" namespace = "com.sw.platecabinet"
compileSdk = 35 compileSdk = 35
@@ -14,13 +22,14 @@ android {
minSdk = 24 minSdk = 24
targetSdk = 35 targetSdk = 35
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ndk { ndk {
abiFilters.addAll(listOf("armeabi-v7a"/*, "arm64-v8a"*/)) abiFilters.addAll(listOf("armeabi-v7a"/*, "arm64-v8a"*/))
} }
setProperty("archivesBaseName", "zncpg_${versionName}")
} }
buildTypes { buildTypes {
@@ -62,11 +71,9 @@ dependencies {
implementation(libs.androidx.camera.view) implementation(libs.androidx.camera.view)
implementation(libs.androidx.camera.extensions) implementation(libs.androidx.camera.extensions)
// 权限申请
implementation(libs.accompanist.permissions)
// hilt注入 // hilt注入
implementation(libs.hilt.android) // implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler) // ksp(libs.hilt.android.compiler)
// retrofit网络请求 // retrofit网络请求
implementation(libs.retrofit) implementation(libs.retrofit)
@@ -82,4 +89,15 @@ dependencies {
implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.coroutines.android)
implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.fragment.ktx)
// implementation(libs.adapter.coroutines)
implementation("androidx.work:work-runtime-ktx:2.8.1")
implementation(libs.core)
implementation(libs.android.core)
implementation("org.greenrobot:eventbus:3.3.1")
} }
+34 -8
View File
@@ -8,18 +8,45 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
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 <application
android:name=".MyApp" android:name=".MyApp"
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_logo512"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_logo512"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.SmartPlateCabinet" android:theme="@style/Theme.SmartPlateCabinet"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true"
tools:targetApi="31"> tools:targetApi="31">
<!-- 注册BootReceiver,监听开机完成广播 -->
<receiver
android:name=".receiver.BootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<activity
android:name=".activity.UnBindDialogActivity"
android:exported="false"
android:theme="@style/DialogActivity" />
<activity
android:name=".activity.InitActivity"
android:exported="false"
android:launchMode="singleTask">
</activity>
<activity <activity
android:name=".activity.LoginByPwdActivity" android:name=".activity.LoginByPwdActivity"
android:exported="false" android:exported="false"
@@ -27,6 +54,10 @@
<activity <activity
android:name=".activity.LoginByFaceActivity" android:name=".activity.LoginByFaceActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTask"/>
<activity
android:name=".activity.DeviceInitActivity"
android:exported="true"
android:launchMode="singleTask"> android:launchMode="singleTask">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@@ -36,12 +67,7 @@
</activity> </activity>
<activity <activity
android:name=".activity.MainActivity" android:name=".activity.MainActivity"
android:exported="true"> android:exported="false">
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.MAIN" />-->
<!-- <category android:name="android.intent.category.LAUNCHER" />-->
<!-- </intent-filter>-->
</activity> </activity>
</application> </application>
@@ -0,0 +1,64 @@
package com.sw.platecabinet
object GlobalData {
/**
* 同一设备全局使用的设备编号
*/
// var globalEquipmentCode: String = "202501171634"
var globalEquipmentCode: String = "202507231144"
/**
* app版本号
*/
var appVersion: String = ""
/**
* 横排数量
*/
var arrayCross: Int = 2
/**
* 竖排数量
*/
var arrayVertical: Int = 11
/**
* 排列方式 0 垂直 1 水平
*/
var arrayMode: Int = 0
var appId = "Hkz1rBk6PZXbS8KwKr67K2eZtsz8bRoMHLg64bUdgCZj"
var sdkKey = "AabXs3sHM8UhhE7oCGf4LVMLrdkGb1nJNksbjjTdVt7k"
var activeKey ="085F-118G-Q3J6-35UX" //"085F-118G-Q391-53YL"
/**
* 具体业务baseurl
*/
var appBaseUrl: String = "https://dev.yixiong-tech.com:8081"
/**
* 设备id
*/
var deviceId: String = ""
var restId: String = ""
}
/**
* 全局常量
*/
object Constants {
/**
* 自动关闭时间
*/
const val AUTO_CLOSE_TIME: Long = 3
}
/**
* key
*/
object GlobalKey {
const val KEY_EQUIPMENT_INFO = "equipmentInfo"
const val KEY_TOKEN = "tokenKey"
const val KEY_FIRST_RUN = "firstRun"
const val KEY_USER_INFO = "userInfoKey"
const val PARAM_EQUIPMENT_INFO = "equipmentUserInfo"
}
+31 -2
View File
@@ -1,9 +1,38 @@
package com.sw.platecabinet package com.sw.platecabinet
import android.app.Application import android.util.Log
import android.content.Context
import com.sw.plate.App import com.sw.plate.App
import com.sw.plate.utils.AppUtil
import com.sw.platecabinet.utils.CrashHandler
import timber.log.Timber
class MyApp : App() { class MyApp : App() {
companion object {
const val DEBUG: Boolean = true
}
override fun onCreate() {
super.onCreate()
Timber.plant(Timber.DebugTree())
Timber.d("初始化")
initGlobalData()
// 初始化崩溃处理器
CrashHandler.init(this)
}
/**
* 初始化全局数据
*/
private fun initGlobalData() {
var deviceId = AppUtil.getUDID(this)
deviceId = "be154831-3466-3ba2-a2ea-57652c919fed"
Log.d("MyApp", "initialize: deviceId=$deviceId")
GlobalData.deviceId = deviceId
GlobalData.appVersion = AppUtil.getAppVersionCode(this).toString()
GlobalData.globalEquipmentCode = "202501171634"
// GlobalData.appBaseUrl = "http://192.168.1.201:14801"
}
} }
@@ -1,65 +1,473 @@
package com.sw.platecabinet.activity package com.sw.platecabinet.activity
import android.app.Dialog
import android.content.Context import android.content.Context
import android.os.Build
import android.os.Bundle 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.view.WindowManager
import android.widget.TextView
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.sw.inbound.utils.DateTimeUtils import com.sw.inbound.utils.DateTimeUtils
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.plate.utils.comn.SerialApi
import com.sw.plate.utils.comn.SerialPortManager
import com.sw.platecabinet.R
import com.sw.platecabinet.databinding.ItemTitleTimeBinding import com.sw.platecabinet.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.model.response.UserFaceModel
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.SettingViewModel
import com.sw.platecabinet.viewmodel.UserViewModel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
/** /**
* activity 基类 * activity 基类
*/ */
abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() { abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
protected lateinit var binding: VB protected lateinit var binding: VB
private set
private var headerBinding: ItemTitleTimeBinding? = null private var headerBinding: ItemTitleTimeBinding? = null
protected lateinit var context: Context protected lateinit var context: Context
private var timeJob: Job? = null private var timeJob: Job? = null
private var mDialogWaiting: CustomLoadingDialog? = null
private val permissionHelpers = mutableMapOf<Int, PermissionHelper>()
protected var keyEventHelper: ScanGunKeyEventHelper? = null
// 管理员对应的viewmodel
private val settingViewModel by viewModels<SettingViewModel>()
// 用户对应的viewModel
protected val viewModel by viewModels<UserViewModel>()
private var startTime: Long = 0
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
startTime = System.currentTimeMillis()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
context = this context = this
disableSystemUICompletely()
// 确保内容延伸到导航栏区域
WindowCompat.setDecorFitsSystemWindows(window, false)
//保持亮屏
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
binding = inflateViewBinding() binding = inflateViewBinding()
headerBinding = inflateTitleBinding() headerBinding = inflateTitleBinding()
setContentView(binding.root) setContentView(binding.root)
updateTime() updateTime()
registerDataChange()
initialize() initialize()
registerKeyEvent()
val durationTime = System.currentTimeMillis() - startTime
Timber.d("启动时间:$durationTime")
startFaceTask()
}
/**
* 监听扫描枪扫描事件
*/
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)
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (keyEventHelper != null) {
if (keyEventHelper!!.isScanGunEvent(event)) {
keyEventHelper!!.analysisKeyEvent(event)
return true
}
}
return super.dispatchKeyEvent(event)
} }
fun updateTime() { fun updateTime() {
headerBinding?.let { headerBinding?.let {
it.tvLeftDate.setClickListeners(
onDoubleClick = {
onLeftDoubleClick()
}
)
it.tvRightTime.setClickListeners(
onDoubleClick = {
onRightDoubleClick()
}
)
val scope = CoroutineScope(Dispatchers.Main) val scope = CoroutineScope(Dispatchers.Main)
timeJob = scope.launch { timeJob = scope.launch {
DateTimeUtils.realTimeChineseDateFlow() DateTimeUtils.realTimeChineseDateFlow()
.collect { (date, time) -> .collect { (date, time) ->
headerBinding!!.tvLeftDate.text = date it.tvLeftDate.text = date
headerBinding!!.tvRightTime.text = time it.tvRightTime.text = time
} }
} }
} }
} }
/**
* 左侧日期双击
*/
open fun onLeftDoubleClick() {
}
/**
* 右侧时间双击
*/
open fun onRightDoubleClick() {
MainActivity.start(context, pageType = PageType.SETTING_LIST)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
val helper = permissionHelpers[requestCode]
val allGranted = PermissionHelper.handlePermissionResult(
this,
requestCode,
permissions,
grantResults,
permissionHelper = helper
)
if (allGranted) {
permissionHelpers.remove(requestCode)
}
}
/**
* 注册PermissionHelper以便处理结果
*/
fun registerPermissionHelper(helper: PermissionHelper) {
permissionHelpers[helper.requestCode] = helper
}
override fun onResume() {
super.onResume()
// 关键点2:防止某些场景下系统栏恢复
enforceImmersiveMode()
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
disableSystemUICompletely()
}
}
private fun disableSystemUICompletely() {
Timber.d("disableSystemUICompletely")
// 禁用系统手势(Android 10+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.insetsController?.systemBarsBehavior =
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
// 全屏+隐藏导航栏(所有版本通用)
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
)
// 禁止窗口扩展至系统栏区域(彻底锁定)
window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
}
private fun enforceImmersiveMode() {
Timber.d("enforceImmersiveMode")
// 持续强制隐藏系统栏(防止手势触发)
window.decorView.postDelayed({
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
)
}, 100) // 延迟100ms确保覆盖手势触发
}
/**
* 显示等待提示框
*/
fun showWaitingDialog(tip: String?): 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() {
mDialogWaiting?.dismiss()
mDialogWaiting = null
}
protected abstract fun inflateViewBinding(): VB protected abstract fun inflateViewBinding(): VB
protected abstract fun inflateTitleBinding(): ItemTitleTimeBinding? protected abstract fun inflateTitleBinding(): ItemTitleTimeBinding?
protected abstract fun initialize() protected abstract fun initialize()
/**
* 注册数据监听
*/
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() { override fun onDestroy() {
timeJob?.cancel() timeJob?.cancel()
keyEventHelper?.onDestroy()
super.onDestroy() super.onDestroy()
} }
private val intervalExecutor by lazy { IntervalExecutor() }
private var faceTaskJob: Job? = null
// private val initialDelay = 5 * 60 * 1000L
// private val dealyMillis = 10 * 60 * 1000L
private val initialDelay = 1 * 60 * 1000L
private val dealyMillis = 1 * 60 * 1000L
private var taskPageNo = 1
fun startFaceTask() {
faceTaskJob =
intervalExecutor.startIntervalTaskWithInitialDelay(initialDelay, dealyMillis) {
getFaceIncrementList()
}
}
private var faceTimestamp = 0L
private fun getFaceIncrementList() {
val timestamp = SpTool.getLastFaceTimestamp()
if (timestamp == 0L) {
return
}
settingViewModel.getFaceIncrementList(
pageNo = taskPageNo,
timestamp = timestamp
) { list ->
runOnUiThread {
if (taskPageNo == 1 && list.isEmpty()) {
//未查询到增量数据
return@runOnUiThread
}
updateFaceData(list)
if (list.size >= settingViewModel.PAGE_SIZE) {
faceTimestamp = list.last().faceUpdateTimestamp?:0
taskPageNo++
getFaceIncrementList()
return@runOnUiThread
}
if (list.isNotEmpty()) {
faceTimestamp = list.last().faceUpdateTimestamp?:0
}
SpTool.setLastFaceTimestamp(faceTimestamp)
}
}
}
private val recognizeViewModel by viewModels<RecognizeViewModel>()
private fun updateFaceData(list: List<UserFaceModel>) {
Thread {
val faceList = mutableListOf<FaceEntity>()
try {
list.forEach { model ->
if (model.faceDeleted == true) {
//删除数据
FaceDatabase.getInstance(this).faceDao().deleteFaceById(model.userId)
} else {
//保存数据
val faceEntity = FaceEntity(
model.userId,
null,
Base64.decode(model.faceFeatureStr)
).also {
it.userType = "1"
}
faceList.add(faceEntity)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
try {
if (faceList.isNotEmpty()) {
FaceDatabase.getInstance(this).faceDao().insert(faceList)
}
recognizeViewModel.refreshFaceList()
} catch (e: Exception) {
e.printStackTrace()
}
}.start()
}
} }
@@ -0,0 +1,91 @@
package com.sw.platecabinet.activity
import android.content.Intent
import android.util.Log
import androidx.activity.viewModels
import com.sw.plate.utils.AppUtil
import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.databinding.ActivityDeviceInitBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.ext.gone
import com.sw.platecabinet.ext.invisible
import com.sw.platecabinet.viewmodel.SettingViewModel
/**
* 设备初始化界面
*/
class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
override fun inflateViewBinding(): ActivityDeviceInitBinding {
return ActivityDeviceInitBinding.inflate(layoutInflater)
}
override fun inflateTitleBinding(): ItemTitleTimeBinding? {
return null
}
override fun initialize() {
// val isSuccess = deviceViewModel.checkEquipmentInfo()
// if (isSuccess) {
// goLoginActivity()
// return
// }
// binding.ivQrCode.setImageBitmap(
// QRCodeUtil.generateQRCode(
// content = GlobalData.deviceId,
// size = 200
// )
// )
// binding.btnInit.setOnClickListener {
// deviceViewModel.getDeviceToken()
// }
// binding.btnInit.gone()
// binding.ivQrCode.invisible()
// binding.root.postDelayed({
// goLoginActivity()
// }, 1000)
showWaitingDialog("加载中……")
viewModel.getDeviceConfig { deviceConfig ->
runOnUiThread {
if (deviceConfig == null) {
hideWaitingDialog()
ToastUtils.showToast("获取设备配置数据失败")
hideWaitingDialog()
goLoginActivity()
return@runOnUiThread
}
GlobalData.appId = deviceConfig.arcsoftAppId ?: ""
GlobalData.sdkKey = deviceConfig.arcsoftSdkKey ?: ""
GlobalData.activeKey = deviceConfig.arcsoftActiveKey ?: ""
binding.root.postDelayed({
hideWaitingDialog()
goLoginActivity()
}, 500)
}
}
// goLoginActivity()
}
override fun registerDataChange() {
super.registerDataChange()
// lifecycleScope.launch {
// deviceViewModel.deviceInfoResult.collect { it ->
// if (it == true) {
// goLoginActivity()
// }
// }
// }
}
private fun goLoginActivity() {
val intent = Intent(this, LoginByFaceActivity::class.java)
startActivity(intent)
finish()
}
override fun onLeftDoubleClick() {
}
}
@@ -0,0 +1,42 @@
package com.sw.platecabinet.activity
import androidx.lifecycle.lifecycleScope
import com.sw.plate.utils.arcface.FaceApi
import com.sw.plate.utils.arcface.faceserver.FaceServer
import com.sw.platecabinet.databinding.ActivityInitBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
/**
* 待机界面
*/
class InitActivity : BaseActivity<ActivityInitBinding>() {
override fun inflateViewBinding(): ActivityInitBinding {
return ActivityInitBinding.inflate(layoutInflater)
}
override fun inflateTitleBinding(): ItemTitleTimeBinding? {
return binding.includeHeader
}
override fun initialize() {
binding.main.setOnClickListener {
finish()
}
// binding.takeButton.setOnClickListener {
//
// lifecycleScope.launch(Dispatchers.IO) {
// FaceApi().deleteByUserName("288")
// }
//
// finish()
// }
}
override fun onLeftDoubleClick() {
finish()
}
}
@@ -1,23 +1,59 @@
package com.sw.platecabinet.activity package com.sw.platecabinet.activity
import android.Manifest
import android.content.Intent import android.content.Intent
import android.graphics.Outline import android.graphics.Point
import android.hardware.Camera
import android.os.Build
import android.os.CountDownTimer
import android.util.DisplayMetrics
import android.view.View import android.view.View
import android.view.ViewOutlineProvider import android.view.ViewGroup
import androidx.camera.core.CameraSelector import android.view.ViewTreeObserver
import androidx.camera.core.Preview import androidx.activity.viewModels
import androidx.camera.lifecycle.ProcessCameraProvider import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat import androidx.lifecycle.Observer
import com.google.common.util.concurrent.ListenableFuture import com.arcsoft.face.ErrorInfo
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.ConfigUtil
import com.sw.plate.utils.arcface.ErrorCodeUtil
import com.sw.plate.utils.arcface.FaceRectTransformer
import com.sw.plate.utils.arcface.FaceRectView
import com.sw.plate.utils.arcface.FaceRectView.DrawInfo
import com.sw.plate.utils.arcface.PreviewConfig
import com.sw.plate.utils.arcface.camera.CameraListener
import com.sw.plate.utils.arcface.camera.DualCameraHelper
import com.sw.plate.utils.arcface.face.constants.LivenessType
import com.sw.plate.utils.arcface.face.model.FacePreviewInfo
import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration
import com.sw.plate.utils.arcface.faceserver.FaceServer
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.ActivityLoginFaceBinding import com.sw.platecabinet.databinding.ActivityLoginFaceBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.network.task.TaskManager
import com.sw.platecabinet.utils.PermissionHelper
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import timber.log.Timber import timber.log.Timber
/** /**
* 人脸识别 * 人脸识别
*/ */
class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>() { class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider> ViewTreeObserver.OnGlobalLayoutListener {
private val recognizeViewModel by viewModels<RecognizeViewModel>()
private var countDownTimer: CountDownTimer? = null
private val CAMERA_PERMISSION_REQUEST_CODE = 100
private val REQUIRED_PERMISSIONS: Array<String> = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
override fun inflateViewBinding(): ActivityLoginFaceBinding { override fun inflateViewBinding(): ActivityLoginFaceBinding {
return ActivityLoginFaceBinding.inflate(layoutInflater) return ActivityLoginFaceBinding.inflate(layoutInflater)
@@ -27,52 +63,386 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>() {
return binding.includeHeader return binding.includeHeader
} }
companion object {
private var instance: LoginByFaceActivity? = null
fun goInitActivity() {
instance?.goInitActivity()
}
}
override fun initialize() { override fun initialize() {
initView() instance = this
startCamera() viewModel.activeEngine()
initCountTime()
initArcViewModel()
initArcView()
openRectInfoDraw = true
recognizeViewModel.setDrawRectInfoTextValue(true)
//viewModel.generateToken()
// viewModel.activeEngine()
viewModel.getUserFaceCache()
binding.llToPwd.setOnClickListener { binding.llToPwd.setOnClickListener {
val intent = Intent(this, LoginByPwdActivity::class.java) val intent = Intent(this, LoginByPwdActivity::class.java)
startActivity(intent) startActivity(intent)
} }
// TODO: 测试请求
TaskManager.startTask()
EventBus.getDefault().register(this)
} }
private fun initView() { private fun checkCameraPermission() {
binding.previewView.outlineProvider = object : ViewOutlineProvider() { Timber.i("checkCameraPermission")
override fun getOutline(view: View, outline: Outline) { val permissionHelper =
// outline.setRoundRect(0, 0, view.width, view.height, 12f.dp) PermissionHelper.with(this, REQUIRED_PERMISSIONS, CAMERA_PERMISSION_REQUEST_CODE)
outline.setOval(0, 0, view.width, view.height) .onGranted {
Timber.d("checkCameraPermission 权限已允许")
// 权限已授予
recognizeViewModel.init()
initRgbCamera()
resumeCamera()
} }
}
binding.previewView.clipToOutline = true
}
private fun startCamera() {
cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
// 创建预览用例
val preview = Preview.Builder()
.build() .build()
.also { registerPermissionHelper(permissionHelper)
it.setSurfaceProvider(binding.previewView.surfaceProvider) permissionHelper.checkAndRequest()
} }
// 选择后置摄像头 @Subscribe(threadMode = ThreadMode.MAIN)
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA fun getFaceEntity(insertEntity: FaceEntity) {
Timber.tag("performSync").e("-time=%s", insertEntity.registerTime)
recognizeViewModel.addFace(insertEntity)
Timber.tag("performSync over").e("-time=%s", insertEntity.registerTime)
}
try { override fun onLeftDoubleClick() {
// 解绑所有用例 finish()
cameraProvider.unbindAll() }
// 绑定到生命周期 private var isRecognition = false
cameraProvider.bindToLifecycle( private var rgbCameraHelper: DualCameraHelper? = null
this, cameraSelector, preview private var rgbFaceRectTransformer: FaceRectTransformer? = null
private val livenessType = LivenessType.RGB
private var openRectInfoDraw = false
private fun initArcViewModel() {
recognizeViewModel.setLiveType(livenessType)
recognizeViewModel.ftInitCode.observe(this, Observer { ftInitCode: Int? ->
if (ftInitCode != ErrorInfo.MOK) {
val error: String? = getString(
R.string.specific_engine_init_failed, "ftEngine",
ftInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(ftInitCode!!)
) )
} catch (exc: Exception) { Timber.e("ftInitCode observe = $error")
Timber.e(exc, "Use case binding failed") ToastUtils.showToast(error)
} }
}, ContextCompat.getMainExecutor(this)) })
recognizeViewModel.frInitCode.observe(this, Observer { frInitCode: Int? ->
if (frInitCode != ErrorInfo.MOK) {
val error: String? = getString(
R.string.specific_engine_init_failed, "frEngine",
frInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(frInitCode!!)
)
Timber.e("frInitCode observe = $error")
ToastUtils.showToast(error)
}
})
recognizeViewModel.flInitCode.observe(this, Observer { flInitCode: Int? ->
if (flInitCode != ErrorInfo.MOK) {
val error: String? = getString(
R.string.specific_engine_init_failed, "flEngine",
flInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(flInitCode!!)
)
Timber.e("flInitCode observe = $error")
ToastUtils.showToast(error)
}
})
recognizeViewModel.recognizeConfiguration
.observe(this, Observer { recognizeConfiguration: RecognizeConfiguration? ->
Timber.i("recognizeConfiguration: observe = ${recognizeConfiguration.toString()}")
})
recognizeViewModel.recognizeNotice.observe(this, Observer { notice: String? ->
Timber.i("recognizeNotice observe notice = $notice")
})
recognizeViewModel.recognizeUserId.observe(this, Observer { userId: String? ->
Timber.i("recognizeUserId observe userId = $userId")
viewModel.getUserInfoById(memberId = userId)
})
recognizeViewModel.drawRectInfoText.observe(this, Observer { info ->
Timber.i("drawRectInfoText observe info = $info")
})
} }
private fun initArcView() {
//在布局结束后才做初始化操作
binding.dualCameraTexturePreviewRgb.getViewTreeObserver().addOnGlobalLayoutListener(this)
recognizeViewModel.getCompareResultList().getValue()
}
override fun onDestroy() {
Timber.d("onDestroy")
instance = null
if (rgbCameraHelper != null) {
rgbCameraHelper!!.release()
rgbCameraHelper = null
}
recognizeViewModel.destroy()
EventBus.getDefault().unregister(this)
super.onDestroy()
}
/**
* 调整View的宽高,使预览显示正常且采集框固定为380x380
*
* @param rgbPreview RGB预览View
* @param previewView 显示预览数据的view
* @param faceRectView 画框的view (380x380)
* @param previewSize 预览大小
* @param displayOrientation 相机旋转角度
* @param scale 缩放比例
* @return 调整后的LayoutParams
*/
private fun adjustPreviewViewSize(
rgbPreview: View,
previewView: View,
faceRectView: FaceRectView,
previewSize: Camera.Size,
displayOrientation: Int,
scale: Float
): ViewGroup.LayoutParams {
val layoutParams = previewView.layoutParams
val measuredWidth = previewView.measuredWidth
val measuredHeight = previewView.measuredHeight
var ratio = (previewSize.height.toFloat()) / previewSize.width.toFloat()
if (ratio > 1) {
ratio = 1 / ratio
}
if (displayOrientation % 180 == 0) {
layoutParams.width = measuredWidth
layoutParams.height = (measuredWidth * ratio).toInt()
} else {
layoutParams.height = measuredHeight
layoutParams.width = (measuredHeight * ratio).toInt()
}
if (scale < 1f) {
val rgbParam = rgbPreview.getLayoutParams()
layoutParams.width = (rgbParam.width * scale).toInt()
layoutParams.height = (rgbParam.height * scale).toInt()
} else {
layoutParams.width = (layoutParams.width * scale).toInt()
layoutParams.height = (layoutParams.height * scale).toInt()
}
val metrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(metrics)
if (layoutParams.width >= metrics.widthPixels) {
val viewRatio = layoutParams.width / (metrics.widthPixels.toFloat())
layoutParams.width = (layoutParams.width / viewRatio).toInt()
layoutParams.height = (layoutParams.height / viewRatio).toInt()
}
if (layoutParams.height >= metrics.heightPixels) {
val viewRatio = layoutParams.height / (metrics.heightPixels.toFloat())
layoutParams.width = (layoutParams.width / viewRatio).toInt()
layoutParams.height = (layoutParams.height / viewRatio).toInt()
}
previewView.setLayoutParams(layoutParams)
faceRectView.setLayoutParams(layoutParams)
return layoutParams
}
private fun initRgbCamera() {
val cameraListener: CameraListener = object : CameraListener {
override fun onCameraOpened(
camera: Camera,
cameraId: Int,
displayOrientation: Int,
isMirror: Boolean
) {
Timber.d("initRgbCamera Rgb onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation")
runOnUiThread({
val previewSizeRgb = camera.getParameters().getPreviewSize()
val layoutParams = adjustPreviewViewSize(
binding.dualCameraTexturePreviewRgb,
binding.dualCameraTexturePreviewRgb, binding.dualCameraFaceRectView,
previewSizeRgb, displayOrientation, 0.6f
)
Timber.d("initRgbCamera previewSizeRgb = ${previewSizeRgb.width}-${previewSizeRgb.height}")
Timber.d("initRgbCamera layoutParams = ${layoutParams.width}-${layoutParams.height}")
Timber.d(
"initRgbCamera isMirror = ${isMirror}, isDrawRgbRectHorizontalMirror = ${
ConfigUtil.isDrawRgbRectHorizontalMirror(
context
)
}, isDrawRgbRectVerticalMirror = ${
ConfigUtil.isDrawRgbRectVerticalMirror(
context
)
}"
)
rgbFaceRectTransformer = FaceRectTransformer(
previewSizeRgb.width,
previewSizeRgb.height,
layoutParams.width,
layoutParams.height,
90,
cameraId,
isMirror,
true,
true
)
recognizeViewModel.onRgbCameraOpened(camera)
recognizeViewModel.setRgbFaceRectTransformer(rgbFaceRectTransformer)
})
}
@RequiresApi(api = Build.VERSION_CODES.Q)
override fun onPreview(nv21: ByteArray?, camera: Camera?) {
if (!isRecognition) {
return
}
binding.dualCameraFaceRectView.clearFaceInfo()
val facePreviewInfoList: MutableList<FacePreviewInfo?>? =
recognizeViewModel.onPreviewFrame(nv21, true)
if (facePreviewInfoList != null && rgbFaceRectTransformer != null) {
drawPreviewInfo(facePreviewInfoList)
}
recognizeViewModel.clearLeftFace(facePreviewInfoList)
}
override fun onCameraClosed() {
Timber.i("initRgbCamera onCameraClosed: ")
}
override fun onCameraError(e: java.lang.Exception) {
Timber.i("initRgbCamera onCameraError: %s", e.message)
e.printStackTrace()
}
override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) {
Timber.i("initRgbCamera onCameraConfigurationChanged: threadName = ${Thread.currentThread().name}")
if (rgbFaceRectTransformer != null) {
rgbFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation
}
Timber.i("initRgbCamera onCameraConfigurationChanged: $cameraID $displayOrientation")
}
}
val previewConfig: PreviewConfig = recognizeViewModel.previewConfig
rgbCameraHelper = DualCameraHelper.Builder()
.previewViewSize(
Point(
binding.dualCameraTexturePreviewRgb.measuredWidth,
binding.dualCameraTexturePreviewRgb.measuredHeight
)
)
.rotation(windowManager.defaultDisplay.rotation)
.additionalRotation(0) // 角度
.previewSize(recognizeViewModel.loadPreviewSize())
.specificCameraId(previewConfig.rgbCameraId)
.isMirror(true)
.previewOn(binding.dualCameraTexturePreviewRgb)
.cameraListener(cameraListener)
.build()
rgbCameraHelper!!.init()
rgbCameraHelper!!.start()
}
/**
* 绘制RGB、IR画面的实时人脸信息
*
* @param facePreviewInfoList RGB画面的实时人脸信息
*/
private fun drawPreviewInfo(facePreviewInfoList: MutableList<FacePreviewInfo?>) {
if (rgbFaceRectTransformer != null) {
val rgbDrawInfoList: MutableList<DrawInfo?>? = recognizeViewModel.getDrawInfo(
facePreviewInfoList,
LivenessType.RGB,
openRectInfoDraw
)
// 识别成功
binding.dualCameraFaceRectView.drawRealtimeFaceInfo(rgbDrawInfoList)
}
}
fun openRectInfoDraw(view: View?) {
openRectInfoDraw = !openRectInfoDraw
recognizeViewModel.setDrawRectInfoTextValue(openRectInfoDraw)
}
override fun onGlobalLayout() {
Timber.d("onGlobalLayout")
binding.dualCameraTexturePreviewRgb.getViewTreeObserver().removeOnGlobalLayoutListener(this)
// 请求权限
checkCameraPermission()
}
private fun resumeCamera() {
isRecognition = true
if (rgbCameraHelper != null && rgbCameraHelper!!.isStopped) {
rgbCameraHelper!!.start()
}
}
override fun onResume() {
super.onResume()
resumeCamera()
viewModel.resetUserInfo()
countDownTimer?.let {
it.cancel()
it.start()
}
}
protected override fun onPause() {
pauseCamera()
super.onPause()
countDownTimer?.cancel()
}
private fun pauseCamera() {
isRecognition = false
recognizeViewModel.onPreviewFrame(ByteArray(1382400), true)
}
override fun handleLoginSuccess(equipmentUserInfo: EquipmentUserInfo, isAdmin: Boolean) {
super.handleLoginSuccess(equipmentUserInfo, isAdmin)
// goInitActivity()
}
fun initCountTime() {
val totalTimeInMillis = 30L * 1000
countDownTimer = object : CountDownTimer(totalTimeInMillis, 1000) {
override fun onTick(millisUntilFinished: Long) {
Timber.d("initCountTime onTick = ${(millisUntilFinished / 1000).toInt()}")
binding.tvToInit.text = "${(millisUntilFinished / 1000).toInt() + 1}"
}
override fun onFinish() {
goInitActivity()
}
}.start()
}
fun goInitActivity() {
val intent = Intent(context, InitActivity::class.java)
startActivity(intent)
}
} }
@@ -1,10 +1,19 @@
package com.sw.platecabinet.activity package com.sw.platecabinet.activity
import android.content.Intent import android.content.Intent
import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.databinding.ActivityLoginByPwdBinding import com.sw.platecabinet.databinding.ActivityLoginByPwdBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.dialog.BalanceNotEnoughDialog
import com.sw.platecabinet.model.request.LoginParam
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.utils.KeyboardUtils
/**
* 密码登录
*/
class LoginByPwdActivity : BaseActivity<ActivityLoginByPwdBinding>() { class LoginByPwdActivity : BaseActivity<ActivityLoginByPwdBinding>() {
override fun inflateViewBinding(): ActivityLoginByPwdBinding { override fun inflateViewBinding(): ActivityLoginByPwdBinding {
return ActivityLoginByPwdBinding.inflate(layoutInflater) return ActivityLoginByPwdBinding.inflate(layoutInflater)
} }
@@ -14,10 +23,40 @@ class LoginByPwdActivity : BaseActivity<ActivityLoginByPwdBinding>() {
} }
override fun initialize() { 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 { binding.tvFaceRec.setOnClickListener {
val intent = Intent(this, LoginByFaceActivity::class.java) val intent = Intent(this, LoginByFaceActivity::class.java)
startActivity(intent) 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()
}
} }
@@ -1,9 +1,27 @@
package com.sw.platecabinet.activity 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.ActivityMainBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding import com.sw.platecabinet.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
/**
* 主界面,内部嵌套framgnet使用
*/
class MainActivity : BaseActivity<ActivityMainBinding>() { class MainActivity : BaseActivity<ActivityMainBinding>() {
private lateinit var fragmentHelper: FragmentHelper
private var pageType = PageType.SETTING_LIST
private var callback: Callback? = null
override fun inflateViewBinding(): ActivityMainBinding { override fun inflateViewBinding(): ActivityMainBinding {
return ActivityMainBinding.inflate(layoutInflater) return ActivityMainBinding.inflate(layoutInflater)
@@ -13,7 +31,94 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
return binding.includeHeader return binding.includeHeader
} }
// 静态启动方法
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
) {
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)
}
}
override fun initialize() { override fun initialize() {
val param = intent.getStringExtra(PARAM_PAGE_TYPE)
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.UNBIND_PLATE -> UnBindPlateFragment.newInstance(equipmentUserInfo)
PageType.PLATE_CABINET_FULL -> PlateCabinetFullFragment()
}
fragmentHelper.addFragment(page)
}
override fun onLeftDoubleClick() {
if (pageType == PageType.SETTING_LIST) {
finish()
}
}
override fun onRightDoubleClick() {
} }
override fun handleScanKeyInfo(scanInfo: String) {
if (callback != null) {
callback!!.invoke(scanInfo)
} }
}
fun registerKeyEventInfo(callback: Callback) {
this.callback = callback
}
override fun registerDataChange() {
super.registerDataChange()
}
}
/**
* 界面类型
*/
enum class PageType {
/**
* 提示
*/
PLATE_TIP,
/**
* 设置列表
*/
SETTING_LIST,
/**
* 绑定餐盘
*/
BIND_PLATE,
/**
* 解绑餐盘
*/
UNBIND_PLATE,
/**
* 餐盘柜已满
*/
PLATE_CABINET_FULL
}
@@ -0,0 +1,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.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.databinding.ActivityUnbindDialogBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.ext.dp
import com.sw.platecabinet.model.response.EquipmentUserInfo
/**
* 解绑确认弹窗界面
*/
class UnBindDialogActivity : BaseActivity<ActivityUnbindDialogBinding>() {
private var equipmentUserInfo: EquipmentUserInfo? = null
override fun inflateViewBinding(): ActivityUnbindDialogBinding {
return ActivityUnbindDialogBinding.inflate(layoutInflater)
}
override fun inflateTitleBinding(): ItemTitleTimeBinding? {
return null
}
override fun initialize() {
window?.apply {
WindowCompat.setDecorFitsSystemWindows(this, false)
setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
attributes = attributes.apply {
y = 314.dp
width = (context.resources.displayMetrics.widthPixels * 0.8).toInt()
height = ViewGroup.LayoutParams.MATCH_PARENT
gravity = Gravity.CENTER
}
}
equipmentUserInfo =
intent.getParcelableExtra<EquipmentUserInfo>(GlobalKey.PARAM_EQUIPMENT_INFO)
if (equipmentUserInfo == null) return
binding.tvTipInfo.text = "即将解除【${equipmentUserInfo!!.name}】的餐盘"
binding.tvCancel.setOnClickListener {
finish()
}
binding.tvConfirm.setOnClickListener {
setResult(RESULT_OK)
finish()
}
}
override fun finish() {
super.finish()
overridePendingTransition(0, 0) // 确保退出也无动画
}
}
@@ -0,0 +1,92 @@
package com.sw.platecabinet.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.sw.platecabinet.R
/**
* 通用分页适配器(支持ViewBinding
*/
class GenericPageAdapter<T, VB : ViewBinding>(
private var pages: List<List<T>>,
private val pageLayoutId: Int,
private val itemBindingInflater: (LayoutInflater, ViewGroup, Boolean) -> VB,
private val itemBindCallback: VB.(item: T, position: Int) -> Unit
) : RecyclerView.Adapter<GenericPageAdapter<T, VB>.PageViewHolder>() {
fun updatePages(newPages: List<List<T>>) {
this.pages = newPages
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageViewHolder {
val view = LayoutInflater.from(parent.context).inflate(pageLayoutId, parent, false)
val holder = PageViewHolder(view)
// 在创建ViewHolder时设置ItemDecoration,只设置一次
holder.childRecyclerView.addItemDecoration(
GridSpacingItemDecoration(
spanCount = 2,
spacing = view.context.dpToPx(12),
includeEdge = true // 包含边缘间距
)
)
return holder
}
override fun onBindViewHolder(holder: PageViewHolder, position: Int) {
holder.bind(pages[position])
}
override fun getItemCount(): Int = pages.size
inner class PageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val childRecyclerView: RecyclerView = itemView.findViewById(R.id.childRecyclerView)
init {
childRecyclerView.layoutManager = GridLayoutManager(itemView.context, 2)
}
fun bind(items: List<T>) {
childRecyclerView.adapter =
GenericItemAdapter(items, itemBindingInflater, itemBindCallback)
}
}
}
/**
* 通用子项适配器(支持ViewBinding
*/
class GenericItemAdapter<T, VB : ViewBinding>(
private var items: List<T>,
private val bindingInflater: (LayoutInflater, ViewGroup, Boolean) -> VB,
private val bindCallback: VB.(item: T, position: Int) -> Unit,
) : RecyclerView.Adapter<GenericItemAdapter<T, VB>.ViewHolder>() {
inner class ViewHolder(private val binding: VB) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: T, position: Int) {
binding.bindCallback(item, position)
}
}
fun updateData(newItem: List<T>) {
this.items = newItem
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = bindingInflater(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position], position)
}
override fun getItemCount(): Int = items.size
}
@@ -0,0 +1,54 @@
package com.sw.platecabinet.adapter
import android.content.Context
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
/**
* 为RecyclerView添加间距的ItemDecoration
*/
class GridSpacingItemDecoration(
private val spanCount: Int, // 列数
private val spacing: Int, // 间距(dp)
private val includeEdge: Boolean // 是否包含边缘
) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val position = parent.getChildAdapterPosition(view) // item position
val column = position % spanCount // item column
if (includeEdge) {
// 左右间距计算
outRect.left = spacing - column * spacing / spanCount
outRect.right = (column + 1) * spacing / spanCount
// 第一行添加顶部间距
if (position < spanCount) {
outRect.top = spacing
}
outRect.bottom = spacing // 每行底部都添加间距
} else {
// 左右间距计算(不包含边缘)
outRect.left = column * spacing / spanCount
outRect.right = spacing - (column + 1) * spacing / spanCount
// 不是第一行时添加顶部间距
if (position >= spanCount) {
outRect.top = spacing
}
}
}
}
/**
* 将dp值转换为px
*/
fun Context.dpToPx(dp: Int): Int {
return (dp * resources.displayMetrics.density).toInt()
}
@@ -0,0 +1,36 @@
package com.sw.platecabinet.dialog
import android.app.Dialog
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.Window
import androidx.fragment.app.FragmentActivity
import com.sw.platecabinet.databinding.DialogBalanceNotEnoughBinding
import com.sw.platecabinet.ext.dp
open class BalanceNotEnoughDialog(
private var activity: FragmentActivity
) : Dialog(activity) {
private lateinit var binding: DialogBalanceNotEnoughBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
binding = DialogBalanceNotEnoughBinding.inflate(layoutInflater)
setContentView(binding.root)
window?.run {
attributes = attributes.apply {
width = 480.dp
}
setBackgroundDrawable(ColorDrawable())
setCancelable(false)
}
addListener()
}
private fun addListener() {
binding.tvConfirm.setOnClickListener { dismiss() }
}
}
@@ -0,0 +1,38 @@
package com.sw.platecabinet.dialog
import android.app.Dialog
import android.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() }
}
}
@@ -2,6 +2,7 @@ package com.sw.platecabinet.ext
import android.content.res.Resources import android.content.res.Resources
import android.util.TypedValue import android.util.TypedValue
import android.view.View
import androidx.annotation.Dimension import androidx.annotation.Dimension
/** /**
@@ -47,3 +48,19 @@ val Float.sp: Float
this, this,
Resources.getSystem().displayMetrics Resources.getSystem().displayMetrics
) )
fun formatNumber(num: String): String {
return num.padStart(2, '0')
}
fun View.visible() {
visibility = View.VISIBLE
}
fun View.invisible() {
visibility = View.INVISIBLE
}
fun View.gone() {
visibility = View.GONE
}
@@ -0,0 +1,37 @@
package com.sw.platecabinet.ext
/**
* 姓名脱敏处理
* 2个字:张*
* 多于2个字:张*某
*/
fun String?.maskName(): String {
if (this == null || this.isEmpty()) return ""
return when {
length == 2 -> "${this[0]}*"
length > 2 -> "${this[0]}*${this[length - 1]}"
else -> this // 1个字的情况原样返回
}
}
/**
* 手机号脱敏处理
* 将第4-7位替换为*
* 例如:138****1234
*/
fun String?.maskPhone(): String {
if (this == null || this.isEmpty()) return ""
if (this.length < 11) {
return this
}
val start = 3 // 第4位(索引从0开始)
val end = 6 // 第7位
val sb = StringBuilder(this)
for (i in start..end) {
sb.setCharAt(i, '*')
}
return sb.toString()
}
@@ -0,0 +1,33 @@
package com.sw.platecabinet.ext
import android.os.Handler
import android.os.Looper
import android.view.View
import android.widget.TextView
/**
* 设置 TextView 的双击和单击事件监听器
* @param doubleClickInterval 双击间隔时间(毫秒),默认 300ms
* @param onDoubleClick 双击回调
* @param onSingleClick 单击回调
*/
fun TextView.setClickListeners(
doubleClickInterval: Long = 300,
onDoubleClick: (View) -> Unit = {},
onSingleClick: (View) -> Unit = {}
) {
var clickCount = 0
val handler = Handler(Looper.getMainLooper())
setOnClickListener { view ->
clickCount++
handler.postDelayed({
if (clickCount == 1) {
onSingleClick(view)
} else if (clickCount == 2) {
onDoubleClick(view)
}
clickCount = 0 // 重置计数
}, doubleClickInterval)
}
}
@@ -0,0 +1,80 @@
package com.sw.platecabinet.fragment
import android.app.Dialog
import android.os.Bundle
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import com.sw.platecabinet.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() {
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,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = bindingInflater(inflater, container, false)
initialize()
registerDataChange()
return binding.root
}
/**
* 注册数据变化监听
*/
open fun registerDataChange() {
lifecycleScope.launch {
viewModel.showLoading.collect {
if (it) {
showWaitingDialog("")
} else {
hideWaitingDialog()
}
}
}
}
abstract fun initialize()
/**
* 显示等待提示框
*/
fun showWaitingDialog(tip: String?): Dialog? {
hideWaitingDialog()
val view = View.inflate(requireContext(), R.layout.dialog_waiting, null)
if (!TextUtils.isEmpty(tip)) (view.findViewById<View?>(R.id.tvTip) as TextView).text = tip
mDialogWaiting = CustomLoadingDialog(requireContext(), view, R.style.MyDialog)
mDialogWaiting!!.show()
mDialogWaiting!!.setCancelable(true)
return mDialogWaiting
}
/**
* 隐藏等待提示框
*/
fun hideWaitingDialog() {
mDialogWaiting?.dismiss()
mDialogWaiting = null
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
@@ -0,0 +1,317 @@
package com.sw.platecabinet.fragment
import android.annotation.SuppressLint
import android.os.Bundle
import android.text.Editable
import android.text.TextUtils
import android.text.TextWatcher
import android.view.inputmethod.EditorInfo
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.comn.SerialApi
import com.sw.plate.utils.comn.SerialPortManager
import com.sw.platecabinet.R
import com.sw.platecabinet.activity.MainActivity
import com.sw.platecabinet.activity.PageType
import com.sw.platecabinet.adapter.GenericItemAdapter
import com.sw.platecabinet.adapter.GridSpacingItemDecoration
import com.sw.platecabinet.adapter.dpToPx
import com.sw.platecabinet.databinding.FragmentBindPlateBinding
import com.sw.platecabinet.databinding.ItemSearchUserInfoBinding
import com.sw.platecabinet.dialog.UserBindRemindDialog
import com.sw.platecabinet.ext.maskName
import com.sw.platecabinet.ext.maskPhone
import com.sw.platecabinet.model.ErrorInfo
import com.sw.platecabinet.model.request.BindParam
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.SearchResult
import com.sw.platecabinet.utils.Debouncer
import com.sw.platecabinet.utils.KeyboardUtils
import com.sw.platecabinet.viewmodel.SettingViewModel
import com.sw.platecabinet.viewmodel.UserViewModel
import kotlinx.coroutines.launch
import timber.log.Timber
/**
* 餐盘柜绑定
*/
class BindPlateFragment : BaseFragment<FragmentBindPlateBinding>(
FragmentBindPlateBinding::inflate
) {
private lateinit var adapter: GenericItemAdapter<SearchResult.Member, ItemSearchUserInfoBinding>
internal val KEY_ACTION_TYPE = "actionType"
private var info: EquipmentUserInfo? = null
private var checkedItem: SearchResult.Member? = null
private var lastText = ""
private val debouncer = Debouncer(2000)
private val userViewModel by viewModels<UserViewModel>()
companion object {
@JvmStatic
fun newInstance(userInfo: EquipmentUserInfo?) =
BindPlateFragment().apply {
arguments = Bundle().apply {
putParcelable(KEY_ACTION_TYPE, userInfo)
}
}
}
@SuppressLint("SetTextI18n")
override fun initialize() {
arguments?.let { args ->
info = args.getParcelable(KEY_ACTION_TYPE)
info?.let {
binding.tvNum.text = "${it.equipmentName}-${it.equipmentBoxCode}"
if (!TextUtils.isEmpty(it.plateNumber)) {
// binding.etCode.text = Editable.Factory.getInstance().newEditable(it.plateNumber)
binding.etCode.apply {
setText(it.plateNumber)
setSelection(length())
}
}
}
}
initView()
initListener()
}
private fun initView() {
adapter = createAdapter()
binding.recyclerview.layoutManager = GridLayoutManager(context, 2)
// 添加间距装饰(12dp)
binding.recyclerview.addItemDecoration(
GridSpacingItemDecoration(
spanCount = 2,
spacing = requireContext().dpToPx(6),
includeEdge = false // 包含边缘间距
)
)
binding.recyclerview.adapter = adapter
binding.recyclerview.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
val totalItems = layoutManager.itemCount
if (!viewModel.isLoading && viewModel.canLoadMore
&& lastVisibleItem >= totalItems - 3
) {
viewModel.getSearchMemberList(
param = lastText,
pageNum = viewModel.currentPage
)
}
}
})
(activity as MainActivity).registerKeyEventInfo {
// binding.etCode.text.clear()
// binding.etCode.postDelayed({
// binding.etCode.text.clear()
// binding.etCode.text = Editable.Factory.getInstance().newEditable(it)
activity?.runOnUiThread {
if (binding.etUserInfo.isFocused) {
return@runOnUiThread
}
binding.etCode.apply {
if (it.length > 6) {
setText(it.substring(0, 6))
} else {
setText(it)
}
if (length() > 0) {
setSelection(length())
}
}
binding.etUserInfo.requestFocus()
}
// }, 50)
}
}
private fun initListener() {
binding.etCode.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) {
Timber.d("beforeTextChanged s = ${s.toString()}, start = $start, count = $count, after = $after")
}
override fun onTextChanged(
s: CharSequence?,
start: Int,
before: Int,
count: Int
) {
Timber.d("onTextChanged s = ${s.toString()}, start = $start, count = $count, before = $before")
}
override fun afterTextChanged(s: Editable?) {
Timber.d("afterTextChanged s = ${s.toString()}")
}
})
binding.etUserInfo.setOnEditorActionListener { _, actionId, keyEvent ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
val currentText = binding.etUserInfo.text.toString()
val containsNewlines = currentText.contains('\n') || currentText.contains('\r')
if (containsNewlines) {
Timber.d("非用户主动搜索")
true
}
debouncer.debounce {
// 执行搜索操作
viewModel.getSearchMemberList(param = currentText, pageNum = 1)
// 隐藏键盘
KeyboardUtils.hideKeyboard(requireActivity())
}
true // 表示已处理该事件
} else {
false // 未处理其他动作
}
}
binding.ivSearch.setOnClickListener {
debouncer.debounce {
// 执行搜索操作
viewModel.getSearchMemberList(
param = binding.etUserInfo.text.toString(),
pageNum = 1
)
// 隐藏键盘
KeyboardUtils.hideKeyboard(requireActivity())
}
}
binding.llBind.setOnClickListener {
if (equipmentBoxCode.isNullOrBlank().not()) {
showBindDialog()
return@setOnClickListener
}
if (info == null || info!!.equipmentCode?.isEmpty() == true || info!!.equipmentBoxCode?.isEmpty() == true) {
ToastUtils.showToast("传入的设备信息异常")
return@setOnClickListener
}
val plateNumber = binding.etCode.text.toString()
if (plateNumber.isEmpty()) {
ToastUtils.showToast("请输入餐盘号")
return@setOnClickListener
}
if (checkedItem == null) {
ToastUtils.showToast("请选择要绑定的会员")
return@setOnClickListener
}
if (checkedItem!!.id == null) {
ToastUtils.showToast("选中的会员信息异常")
return@setOnClickListener
}
val bindParam = BindParam(
equipmentId = info?.equipmentId,
equipmentCode = info?.equipmentCode,
equipmentBoxCode = info?.equipmentBoxCode,
faceId = checkedItem?.faceId,
plateNumber = plateNumber
)
viewModel.bindPlate(bindParam) { pair ->
updateBindState(pair)
}
KeyboardUtils.hideKeyboard(requireActivity())
}
binding.tvBack.setOnClickListener { activity?.finish() }
}
/**
* 监听数据变化
*/
override fun registerDataChange() {
super.registerDataChange()
lifecycleScope.launch {
viewModel.searchMemberList.collect {
adapter.updateData(it)
}
}
// lifecycleScope.launch {
// viewModel.bindStateChange.collect {
// updateBindState(it)
// }
// }
}
private fun createAdapter(): GenericItemAdapter<SearchResult.Member, ItemSearchUserInfoBinding> {
return GenericItemAdapter(
items = emptyList(),
bindingInflater = ItemSearchUserInfoBinding::inflate,
bindCallback = { item, position ->
this.tvName.text = item.name.maskName()
this.tvPhone.text = item.phone.maskPhone()
if (item.id != checkedItem?.id) {
this.llRoot.setBackgroundResource(R.drawable.grid_item_bind)
} else {
this.llRoot.setBackgroundResource(R.drawable.grid_item_unbind)
}
this.llRoot.setOnClickListener {
Timber.d("itemClick ${item.name}, position = $position")
checkedItem = item
adapter.notifyDataSetChanged()
//TODO 调用接口查询用户是否已绑盘
this@BindPlateFragment.equipmentBoxCode = null
(activity as? MainActivity)?.showWaitingDialog("查询中,请稍后……")
userViewModel.getUserInfoById(memberId = item.faceId) {
(activity as? MainActivity)?.hideWaitingDialog()
this@BindPlateFragment.equipmentBoxCode = it?.equipmentBoxCode
}
}
}
)
}
private var equipmentBoxCode:String? = null
private fun showBindDialog() {
UserBindRemindDialog(
activity = requireActivity(),
content = "您已绑定编号为${equipmentBoxCode}的柜子,请解绑后重试"
)
.show()
}
private fun updateBindState(pair: Pair<Boolean?, ErrorInfo>){
if (pair.first == null) return // 解绑状态过滤
val errorInfo = pair.second
if (errorInfo.isSuccess()) {
if (errorInfo.equipmentUserInfo == null) {
ToastUtils.showToast("数据异常")
return
}
// if (errorInfo.equipmentUserInfo.isOtherEquipment()) {
// MainActivity.start(
// requireContext(),
// pageType = PageType.PLATE_TIP,
// equipmentUserInfo = errorInfo.equipmentUserInfo,
// isAdmin = true
// )
// } else {
SerialApi.openPlate(
errorInfo.equipmentUserInfo.equipmentBoxCode!!.toInt(),
object : SerialPortManager.SendCallback {
override fun onSuccess() {
ToastUtils.showToast("绑定成功")
activity?.finish()
}
override fun onFail(e: Exception?) {
ToastUtils.showToast("绑定失败")
}
})
// }
} else {
ToastUtils.showToast(errorInfo.msg)
}
}
}
@@ -0,0 +1,32 @@
package com.sw.platecabinet.fragment
import android.os.CountDownTimer
import com.sw.platecabinet.Constants
import com.sw.platecabinet.databinding.FragmentPlateCabinetFullBinding
import timber.log.Timber
/**
* 餐盘柜已满
*/
class PlateCabinetFullFragment :
BaseFragment<FragmentPlateCabinetFullBinding>(FragmentPlateCabinetFullBinding::inflate) {
private var totalTimeInMillis: Long = Constants.AUTO_CLOSE_TIME * 1000
override fun initialize() {
Timber.d("initialize")
initCountTime()
}
fun initCountTime() {
object : CountDownTimer(totalTimeInMillis, 1000) {
override fun onTick(millisUntilFinished: Long) {
binding.tvAutoClose.text = "${(millisUntilFinished / 1000).toInt() + 1}秒后返回主屏"
}
override fun onFinish() {
activity?.finish()
}
}.start()
}
}
@@ -0,0 +1,76 @@
package com.sw.platecabinet.fragment
import android.content.Intent
import android.os.Bundle
import android.os.CountDownTimer
import com.sw.platecabinet.Constants
import com.sw.platecabinet.R
import com.sw.platecabinet.activity.InitActivity
import com.sw.platecabinet.databinding.FragmentPlateOpenBinding
import com.sw.platecabinet.model.response.EquipmentUserInfo
/**
* 餐盘柜提示界面
*/
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
companion object {
@JvmStatic
fun newInstance(userInfo: EquipmentUserInfo?, isAdmin: Boolean) =
PlateOpenFragment().apply {
arguments = Bundle().apply {
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))
}
}
}
initCountTime()
}
fun initCountTime() {
object : CountDownTimer(totalTimeInMillis, 1000) {
override fun onTick(millisUntilFinished: Long) {
binding.tvAutoClose.text = ((millisUntilFinished / 1000).toInt() + 1).toString()
}
override fun onFinish() {
activity?.finish()
if (!isAdmin) {
val intent = Intent(context, InitActivity::class.java)
startActivity(intent)
}
}
}.start()
}
}
@@ -0,0 +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.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.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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
/**
* 餐盘柜绑定的用户列表界面
*/
class SettingListFragment :
BaseFragment<FragmentSettingListBinding>(FragmentSettingListBinding::inflate) {
/**
* 需要横向滚动的adapter
*/
private var pageAdapter: GenericPageAdapter<EquipmentUserInfo, ItemBindViewBinding>? = null
/**
* 垂直滚动的adapter
*/
private var itemAdapter: GenericItemAdapter<EquipmentUserInfo, ItemBindViewBinding>? = null
override fun registerDataChange() {
super.registerDataChange()
// lifecycleScope.launch {
// viewModel.equipmentList.collect { it ->
// Timber.d("registerDateChange updateAdapter it = $it")
// updateAdapter(it)
// }
// }
}
override fun initialize() {
binding.mainRecyclerView.apply {
if (GlobalData.arrayCross > 2) {
layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
pageAdapter = createPageAdapter()
adapter = pageAdapter
} else {
layoutManager = GridLayoutManager(context, 2)
// 添加间距装饰(12dp)
addItemDecoration(
GridSpacingItemDecoration(
spanCount = 2,
spacing = requireContext().dpToPx(12),
includeEdge = true // 包含边缘间距
)
)
itemAdapter = createItemAdapter()
adapter = itemAdapter
}
}
}
override fun onResume() {
super.onResume()
viewModel.getEquipmentList{
updateAdapter(it)
}
}
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)
}
}
@@ -0,0 +1,95 @@
package com.sw.platecabinet.fragment
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.lifecycleScope
import com.sw.inbound.utils.DateTimeUtils
import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.GlobalKey
import com.sw.platecabinet.activity.UnBindDialogActivity
import com.sw.platecabinet.databinding.FragmentUnbindPlateBinding
import com.sw.platecabinet.ext.maskName
import com.sw.platecabinet.ext.maskPhone
import com.sw.platecabinet.model.ErrorInfo
import com.sw.platecabinet.model.response.EquipmentUserInfo
import kotlinx.coroutines.launch
/**
* 餐盘柜绑定
*/
class UnBindPlateFragment private constructor() : BaseFragment<FragmentUnbindPlateBinding>(
FragmentUnbindPlateBinding::inflate
) {
internal val ARG_PARAM1 = "param1"
private var info: EquipmentUserInfo? = null
companion object {
@JvmStatic
fun newInstance(param1: EquipmentUserInfo?) =
UnBindPlateFragment().apply {
arguments = Bundle().apply {
putParcelable(ARG_PARAM1, param1)
}
}
}
private val dialogLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
viewModel.unbindPlate(info?.id) { pair ->
updateBindState(pair)
}
}
}
override fun initialize() {
arguments?.let { args ->
info = args.getParcelable(ARG_PARAM1)
info?.let {
binding.tvNum.text = "${it.equipmentName}-${it.equipmentBoxCode}"
binding.tvPlateNumberValue.text = it.plateNumber
binding.tvNameValue.text = it.name.maskName()
binding.tvPhoneValue.text = it.phone.maskPhone()
val timeInfo = DateTimeUtils.parseDateTime(it.updateTime)
val timeAgo = DateTimeUtils.getTimeAgo(timeInfo)
binding.tvUpdateTimeValue.text = timeAgo
}
}
binding.llUnbind.setOnClickListener {
if (info == null) return@setOnClickListener
val intent = Intent(requireContext(), UnBindDialogActivity::class.java).apply {
putExtra(GlobalKey.PARAM_EQUIPMENT_INFO, info)
}
dialogLauncher.launch(intent)
requireActivity().overridePendingTransition(0, 0)
}
binding.tvBack.setOnClickListener {
activity?.finish()
}
}
override fun registerDataChange() {
super.registerDataChange()
// lifecycleScope.launch {
// viewModel.bindStateChange.collect {
// updateBindState(it)
// }
// }
}
private fun updateBindState(pair: Pair<Boolean?, ErrorInfo>){
if (pair.first == null) return // 绑定状态过滤
val errorInfo = pair.second
if (errorInfo.isSuccess()) {
ToastUtils.showToast("解绑成功")
// activity?.finish()
// 关闭宿主 Activity
requireActivity().finish()
requireActivity().overridePendingTransition(0, 0)
} else {
ToastUtils.showToast(errorInfo.msg)
}
}
}
@@ -0,0 +1,27 @@
package com.sw.platecabinet.model
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 DeviceConfig(
var arcsoftAppId: String? = null,
var arcsoftSdkKey: String? = null,
var arcsoftActiveKey: String? = null
)
data class CodeMsg(
val code: String? = "",
val msg: String? = ""
)
@@ -0,0 +1,36 @@
package com.sw.platecabinet.model.request
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
/**
*
* 设备与用户餐盘信息绑定解绑请求参数
*/
@Parcelize
data class BindParam(
/**
* 设备Id
*/
val equipmentId: String? = null,
/**
* 设备编号
*/
var equipmentCode: String? = "",
/**
*设备盒子编号
*/
val equipmentBoxCode: String? = null,
/**
*
* 会员Id
*/
val memberId: String? = null,
val faceId: String? = null,
/**
*餐盘编号
*/
val plateNumber: String? = null
) : Parcelable
@@ -0,0 +1,21 @@
package com.sw.platecabinet.model.request
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
/**
* 根据设备ID查询当前用户的餐盘信息,用户信息,设备盒子信息请求参数
*/
@Parcelize
data class EquipmentParam(
/**
* app版本号
*/
var appVersion: String = "",
/**
* 设备ID
*/
@SerializedName("equipmentCode")
var equipmentCode: String = "",
) : Parcelable
@@ -0,0 +1,30 @@
package com.sw.platecabinet.model.request
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
/**
* 登录参数
*/
@Parcelize
data class LoginParam(
/**
* 设备Id
*/
val equipmentId: Int? = null,
/**
* 会员信息
*/
// val memberId: String? = null,
val faceId: String? = null,
/**
* 密码
*/
val password: String? = null,
/**
* 手机号
*/
val phone: String? = null
) : Parcelable
@@ -0,0 +1,36 @@
package com.sw.platecabinet.model.request
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
/**
* (手机号/会员信息/会员卡号)模糊搜索
*/
@Parcelize
data class SearchParam(
// /**
// * app版本号
// */
// var appVersion: String = "",
// /**
// * 设备ID
// */
// @SerializedName("equipmentCode")
// var equipmentCode: String = "",
// @SerializedName("consumptionTime")
// val consumptionTime: List<String?>? = listOf(),
// @SerializedName("createTime")
// val createTime: List<String?>? = listOf(),
// @SerializedName("memberFrom")
// val memberFrom: Int? = 0,
// @SerializedName("pageNum")
var pageNum: Int? = 0,
// @SerializedName("pageSize")
var pageSize: Int? = 0,
// @SerializedName("param")
// val `param`: String? = "",
var name:String?=null,
var phone:String?=null
) : Parcelable
@@ -0,0 +1,12 @@
package com.sw.platecabinet.model.response
data class ApiResponse<T>(
val code: String,
// val success: Boolean? = false,
val msg: String? = "",
val data: T? = null,
// val result: T? = null,
) {
// fun isSuccess(): Boolean = code == 200
fun isSuccess(): Boolean = code == "00000"
}
@@ -0,0 +1,72 @@
package com.sw.platecabinet.model.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
data class EquipmentInfo(
@SerializedName("appPackageLocalUrl")
val appPackageLocalUrl: String? = "",
@SerializedName("appPackageUrl")
val appPackageUrl: String? = "",
@SerializedName("arcsoftActiveKey")
val arcsoftActiveKey: String? = "",
@SerializedName("arcsoftAppId")
val arcsoftAppId: String? = "",
@SerializedName("arcsoftSdkKey")
val arcsoftSdkKey: String? = "",
@SerializedName("arrayCross")
val arrayCross: Int? = 0,
@SerializedName("arrayMode")
val arrayMode: String? = "",
@SerializedName("arrayVertical")
val arrayVertical: Int? = 0,
@SerializedName("canteenId")
val canteenId: String? = "",
@SerializedName("canteenName")
val canteenName: String? = "",
@SerializedName("clientServerIp")
val clientServerIp: String? = "",
@SerializedName("createBy")
val createBy: String? = "",
@SerializedName("createTime")
val createTime: String? = "",
@SerializedName("customerName")
val customerName: String? = "",
@SerializedName("equipmentCode")
val equipmentCode: String? = "",
@SerializedName("equipmentName")
val equipmentName: String? = "",
@SerializedName("equipmentName_dictText")
val equipmentNameDictText: String? = "",
@SerializedName("id")
val id: String? = "",
@SerializedName("isSync")
val isSync: Int? = 0,
@SerializedName("mqName")
val mqName: String? = "",
@SerializedName("mqPassword")
val mqPassword: String? = "",
@SerializedName("orgCode")
val orgCode: String? = "",
@SerializedName("owningTrack")
val owningTrack: Int? = 0,
@SerializedName("owningTrackOrder")
val owningTrackOrder: Int? = 0,
@SerializedName("screenHtml")
val screenHtml: String? = "",
@SerializedName("serviceRequestAddress")
val serviceRequestAddress: String? = "",
@SerializedName("status")
val status: Int? = 0,
@SerializedName("sysOrgCode")
val sysOrgCode: String? = "",
@SerializedName("updateBy")
val updateBy: String? = "",
@SerializedName("updateTime")
val updateTime: String? = "",
@SerializedName("zhstServerIp")
val zhstServerIp: String? = ""
) : Parcelable
@@ -0,0 +1,90 @@
package com.sw.platecabinet.model.response
import android.os.Parcelable
import android.text.TextUtils
//import com.google.gson.annotations.SerializedName
import com.sw.platecabinet.GlobalData
import kotlinx.parcelize.Parcelize
/**
* 设备绑定用户信息
*/
@Parcelize
data class EquipmentUserInfo(
/**
* 主键
*/
val id: Long? = 0,
/**
*
* 设备Id
*/
val equipmentId: String? = "",
/**
* 设备编号
*/
var equipmentCode: String? = "",
/**
* 设备盒子编号
*/
var equipmentBoxCode: String? = "",
/**
* 会员Id
*/
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 == ""
}
}
@@ -0,0 +1,85 @@
package com.sw.platecabinet.model.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
/**
* 搜索会员信息结果
*/
@Parcelize
data class SearchResult(
@SerializedName("endRow")
val endRow: Int? = 0,
@SerializedName("hasNextPage")
val hasNextPage: Boolean? = false,
@SerializedName("hasPreviousPage")
val hasPreviousPage: Boolean? = false,
@SerializedName("isFirstPage")
val isFirstPage: Boolean? = false,
@SerializedName("isLastPage")
val isLastPage: Boolean? = false,
@SerializedName("list")
val list: List<Member>? = listOf(),
@SerializedName("navigateFirstPage")
val navigateFirstPage: Int? = 0,
@SerializedName("navigateLastPage")
val navigateLastPage: Int? = 0,
@SerializedName("navigatePages")
val navigatePages: Int? = 0,
@SerializedName("navigatepageNums")
val navigatepageNums: List<Int?>? = listOf(),
@SerializedName("nextPage")
val nextPage: Int? = 0,
@SerializedName("pageNum")
val pageNum: Int? = 0,
@SerializedName("pageSize")
val pageSize: Int? = 0,
@SerializedName("pages")
val pages: Int? = 0,
@SerializedName("prePage")
val prePage: Int? = 0,
@SerializedName("size")
val size: Int? = 0,
@SerializedName("startRow")
val startRow: Int? = 0,
@SerializedName("total")
val total: Int? = 0
) : Parcelable {
@Parcelize
data class Member(
// @SerializedName("cardBalance")
// val cardBalance: String? = "",
// @SerializedName("cardCode")
// val cardCode: String? = "",
// @SerializedName("consumptionCount")
// val consumptionCount: Int? = 0,
// @SerializedName("consumptionTotal")
// val consumptionTotal: String? = "",
// @SerializedName("createTime")
// val createTime: String? = "",
// @SerializedName("firstTopUpTime")
// val firstTopUpTime: String? = "",
// @SerializedName("id")
// val id: Int? = 0,
// @SerializedName("integralBalance")
// val integralBalance: Int? = 0,
// @SerializedName("lastConsumptionTime")
// val lastConsumptionTime: String? = "",
// @SerializedName("memberFrom")
// val memberFrom: Int? = 0,
// @SerializedName("name")
// val name: String? = "",
// @SerializedName("phone")
// val phone: String? = "",
// @SerializedName("rewardBalance")
// val rewardBalance: String? = "",
// @SerializedName("topUpBalance")
// val topUpBalance: String? = ""
val id: String? = "",
val faceId: String? = "",
val name: String? = "",
val phone: String? = "",
) : Parcelable
}
@@ -0,0 +1,30 @@
package com.sw.platecabinet.model.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
/**
* 用户人脸信息
*/
@Parcelize
data class UserFaceModel(
// @SerializedName("faceFeature")
// val faceFeature: String? = "",
// @SerializedName("faceFeatureString")
// val faceFeatureString: String? = "",
// @SerializedName("faceType")
// val faceType: String? = "",
// @SerializedName("userFaceId")
// val userFaceId: String? = "",
// @SerializedName("userId")
// val userId: String? = "",
// @SerializedName("updateTimeStamp")
// val updateTimeStamp: Long = 0,
val userId: String? = "",
val faceFeatureStr: String? = "",
val faceUpdateTimestamp:Long?=null,
val faceDeleted: Boolean?=false
) : Parcelable
@@ -0,0 +1,47 @@
package com.sw.platecabinet.network
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.MyApp
import com.sw.platecabinet.network.api.ApiService
import com.sw.platecabinet.network.interceptor.RequestInterceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import timber.log.Timber
import java.util.concurrent.TimeUnit
object ApiClient {
//private const val BASE_URL = "https://vip.shuziweidao.com"
// private const val DEVICE_BASE_URL = "http://device.shuziweidao.com:8889/"
private var DEVICE_BASE_URL = GlobalData.appBaseUrl
// private const val BASE_URL = "http://192.168.1.8:9092"
private const val TIME_OUT = 30L // 超时时间(秒)
val okHttpClient = OkHttpClient.Builder()
.connectTimeout(TIME_OUT, TimeUnit.SECONDS)
.readTimeout(TIME_OUT, TimeUnit.SECONDS)
.writeTimeout(TIME_OUT, TimeUnit.SECONDS)
.addNetworkInterceptor(HttpLoggingInterceptor(logger = {
Timber.d("okhttp logger ==>${it}")
}).apply {
level = if (MyApp.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
})
.addInterceptor(RequestInterceptor())
.build()
private val retrofit = Retrofit.Builder()
.baseUrl(DEVICE_BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
// .addCallAdapterFactory(CoroutineCallAdapterFactory()) // 协程适配器
.build()
val apiService: ApiService by lazy {
retrofit.create(ApiService::class.java)
}
}
@@ -0,0 +1,189 @@
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 retrofit2.http.Body
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
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>
): ApiResponse<List<UserFaceModel>?>
/**
* 获取设备配置数据
*/
@GET
suspend fun getDeviceConfig(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/getYxEquipmentByEquipmentCode"
): ApiResponse<DeviceConfig?>
}
@@ -0,0 +1,36 @@
package com.sw.platecabinet.network.interceptor
import com.sw.inbound.utils.SPUtil
import com.sw.plate.App
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.GlobalKey
import okhttp3.Interceptor
import okhttp3.Response
/**
* 请求拦截器
*/
class RequestInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val requestBuilder = originalRequest.newBuilder()
.header("Content-Type", "application/json")
.header("Accept", "application/json")
// .header("Authorization", "Bearer ${getToken()}")
// .header("Authorization", getToken())
.header("X-Access-Token", "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjYW50ZWVuSWQiOiJiZTE1NDgzMS0zNDY2LTNiYTItYTJlYS01NzY1MmM5MTlmZWQiLCJ0eXBlIjoiNCIsInVzZXJJZCI6IjAwMDAwMDAwMDAwMDAwMDAwMDEifQ.sN40cOC-O5WQFrF4IDUs8fFlkNdUKLbJt_rHyTsgYYM")
.header("X-DEVICE-CODE", GlobalData.deviceId)
.header("authorization", "57ee87183f2a4fa59683ec9ef41c8f5d")
val newRequest = requestBuilder.build()
return chain.proceed(newRequest)
}
private fun getToken(): String {
// 从本地获取token的逻辑
val spUtil = SPUtil.getInstance(context = App.getContext())
return spUtil.get(GlobalKey.KEY_TOKEN, "") as String
// return "eyJhbGciOiJIUzUxMiJ9.eyJpZCI6MTQ2LCJ1c2VyTmFtZSI6IjEzNjgxNDQ4ODU2IiwibmFtZSI6IuW-kOejiiIsInBhc3N3b3JkIjoiOTllOTQ1ZmVjZmZjNWIzNDI4MmUwNDRlODYyMzdjM2UxZjU5OWY5OCIsInNhbHQiOiI0NmEzMzUzYWU4OTA0MDYxYjMzODU5ZWNlYTBlMGE2NyIsInBob25lIjoiMTM2ODE0NDg4NTYiLCJzdGF0dXMiOjEsInVzZXJUeXBlIjoyLCJjcmVhdGVVc2VyTm8iOiIxNDEiLCJjcmVhdGVUaW1lIjoxNjI2OTQzNDE2MDAwLCJ1cGRhdGVVc2VyTm8iOiIxNDEiLCJ1cGRhdGVUaW1lIjoxNjI2OTQzNDE2MDAwLCJpc0RlbCI6ZmFsc2UsImVhSWQiOjk5LCJlYUlkTGlzdCI6Ijk5IiwiaXNTaG9wTWFuYWdlciI6dHJ1ZSwidXNlck5vIjoiMWY5Nzk5ZWMtODlkYi00MWYyLTk1YTEtY2UzNTA3Y2QyMTU2In0.f7wImPgBOYMV0AqRchnXGPkUWZN9dFJ9gLPsaB8uNldd21IfXLjJl8y-FiWVuVUvlwUvGpgqGDFR1JKj5H7amw"
}
}
@@ -0,0 +1,104 @@
package com.sw.platecabinet.network.task
import android.content.Context
import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.arcsoft.face.FaceEngine
import com.sw.inbound.utils.GsonUtils
import com.sw.plate.App
import com.sw.plate.utils.Base64
import com.sw.plate.utils.PrefUtils
import com.sw.plate.utils.arcface.FaceApi
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import com.sw.plate.utils.arcface.faceserver.FaceServer
import com.sw.platecabinet.network.ApiClient
import com.sw.platecabinet.repository.RemoteRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import timber.log.Timber
class HeartBeatTask(appContext: Context, workerParams: WorkerParameters) :
CoroutineWorker(appContext, workerParams) {
companion object {
private const val TAG = "HeartBeatTask"
}
override suspend fun doWork(): Result {
return try {
// 执行后台任务逻辑
performSync()
TaskManager.startTask()
Result.success()
} catch (e: Exception) {
e.printStackTrace()
Result.retry()
}
}
private val repository: RemoteRepository by lazy {
RemoteRepository(ApiClient.apiService)
}
private val faceApi: FaceApi = FaceApi()
private suspend fun performSync() {
// withContext(Dispatchers.IO) {
// val faceUpdateTimeStamp = PrefUtils.getLong(App.getContext(), "faceUpdateTimeStamp", 0)
// if (faceUpdateTimeStamp == 0L) {
// val timestamp = System.currentTimeMillis()
// PrefUtils.setLong(App.getContext(), "faceUpdateTimeStamp", timestamp)
// }
// val resp = repository.getHeartbeatInterface(
// PrefUtils.getLong(
// App.getContext(),
// "faceUpdateTimeStamp",
// 0
// )
// )
// Timber.tag(TAG).d("performSync: ${GsonUtils.toJson(resp)}")
// val list = resp.data
// if (list.isNullOrEmpty()) {
// return@withContext
// }
// val faceServer = FaceServer.getInstance()
// val faceEngine: FaceEngine? = faceServer.faceEngine
//
// list.forEachIndexed { index, it ->
// try {
// if (index == list.size - 1) {
// PrefUtils.setLong(
// App.getContext(),
// "faceUpdateTimeStamp",
// it.updateTimeStamp
// )
// }
// if (it.faceType == "1") {
// if (it.userId.isNullOrBlank().not()) {
// //userId非空,根据userId删除人脸数据
// faceApi.deleteByUserName(it.userId)
// faceEngine?.removeFaceFeature(it.userId.toInt())
// }
// } else if (it.faceType == "0") {
// //新增人脸数据
// val featureData = Base64.decode(it.faceFeature)
// val insertEntity = FaceEntity(it.userId, null, featureData)
// val faceId = faceApi.insert(insertEntity)
// insertEntity.faceId = faceId
//// faceServer.registerFaceFeatureInfoFromDb(insertEntity, faceEngine)
//
// EventBus.getDefault().post(insertEntity)
//// callback(insertEntity)
// }
// } catch (e: Exception) {
// e.printStackTrace()
// }
// }
// }
}
}
@@ -0,0 +1,40 @@
package com.sw.platecabinet.network.task
import androidx.lifecycle.LifecycleOwner
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.sw.plate.App
import java.util.concurrent.TimeUnit
object TaskManager {
private const val TASK_NAME = "SmartPlateCabinetTask"
// private var owner: LifecycleOwner?=null
// fun setOwner(owner: LifecycleOwner) {
// this.owner = owner
// }
fun startTask() {
val nextRequest = OneTimeWorkRequestBuilder<HeartBeatTask>()
.setInitialDelay(1, TimeUnit.MINUTES)
// .setInputData(inputData)
.build()
WorkManager.getInstance(App.getContext())
// .apply{
// getWorkInfoByIdLiveData(nextRequest.id)
// .observe(owner) { workInfo ->
// if (workInfo?.state == WorkInfo.State.SUCCEEDED) {
// val result = workInfo.outputData.getString("RESULT_KEY")
// // 使用返回的结果
// }
// }
// }
.enqueueUniqueWork(TASK_NAME, ExistingWorkPolicy.REPLACE, nextRequest)
}
fun cancelTask() {
WorkManager.getInstance(App.getContext())
.cancelUniqueWork(TASK_NAME)
}
}
@@ -0,0 +1,21 @@
package com.sw.platecabinet.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.sw.platecabinet.activity.DeviceInitActivity
import com.sw.platecabinet.activity.LoginByFaceActivity
import timber.log.Timber
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_BOOT_COMPLETED == intent.action) {
Timber.d("设备启动完成,开始执行自启动逻辑")
// val intent = Intent(context, LoginByFaceActivity::class.java)
val intent = Intent(context, DeviceInitActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
}
}
@@ -0,0 +1,58 @@
package com.sw.platecabinet.repository
import com.google.gson.JsonParseException
import com.sw.inbound.utils.GsonUtils
import com.sw.platecabinet.model.CodeMsg
import com.sw.platecabinet.model.response.ApiResponse
import retrofit2.HttpException
import timber.log.Timber
import java.io.IOException
import java.net.ConnectException
import java.net.SocketTimeoutException
import javax.net.ssl.SSLHandshakeException
abstract class BaseRepository {
suspend fun <T> safeApiCall(apiCall: suspend () -> ApiResponse<T>): ApiResponse<T> {
return try {
apiCall()
} catch (e: Exception) {
Timber.e("safeApiCall Exception: ${e.stackTraceToString()}")
when (e) {
is HttpException -> {
val jsonData = e.response()?.errorBody()?.string()
val codeMsg = GsonUtils.fromJson(jsonData, CodeMsg::class.java)
if (codeMsg?.msg.isNullOrBlank()) {
ApiResponse(code = "" + e.code(), msg = e.message())
} else {
ApiResponse(code = codeMsg?.code?:"", msg = codeMsg?.msg?:"")
}
}
is SocketTimeoutException -> {
ApiResponse(code = "-2", msg = "请求超时: ${e.message}")
}
is ConnectException -> {
ApiResponse(code = "-3", msg = "连接失败: ${e.message}")
}
is SSLHandshakeException -> {
ApiResponse(code = "-4", msg = "SSL握手失败: ${e.message}")
}
is JsonParseException -> {
ApiResponse(code = "-5", msg = "JSON解析错误: ${e.message}")
}
is IOException -> {
ApiResponse(code = "-6", msg = "网络IO错误: ${e.message}")
}
else -> {
ApiResponse(code = "-1", msg = "未知错误: ${e.message ?: "无错误信息"}")
}
}
}
}
}
@@ -0,0 +1,147 @@
package com.sw.platecabinet.repository
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 com.sw.platecabinet.network.api.ApiService
/**
* 远程数据处理
*/
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
// )
// }
// }
/**
* 获取人脸数据
*/
suspend fun getUserFaceCache(
pageNum: Int,
pageSize: Int = 100,
): ApiResponse<List<UserFaceModel>> {
return safeApiCall {
apiService.getUserFaceCache(
param = mapOf(
"pageNum" to pageNum,
"pageSize" to pageSize
)
)
}
}
/**
* 登录
*/
suspend fun 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,
pageSize: Long = 100L,
timestamp: Long
): ApiResponse<List<UserFaceModel>?> {
return safeApiCall {
apiService.getFaceIncrementList(
param = mapOf(
"pageNum" to pageNum,
"pageSize" to pageSize,
"timestamp" to timestamp
)
)
}
}
suspend fun getDeviceConfig(): ApiResponse<DeviceConfig?> {
return safeApiCall { apiService.getDeviceConfig() }
}
}
@@ -0,0 +1,237 @@
package com.sw.platecabinet.utils
import android.app.ActivityManager
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.Process
import com.sw.platecabinet.activity.InitActivity
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.PrintWriter
import java.io.StringWriter
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.system.exitProcess
/**
* 崩溃处理
*/
class CrashHandler private constructor(private val context: Context) :
Thread.UncaughtExceptionHandler {
companion object {
private const val TAG = "CrashHandler"
private const val CRASH_REPORTS_DIR = "crash_reports"
private const val LOG_LINES = 500 // 收集最近500行日志
@Volatile
private var instance: CrashHandler? = null
fun init(context: Context) {
if (instance == null) {
synchronized(CrashHandler::class.java) {
if (instance == null) {
instance = CrashHandler(context.applicationContext)
}
}
}
}
fun getCrashReportFiles(context: Context): Array<File> {
val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
return if (crashDir.exists() && crashDir.isDirectory) {
crashDir.listFiles { _, name -> name.endsWith(".log") } ?: emptyArray()
} else {
emptyArray()
}
}
fun clearCrashReports(context: Context) {
getCrashReportFiles(context).forEach { it.delete() }
}
}
private val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
init {
Thread.setDefaultUncaughtExceptionHandler(this)
}
override fun uncaughtException(thread: Thread, ex: Throwable) {
handleException(thread, ex)
// 如果系统提供了默认的异常处理器,则交给系统去结束程序
// 否则自己结束程序
defaultHandler?.uncaughtException(thread, ex) ?: run {
Process.killProcess(Process.myPid())
exitProcess(1)
}
}
/**
* 自动重启app
*/
private fun restartApp() {
// 延迟1秒后重启应用
Handler(Looper.getMainLooper()).postDelayed({
val intent = Intent(context, InitActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
val pendingIntent = PendingIntent.getActivity(
context, 0, intent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
)
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + 100, pendingIntent)
Process.killProcess(Process.myPid())
exitProcess(1)
}, 1000)
}
private fun handleException(thread: Thread, ex: Throwable) {
// 收集设备信息和异常信息
val crashInfo = collectCrashInfo(thread, ex)
// 保存日志文件
saveCrashInfoToFile(crashInfo)
// 这里可以添加其他处理逻辑,比如上传到服务器等
}
private fun collectCrashInfo(thread: Thread, ex: Throwable): String {
return buildString {
// 收集设备信息
collectDeviceInfo(this)
// 收集应用日志
append("\n\n").append(collectLogs())
// 收集线程和异常信息
append("\n\n========== Thread & Exception Info ==========\n")
append("Thread: ${thread.name}\n")
append("Stack Trace:\n")
val sw = StringWriter()
val pw = PrintWriter(sw)
ex.printStackTrace(pw)
var cause: Throwable? = ex.cause
while (cause != null) {
cause.printStackTrace(pw)
cause = cause.cause
}
pw.close()
append(sw.toString())
}
}
private fun collectDeviceInfo(sb: StringBuilder) {
sb.append("========== Device Info ==========\n")
try {
// 应用信息
val pm = context.packageManager
val pi = pm.getPackageInfo(context.packageName, 0)
sb.append("App Version: ${pi.versionName}_${pi.versionCode}\n")
// Android 版本信息
sb.append("OS Version: ${Build.VERSION.RELEASE}_${Build.VERSION.SDK_INT}\n")
// 设备信息
sb.append("Vendor: ${Build.MANUFACTURER}\n")
sb.append("Model: ${Build.MODEL}\n")
sb.append("CPU ABI: ${Build.SUPPORTED_ABIS[0]}\n")
// 其他信息
sb.append("Locale: ${Locale.getDefault()}\n")
sb.append(
"Current Time: ${
SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss",
Locale.getDefault()
).format(Date())
}\n"
)
// 内存信息
val memoryInfo = ActivityManager.MemoryInfo()
val activityManager =
context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
activityManager.getMemoryInfo(memoryInfo)
sb.append("Available Memory: ${memoryInfo.availMem / (1024 * 1024)}MB\n")
sb.append("Total Memory: ${memoryInfo.totalMem / (1024 * 1024)}MB\n")
sb.append("Low Memory: ${memoryInfo.lowMemory}\n")
} catch (e: Exception) {
Timber.e(e, "Error while collecting device info")
sb.append("Error while collecting device info: ${e.message}\n")
}
}
private fun collectLogs(): String {
return buildString {
append("========== Application Logs ==========\n")
try {
val process = Runtime.getRuntime().exec("logcat -d -v threadtime")
val reader = process.inputStream.bufferedReader()
val logLines = reader.readLines()
val start = kotlin.comparisons.maxOf(0, logLines.size - LOG_LINES)
logLines.subList(start, logLines.size).forEach {
append(it).append("\n")
}
} catch (e: IOException) {
Timber.e(e, "Error collecting logs")
append("Error collecting logs: ${e.message}\n")
}
}
}
private fun saveCrashInfoToFile(crashInfo: String) {
try {
val time = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.getDefault()).format(Date())
val fileName = "crash_$time.log"
val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
if (!crashDir.exists() && !crashDir.mkdirs()) {
Timber.tag(TAG).e("Failed to create crash report directory")
return
}
val crashFile = File(crashDir, fileName)
FileOutputStream(crashFile).use { it.write(crashInfo.toByteArray()) }
Timber.tag(TAG).d("Crash info saved to: ${crashFile.absolutePath}")
} catch (e: Exception) {
Timber.tag(TAG).e(e, "Error saving crash info to file")
}
}
/**
* 清理旧的崩溃日志
*/
fun cleanupOldCrashReports(maxAgeDays: Int = 7) {
val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
if (!crashDir.exists() || !crashDir.isDirectory) return
val now = System.currentTimeMillis()
val maxAgeMillis = maxAgeDays * 24 * 60 * 60 * 1000L
crashDir.listFiles()?.forEach { file ->
if (file.lastModified() < now - maxAgeMillis) {
file.delete()
}
}
}
}
@@ -2,9 +2,11 @@ package com.sw.inbound.utils
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import timber.log.Timber
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import kotlin.math.abs
/** /**
* 时间格式化工具类 * 时间格式化工具类
@@ -18,6 +20,13 @@ object DateTimeUtils {
return SimpleDateFormat("yyyy年M月d日 EEEE", Locale.CHINA).format(date) return SimpleDateFormat("yyyy年M月d日 EEEE", Locale.CHINA).format(date)
} }
/**
* 获取标准时间格式
*/
fun getDateTimeString(date: Date = Date()): String {
return SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA).format(date)
}
/** /**
* 获取带时间的完整中文格式(示例:2025年6月11日 星期三 14:30 * 获取带时间的完整中文格式(示例:2025年6月11日 星期三 14:30
*/ */
@@ -47,4 +56,65 @@ object DateTimeUtils {
delay(intervalMillis) delay(intervalMillis)
} }
} }
/**
* 解析日期时间字符串
* @param timeString 格式为 "yyyy-MM-dd HH:mm:ss" 的字符串
* @return Date 对象,解析失败返回 null
*/
fun parseDateTime(timeString: String?): Date? {
return try {
if (timeString == null) return null
val format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
format.parse(timeString)
} catch (e: Exception) {
// e.printStackTrace()
Timber.e(e.message)
null
}
}
/**
* 判断给定时间是否距离当前时间超过72小时
* @param timeInMillis 时间戳(毫秒)
* @return true 表示超过72小时,false 表示未超过
*/
fun isMoreThanHoursFromNow(timeInMillis: Long): Boolean {
val currentTime = System.currentTimeMillis()
val timeDifference = currentTime - timeInMillis
val hoursDifference = timeDifference / (1000 * 60 * 60) // 毫秒转小时
return hoursDifference >= 72
}
/**
* 获取时间间隔描述
*/
fun getTimeAgo(date: Date?): String {
if (date == null) return "未知时间"
val now = Date()
val diffMillis = now.time - date.time
// 如果是未来时间
if (diffMillis < 0) {
val futureHours = abs(diffMillis) / (1000 * 60 * 60)
return if (futureHours < 24) {
"未来 $futureHours 小时"
} else {
val days = futureHours / 24
"未来 $days"
}
}
// 过去时间
val hours = diffMillis / (1000 * 60 * 60)
return when {
hours < 1 -> "刚刚"
hours < 24 -> "${hours}小时前"
else -> {
val days = hours / 24
"${days}天前"
}
}
}
} }
@@ -0,0 +1,25 @@
package com.sw.platecabinet.utils
class Debouncer(private val delayMillis: Long) {
private var lastActionTime = 0L
/**
* 执行防抖操作
* @param action 要执行的操作
* @return Boolean 是否执行了操作 (true=已执行, false=被防抖)
*/
fun debounce(action: () -> Unit): Boolean {
val currentTime = System.currentTimeMillis()
if (currentTime - lastActionTime >= delayMillis) {
lastActionTime = currentTime
action()
return true
}
return false
}
// 重置防抖计时
fun reset() {
lastActionTime = 0L
}
}
@@ -0,0 +1,141 @@
package com.sw.platecabinet.utils
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
class FragmentHelper(
private val fragmentManager: FragmentManager,
private val containerId: Int
) {
/**
* 添加Fragment到容器中
* @param fragment 要添加的Fragment
* @param addToBackStack 是否添加到返回栈
* @param tag Fragment的标签
*/
fun addFragment(fragment: Fragment, addToBackStack: Boolean = false, tag: String? = null) {
fragmentManager.beginTransaction().apply {
add(containerId, fragment, tag)
if (addToBackStack) {
addToBackStack(tag)
}
commit()
}
}
/**
* 替换容器中的Fragment
* @param fragment 要替换的Fragment
* @param addToBackStack 是否添加到返回栈
* @param tag Fragment的标签
* @param animEnter 进入动画
* @param animExit 退出动画
* @param animPopEnter 弹出进入动画
* @param animPopExit 弹出退出动画
*/
fun replaceFragment(
fragment: Fragment,
addToBackStack: Boolean = true,
tag: String? = null,
animEnter: Int = 0,
animExit: Int = 0,
animPopEnter: Int = 0,
animPopExit: Int = 0
) {
fragmentManager.beginTransaction().apply {
if (animEnter != 0 || animExit != 0 || animPopEnter != 0 || animPopExit != 0) {
setCustomAnimations(animEnter, animExit, animPopEnter, animPopExit)
}
replace(containerId, fragment, tag)
if (addToBackStack) {
addToBackStack(tag)
}
commit()
}
}
/**
* 显示Fragment
* @param fragment 要显示的Fragment
*/
fun showFragment(fragment: Fragment) {
fragmentManager.beginTransaction()
.show(fragment)
.commit()
}
/**
* 隐藏Fragment
* @param fragment 要隐藏的Fragment
*/
fun hideFragment(fragment: Fragment) {
fragmentManager.beginTransaction()
.hide(fragment)
.commit()
}
/**
* 移除Fragment
* @param fragment 要移除的Fragment
*/
fun removeFragment(fragment: Fragment) {
fragmentManager.beginTransaction()
.remove(fragment)
.commit()
}
/**
* 返回上一个Fragment
* @return 是否成功返回
*/
fun popBackStack(): Boolean {
return fragmentManager.popBackStackImmediate()
}
/**
* 返回到指定的Fragment
* @param tag Fragment的标签
* @param inclusive 是否包含指定的Fragment
*/
fun popBackStackTo(tag: String, inclusive: Boolean = false): Boolean {
return fragmentManager.popBackStackImmediate(
tag,
if (inclusive) FragmentManager.POP_BACK_STACK_INCLUSIVE else 0
)
}
/**
* 获取当前显示的Fragment
*/
fun getCurrentFragment(): Fragment? {
return fragmentManager.findFragmentById(containerId)
}
/**
* 通过tag查找Fragment
*/
fun findFragmentByTag(tag: String): Fragment? {
return fragmentManager.findFragmentByTag(tag)
}
/**
* 清除所有Fragment
*/
fun clearAllFragments() {
// 清除所有回退栈中的Fragment
fragmentManager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
// 清除所有已添加的Fragment
fragmentManager.fragments.forEach { fragment ->
removeFragment(fragment)
}
}
/**
* 检查返回栈是否为空
*/
fun isBackStackEmpty(): Boolean {
return fragmentManager.backStackEntryCount == 0
}
}
@@ -0,0 +1,66 @@
package com.sw.platecabinet.utils
import kotlinx.coroutines.*
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
class IntervalExecutor {
/**
* 启动定时任务
* @param delayMillis 延迟时间(毫秒)
* @param action 要执行的方法
* @return Job 可用于取消任务
*/
fun startIntervalTask(delayMillis: Long, action: suspend () -> Unit): Job {
return CoroutineScope(Dispatchers.Default).launch {
while (isActive) {
action()
delay(delayMillis)
}
}
}
/**
* 启动定时任务(带初始延迟)
* @param initialDelay 初始延迟时间(毫秒)
* @param delayMillis 后续执行间隔(毫秒)
* @param action 要执行的方法
* @return Job 可用于取消任务
*/
fun startIntervalTaskWithInitialDelay(
initialDelay: Long,
delayMillis: Long,
action: suspend () -> Unit
): Job {
return CoroutineScope(Dispatchers.Default).launch {
delay(initialDelay)
while (isActive) {
action()
delay(delayMillis)
}
}
}
}
// 使用示例
fun main() = runBlocking {
val executor = IntervalExecutor()
// 示例1:每隔10秒执行一次
val job1 = executor.startIntervalTask(10000) {
println("定时任务执行: ${System.currentTimeMillis()}")
// 这里可以执行你的业务逻辑
}
// 示例2:先延迟5秒,然后每隔3秒执行一次
val job2 = executor.startIntervalTaskWithInitialDelay(5000, 3000) {
println("带初始延迟的定时任务: ${System.currentTimeMillis()}")
}
// 运行30秒后取消任务
delay(30000)
job1.cancel()
job2.cancel()
println("所有定时任务已取消")
}
@@ -0,0 +1,29 @@
package com.sw.platecabinet.utils
import android.app.Activity
import android.content.Context
import android.view.View
import android.view.inputmethod.InputMethodManager
/**
* 键盘工具类
*/
object KeyboardUtils {
/**
* 显示键盘
*/
fun showKeyboard(view: View) {
val imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT)
}
/**
* 隐藏键盘
*/
fun hideKeyboard(activity: Activity) {
val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
val view = activity.currentFocus ?: View(activity)
imm.hideSoftInputFromWindow(view.windowToken, 0)
}
}
@@ -0,0 +1,88 @@
package com.sw.platecabinet.utils
import com.sw.platecabinet.model.response.EquipmentUserInfo
import timber.log.Timber
import kotlin.math.ceil
/**
* 列表排序工具类
*/
object ListArrangementUtil {
/**
* page item水平排序
*/
fun horizontalSortPageItem(
list: List<EquipmentUserInfo>,
columns: Int,
defaultValue: EquipmentUserInfo = EquipmentUserInfo()
): List<EquipmentUserInfo> {
Timber.d("horizontalSortPageItem")
require(columns > 0) { "Columns must be positive" }
val filledList = list.padToMultiple(columns, defaultValue)
val chunks = filledList.chunked(columns)
return (0 until columns)
.windowed(2, 2, partialWindows = true)
.flatMap { group ->
when (group.size) {
2 -> chunks.flatMap { listOf(it[group[0]], it[group[1]]) }
1 -> chunks.flatMap {
listOf(it[group[0]], EquipmentUserInfo(equipmentBoxCode = null))
}.dropLast(1)
else -> error("Unexpected group size")
}
}
}
/**
* item 垂直排序
*/
fun verticalSortItem(list: List<EquipmentUserInfo>): List<EquipmentUserInfo> {
Timber.d("verticalSortItem")
val users = list.padToEvenSize()
val half = users.size / 2
return users.take(half)
.zip(users.drop(half)) { a, b -> listOf(a, b) }
.flatten()
}
/**
* page item垂直排序
*/
fun convertToColumnFirst(
original: List<EquipmentUserInfo>,
cols: Int
): List<EquipmentUserInfo> {
Timber.d("convertToColumnFirst")
require(cols > 0) { "Columns must be positive" }
val rows = ceil(original.size.toDouble() / cols).toInt()
return List(original.size) { pos ->
original.getOrNull(pos % rows * cols + pos / rows) ?: EquipmentUserInfo()
}
}
// 提取的扩展函数
private fun List<EquipmentUserInfo>.padToMultiple(
multiple: Int,
defaultValue: EquipmentUserInfo
): List<EquipmentUserInfo> {
return if (size % multiple != 0) {
this + List(multiple - (size % multiple)) { defaultValue }
} else {
this
}
}
private fun List<EquipmentUserInfo>.padToEvenSize(): List<EquipmentUserInfo> {
return if (size % 2 != 0) {
Timber.d("列表长度必须是偶数")
this + EquipmentUserInfo()
} else {
this
}
}
}
@@ -0,0 +1,155 @@
package com.sw.platecabinet.utils
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.provider.Settings
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
class PermissionHelper private constructor(
private val context: Context,
private val permissions: Array<String>,
internal val requestCode: Int,
private val rationale: String? = null,
private val onGranted: (() -> Unit)?
) {
companion object {
/**
* 创建权限请求构建器
*/
fun with(context: Context, permissions: Array<String>, requestCode: Int): Builder {
return Builder(context, permissions, requestCode)
}
/**
* 检查是否已授予所有权限
*/
fun checkPermissions(context: Context, permissions: Array<String>): Boolean {
return permissions.all { permission ->
ContextCompat.checkSelfPermission(
context,
permission
) == PackageManager.PERMISSION_GRANTED
}
}
/**
* 处理权限请求结果(供BaseActivity/BaseFragment调用)
* @return 是否所有权限都已授予
*/
fun handlePermissionResult(
context: Context,
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray,
permissionHelper: PermissionHelper? = null
): Boolean {
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
permissionHelper?.onGranted?.invoke()
return true
}
val permanentlyDeniedPermissions = mutableListOf<String>()
permissions.forEachIndexed { index, permission ->
if (grantResults[index] != PackageManager.PERMISSION_GRANTED) {
if (!ActivityCompat.shouldShowRequestPermissionRationale(
context as Activity,
permission
)
) {
permanentlyDeniedPermissions.add(permission)
}
}
}
return false
}
/**
* 跳转到应用设置页面
*/
fun openAppSettings(context: Context) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
if (context is Activity) {
context.startActivity(intent)
} else if (context is Fragment) {
context.startActivity(intent)
}
}
}
class Builder(
private val context: Context,
private val permissions: Array<String>,
private val requestCode: Int
) {
private var rationale: String? = null
private var onGranted: (() -> Unit)? = null
/**
* 设置权限说明
*/
fun setRationale(rationale: String): Builder {
this.rationale = rationale
return this
}
/**
* 设置权限授予回调
*/
fun onGranted(callback: () -> Unit): Builder {
this.onGranted = callback
return this
}
/**
* 构建PermissionHelper实例
*/
fun build(): PermissionHelper {
return PermissionHelper(context, permissions, requestCode, rationale, onGranted)
}
}
/**
* 检查并请求权限
*/
fun checkAndRequest() {
if (checkPermissions(context, permissions)) {
onGranted?.invoke()
} else {
requestPermissions()
}
}
private fun requestPermissions() {
val activity = context as? Activity ?: return
val shouldShowRationale = permissions.any { permission ->
ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
}
if (shouldShowRationale && rationale != null) {
AlertDialog.Builder(activity)
.setTitle("权限说明")
.setMessage(rationale)
.setPositiveButton("确定") { _, _ ->
doRequestPermissions(activity)
}
.setNegativeButton("取消", null)
.show()
} else {
doRequestPermissions(activity)
}
}
private fun doRequestPermissions(activity: Activity) {
ActivityCompat.requestPermissions(activity, permissions, requestCode)
}
}
@@ -0,0 +1,103 @@
package com.sw.platecabinet.utils
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.WriterException
import com.google.zxing.common.BitMatrix
import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import kotlin.apply
import kotlin.ranges.until
import kotlin.text.isEmpty
/**
* 二维码生成工具类
*/
object QRCodeUtil {
/**
* 生成二维码(默认大小)
* @param content 二维码内容
* @return 生成的二维码Bitmap
*/
@JvmOverloads
fun generateQRCode(content: String, size: Int = 500): Bitmap? {
return generateQRCode(content, size, Color.BLACK, Color.WHITE)
}
/**
* 生成二维码(自定义颜色)
* @param content 二维码内容
* @param size 二维码边长(像素)
* @param colorCode 二维码颜色
* @param backgroundColor 背景颜色
* @return 生成的二维码Bitmap
*/
fun generateQRCode(
content: String,
size: Int,
colorCode: Int,
backgroundColor: Int
): Bitmap? {
if (content.isEmpty()) {
return null
}
return try {
val hints = mutableMapOf<EncodeHintType, Any>().apply {
put(EncodeHintType.CHARACTER_SET, "UTF-8")
put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H) // 纠错级别
put(EncodeHintType.MARGIN, 1) // 边距
}
val bitMatrix = QRCodeWriter().encode(
content,
BarcodeFormat.QR_CODE,
size,
size,
hints
)
val pixels = IntArray(size * size).apply {
for (y in 0 until size) {
for (x in 0 until size) {
this[y * size + x] = if (bitMatrix.get(x, y)) colorCode else backgroundColor
}
}
}
Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888).apply {
setPixels(pixels, 0, size, 0, 0, size, size)
}
} catch (e: WriterException) {
e.printStackTrace()
null
}
}
/**
* 生成带Logo的二维码
* @param content 二维码内容
* @param size 二维码边长(像素)
* @param logo Logo Bitmap
* @return 带Logo的二维码Bitmap
*/
fun generateQRCodeWithLogo(content: String, size: Int, logo: Bitmap?): Bitmap? {
val qrCode = generateQRCode(content, size) ?: return null
logo ?: return qrCode
val logoSize = size / 5 // Logo大小约为二维码的1/5
val scaledLogo = Bitmap.createScaledBitmap(logo, logoSize, logoSize, false)
val offset = (size - logoSize) / 2
return Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888).apply {
val canvas = Canvas(this)
canvas.drawBitmap(qrCode, 0f, 0f, null)
canvas.drawBitmap(scaledLogo, offset.toFloat(), offset.toFloat(), null)
}
}
}
@@ -0,0 +1,20 @@
package com.sw.platecabinet.utils;
import com.sw.inbound.utils.SPUtil;
import com.sw.platecabinet.MyApp;
public class SpTool {
public static final String LAST_FACE_TIMESTAMP = "faceTimestamp";
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);
}
}
@@ -0,0 +1,91 @@
package com.sw.platecabinet.view;
import android.app.Dialog;
import android.content.Context;
import android.content.res.Resources;
import android.os.Build;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
/**
* 加载进度弹窗
*/
public class CustomLoadingDialog extends Dialog {
/**
* 宽高由布局文件中指定(但是最底层的宽度无效,可以多嵌套一层解决)
*/
public CustomLoadingDialog(Context context, View layout, int style) {
super(context, style);
setContentView(layout);
Window window = getWindow();
WindowManager.LayoutParams params = window.getAttributes();
params.gravity = Gravity.CENTER;
window.setAttributes(params);
}
/**
* 宽高由该方法的参数设置
*/
public CustomLoadingDialog(Context context, int width, int height, View layout,
int style) {
super(context, style);
// 设置内容
setContentView(layout);
// 设置窗口属性
Window window = getWindow();
WindowManager.LayoutParams params = window.getAttributes();
// 设置宽度、高度、密度、对齐方式
float density = getDensity(context);
params.width = (int) (width * density);
params.height = (int) (height * density);
params.gravity = Gravity.CENTER;
window.setAttributes(params);
}
/**
* 获取显示密度
*
* @param context
* @return
*/
public float getDensity(Context context) {
Resources res = context.getResources();
DisplayMetrics dm = res.getDisplayMetrics();
return dm.density;
}
private void fullScreenImmersive(View view) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_FULLSCREEN;
view.setSystemUiVisibility(uiOptions);
}
}
@Override
public void show() {
this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
super.show();
// fullScreenImmersive(getWindow().getDecorView());
// this.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
}
}
@@ -0,0 +1,76 @@
package com.sw.platecabinet.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.GlobalData
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!!
}
}
@@ -0,0 +1,68 @@
//package com.sw.platecabinet.viewmodel
//
//import com.sw.inbound.utils.GsonUtils
//import com.sw.inbound.utils.SPUtil
//import com.sw.platecabinet.GlobalData
//import com.sw.platecabinet.GlobalKey
//import com.sw.platecabinet.model.response.EquipmentInfo
//import kotlinx.coroutines.flow.MutableStateFlow
//import kotlinx.coroutines.flow.StateFlow
//import timber.log.Timber
//import kotlin.jvm.java
//
///**
// * 初始化的viewmodel
// */
//class DeviceViewModel : BaseViewModel() {
// private val _deviceInfoResult = MutableStateFlow<Boolean?>(null)
// val deviceInfoResult: StateFlow<Boolean?> = _deviceInfoResult
//
// /**
// * 获取token
// */
// fun getDeviceToken(deviceId: String = GlobalData.deviceId) {
// launchWithLoading {
// val response = repository.getDeviceToken(deviceId)
// if (parseResponse(response)) {
// val response1 = repository.getDeviceInfo(deviceId, response.result!!)
//
// if (parseResponse(response1)) {
// val equipmentInfo = response1.result
// if (equipmentInfo == null) return@launchWithLoading
// try {
// parseEquipmentInfo(equipmentInfo)
// SPUtil.getInstance()
// .put(GlobalKey.KEY_EQUIPMENT_INFO, GsonUtils.toJson(equipmentInfo))
// _deviceInfoResult.value = true
// } catch (e: Exception) {
// Timber.e(e)
// }
// }
// }
// }
// }
//
// /**
// * 检查缓存数据
// */
// fun checkEquipmentInfo(): Boolean {
// val equipmentInfoStr = SPUtil.getInstance().get(GlobalKey.KEY_EQUIPMENT_INFO, "")
// if (equipmentInfoStr == null) {
// Timber.e("获取缓存设备信息失败")
// return false
// }
// val equipmentInfo =
// GsonUtils.fromJson<EquipmentInfo>(equipmentInfoStr, EquipmentInfo::class.java)
// if (equipmentInfo == null) {
// Timber.e("解析缓存设备信息失败")
// return false
// }
// try {
// parseEquipmentInfo(equipmentInfo)
// return true
// } catch (e: Exception) {
// Timber.e(e)
// return false
// }
// }
//}
@@ -0,0 +1,237 @@
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)) {
// _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 unbindPlate(
// equipmentBoxCode: String,
// equipmentCode: String = globalEquipmentCode
// ) {
// launchWithLoading {
// val bindParam = BindParam(
// appVersion = GlobalData.appVersion,
// equipmentBoxCode = equipmentBoxCode,
// equipmentCode = equipmentCode
// )
// val response = repository.bindEquipment(bindParam)
// if (parseResponse(response)) {
// _bindStateChange.value = false to ErrorInfo()
// } else {
// _bindStateChange.value = false to ErrorInfo(response.code, response.msg.toString())
// }
// }
// }
fun unbindPlate(id: Long?, block:(Pair<Boolean?, ErrorInfo>)-> Unit) {
block(null to ErrorInfo())
launchWithLoading {
val response = repository.plateUnbind(id)
if (parseResponse(response)) {
block(false to ErrorInfo())
} else {
block(false to ErrorInfo(response.code, response.msg.toString()))
}
}
}
fun bindPlate(bindParam: BindParam, block:(Pair<Boolean?, ErrorInfo>)-> Unit) {
launchWithLoading {
val response = repository.plateBind(bindParam)
if (parseResponse(response)) {
var userInfo = response.data
if (userInfo == null) {
userInfo = EquipmentUserInfo()
userInfo.equipmentBoxCode = bindParam.equipmentBoxCode
userInfo.plateNumber = bindParam.plateNumber
userInfo.equipmentCode = bindParam.equipmentCode
}
block(true to ErrorInfo(equipmentUserInfo = userInfo))
} else {
block(true to ErrorInfo(response.code, response.msg.toString()))
}
}
}
fun findByPlateNumber(
plateNumber: String,
equipmentCode: String = globalEquipmentCode
) {
_searchUserInfo.value = null
launchWithLoading {
// val bindParam = BindParam(plateNumber = plateNumber)
val response = repository.findByPlateNumber(plateNumber)
if (response.code == "00000") {
_searchUserInfo.value = response.data
} else {
_searchUserInfo.value = EquipmentUserInfo(
plateNumber = plateNumber,
equipmentCode = equipmentCode,
updateTime = DateTimeUtils.getDateTimeString()
)
}
}
}
val PAGE_SIZE = 100
/**
* 获取人脸数据
*/
fun getFaceIncrementList(
pageNo: Int = 1,
pageSize: Int = PAGE_SIZE,
timestamp: Long,
block: (List<UserFaceModel>) -> Unit
) {
launch {
val response = repository.getFaceIncrementList(
pageNum = pageNo.toLong(),
pageSize = pageSize.toLong(),
timestamp = timestamp
)
if (parseResponse(response)) {
withContext(Dispatchers.Default) {
val list: List<UserFaceModel> = response.data ?: emptyList()
block(list)
}
}
}
}
}
@@ -0,0 +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.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.generateToken(deviceId)
// if (parseResponse(response)) {
// // 缓存token
// SPUtil.getInstance().put(GlobalKey.KEY_TOKEN, response.data)
// // 首次运行获取人脸数据
// if (SPUtil.getInstance().get(GlobalKey.KEY_FIRST_RUN, false) != true) {
// getUserFaceCache()
// }
// }
// }
// }
/**
* 获取人脸数据
*/
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.

After

Width:  |  Height:  |  Size: 90 KiB

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

After

Width:  |  Height:  |  Size: 113 KiB

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#30000000">
<item>
<shape android:shape="rectangle">
<solid android:color="#ff08C6DC" />
<corners android:radius="8dp" />
</shape>
</item>
</ripple>
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:width="600dp"
android:height="1024dp">
<bitmap android:src="@drawable/bg"/>
</item>
<item
android:width="337dp"
android:height="322dp"
android:gravity="top|center_horizontal"
android:top="112dp">
<bitmap android:src="@drawable/img_init" android:scaleType="fitCenter"/>
</item>
</layer-list>
@@ -4,5 +4,5 @@
<corners android:radius="12dp" /> <corners android:radius="12dp" />
<stroke <stroke
android:width="2dp" android:width="2dp"
android:color="#887872" /> android:color="#FFCC99" />
</shape> </shape>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="5dp" />
<solid android:color="@color/black" />
</shape>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="6dp" />
<solid android:color="#3C3D45" />
</shape>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="6dp" />
<solid android:color="#FFCC99" />
</shape>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#282931" />
<corners
android:bottomLeftRadius="12dp"
android:bottomRightRadius="12dp"
android:topLeftRadius="12dp"
android:topRightRadius="12dp" />
<stroke
android:width="2dp"
android:color="#34363E" />
</shape>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#ff3b3638" />
<corners
android:bottomLeftRadius="12dp"
android:bottomRightRadius="12dp"
android:topLeftRadius="12dp"
android:topRightRadius="12dp" />
<stroke
android:width="2dp"
android:color="#907C74" />
</shape>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 927 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 999 KiB

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/transparent" />
<!-- 边框设置 -->
<stroke
android:width="2dp"
android:color="#8c7c75" />
<!-- 圆角半径 -->
<corners android:radius="12dp" />
</shape>
+1 -4
View File
@@ -1,8 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"> <shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="12dp" /> <corners android:radius="12dp" />
<!-- <solid android:color="#F3D2BD"/>--> <solid android:color="#FFCC99" />
<gradient
android:endColor="#E5B79F"
android:startColor="#F9DDC9" />
</shape> </shape>
+1 -1
View File
@@ -3,5 +3,5 @@
<corners android:radius="12dp" /> <corners android:radius="12dp" />
<stroke <stroke
android:width="2dp" android:width="2dp"
android:color="#F0C8B4" /> android:color="#FFCC99" />
</shape> </shape>
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
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"/>
<TextView
android:id="@+id/btnInit"
android:layout_width="225dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="75dp"
android:background="@drawable/bg_init_button"
android:gravity="center"
android:padding="10dp"
android:text="设备初始化"
android:textColor="@color/white"
android:textSize="22sp"
android:textStyle="bold"
android:visibility="invisible"
tools:ignore="HardcodedText" />
<Space
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1" />
<ImageView
android:id="@+id/ivQrCode"
android:layout_width="150dp"
android:layout_height="150dp"
android:layout_gravity="center_horizontal|bottom"
android:layout_marginBottom="37dp"
tools:ignore="ContentDescription"
tools:src="@mipmap/ic_logo512"
android:visibility="invisible"/>
</LinearLayout>
+63
View File
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg"
android:orientation="vertical"
tools:context=".activity.InitActivity">
<include
android:id="@+id/includeHeader"
layout="@layout/item_title_time" />
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="0dp"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="wrap_content"
android:layout_height="142dp"
android:src="@drawable/ic_init_text"
android:adjustViewBounds="true"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="345dp"
android:layout_marginTop="90dp"
android:src="@drawable/ic_init_img"
android:adjustViewBounds="true" />
<!-- <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="60dp"
android:text="点击取盘"
android:textColor="#FFCC99"
android:textSize="24sp"
android:paddingHorizontal="45dp"
android:gravity="center"
android:background="@drawable/btn_outline"
tools:ignore="HardcodedText" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
@@ -2,7 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@mipmap/bg" android:background="@drawable/bg"
android:orientation="vertical"> android:orientation="vertical">
<include <include
@@ -21,7 +21,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="手机后四位" android:text="手机后四位"
android:textColor="#F0C8B4" android:textColor="#FFCC99"
android:textSize="26sp" android:textSize="26sp"
android:textStyle="bold" /> android:textStyle="bold" />
@@ -33,18 +33,20 @@
android:gravity="center_vertical"> android:gravity="center_vertical">
<ImageView <ImageView
android:layout_width="32dp" android:layout_width="wrap_content"
android:layout_height="32dp" android:layout_height="wrap_content"
android:layout_marginStart="24dp" android:layout_marginStart="24dp"
android:layout_marginEnd="55dp" android:layout_marginEnd="55dp"
android:src="@mipmap/ic_phone" /> android:src="@drawable/ic_phone" />
<EditText <EditText
android:id="@+id/et_phone"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginEnd="20dp" android:layout_marginEnd="20dp"
android:background="@android:color/transparent" android:background="@android:color/transparent"
android:hint="输入手机后四位" android:hint="输入手机后四位"
android:imeOptions="actionNext"
android:inputType="number" android:inputType="number"
android:maxLength="4" android:maxLength="4"
android:maxLines="1" android:maxLines="1"
@@ -60,7 +62,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="25dp" android:layout_marginTop="25dp"
android:text="校验码" android:text="校验码"
android:textColor="#F0C8B4" android:textColor="#FFCC99"
android:textSize="26sp" android:textSize="26sp"
android:textStyle="bold" /> android:textStyle="bold" />
@@ -72,22 +74,24 @@
android:gravity="center_vertical"> android:gravity="center_vertical">
<ImageView <ImageView
android:layout_width="32dp" android:layout_width="wrap_content"
android:layout_height="32dp" android:layout_height="wrap_content"
android:layout_marginStart="24dp" android:layout_marginStart="24dp"
android:layout_marginEnd="55dp" android:layout_marginEnd="55dp"
android:src="@mipmap/ic_password" /> android:src="@drawable/ic_pwd" />
<EditText <EditText
android:id="@+id/et_pwd"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginEnd="20dp" android:layout_marginEnd="20dp"
android:background="@android:color/transparent" android:background="@android:color/transparent"
android:hint="输入校验码" android:hint="输入校验码"
android:inputType="textPassword" android:inputType="numberPassword"
android:maxLines="1" android:maxLines="1"
android:textColor="#F3D2BD" android:textColor="#F3D2BD"
android:textColorHint="#7B6D6A" android:textColorHint="#7B6D6A"
android:imeOptions="actionDone"
android:textSize="26sp" android:textSize="26sp"
android:textStyle="bold" /> android:textStyle="bold" />
@@ -111,7 +115,7 @@
android:layout_marginTop="40dp" android:layout_marginTop="40dp"
android:padding="8dp" android:padding="8dp"
android:text="人脸识别" android:text="人脸识别"
android:textColor="#887872" android:textColor="#FFCC99"
android:textSize="26sp" android:textSize="26sp"
android:textStyle="bold" /> android:textStyle="bold" />
</LinearLayout> </LinearLayout>
+55 -30
View File
@@ -4,7 +4,8 @@
android:id="@+id/main" android:id="@+id/main"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@mipmap/bg" android:background="@drawable/bg"
android:fitsSystemWindows="false"
android:orientation="vertical" android:orientation="vertical"
tools:context=".activity.LoginByFaceActivity"> tools:context=".activity.LoginByFaceActivity">
@@ -18,45 +19,57 @@
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:orientation="vertical"> android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:visibility="visible">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="0dp"
android:paddingTop="1dp">
<TextureView
android:id="@+id/dual_camera_texture_preview_rgb"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible" />
<com.sw.plate.utils.arcface.FaceRectView
android:id="@+id/dual_camera_face_rect_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible" />
</FrameLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginTop="0dp"
android:background="@drawable/bg_face"
android:visibility="visible" />
<TextView <TextView
android:id="@+id/tv_tip" android:id="@+id/tv_tip"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="159dp" android:layout_gravity="center_horizontal"
android:layout_marginTop="129dp"
android:text="请正视屏幕" android:text="请正视屏幕"
android:textColor="#fff0c8b4" android:textStyle="bold"
android:textColor="#FFCC99"
android:textSize="36sp" /> android:textSize="36sp" />
<FrameLayout
android:layout_width="380dp"
android:layout_height="380dp"
android:layout_marginTop="119dp"
android:padding="10dp"
android:visibility="visible">
<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="0dp"
android:background="@drawable/circle_dashed_border" />
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@mipmap/bg_face" />
</FrameLayout>
<LinearLayout <LinearLayout
android:id="@+id/ll_to_pwd" android:id="@+id/ll_to_pwd"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="120dp" android:layout_marginStart="208dp"
android:background="@drawable/btn_outline_887872" android:layout_marginTop="730dp"
android:background="@drawable/btn_outline"
android:gravity="center" android:gravity="center"
android:paddingHorizontal="45dp" android:paddingHorizontal="45dp"
android:paddingVertical="18dp"> android:paddingVertical="18dp">
@@ -65,9 +78,21 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="密码登录" android:text="密码登录"
android:textColor="#ff887872" android:textColor="#FFCC99"
android:textSize="24sp" /> android:textSize="24sp" />
</LinearLayout> </LinearLayout>
<TextView
android:id="@+id/tv_to_init"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="30"
android:padding="12dp"
android:gravity="center"
android:layout_marginTop="185dp"
android:textColor="#FFCC99"
android:textSize="36sp" />
</FrameLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
+10 -2
View File
@@ -1,12 +1,20 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main" android:id="@+id/main"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"
android:background="@drawable/bg"
tools:context=".activity.MainActivity"> tools:context=".activity.MainActivity">
<include <include
android:id="@+id/includeHeader" android:id="@+id/includeHeader"
layout="@layout/item_title_time" /> layout="@layout/item_title_time" />
</androidx.constraintlayout.widget.ConstraintLayout>
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
+14
View File
@@ -0,0 +1,14 @@
<?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"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/parentRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</LinearLayout>
@@ -0,0 +1,81 @@
<?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="1024dp"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:id="@+id/dialogContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:layout_marginTop="0dp"
android:background="@drawable/bg_dialog"
android:gravity="center"
android:orientation="vertical"
android:paddingTop="30dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_tip_warn_unbind" />
<TextView
android:id="@+id/tv_tip_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
tools:text="即将解除【曹德磊】的餐盘"
android:textColor="#ffffffff"
android:textSize="28sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="解绑后该用户将无法使用餐盘柜"
android:textColor="#ff999999"
android:textSize="24sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="30dp"
android:background="#ff333333" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="90dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_cancel"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="取消"
android:textColor="#ff999999"
android:textSize="30sp"
android:textStyle="bold" />
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="#333333" />
<TextView
android:id="@+id/tv_confirm"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="确定解绑"
android:textColor="#ffffcc99"
android:textSize="30sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/dialogContainer"
android:layout_width="480dp"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:layout_marginTop="0dp"
android:background="@drawable/bg_dialog"
android:gravity="center"
android:orientation="vertical"
tools:ignore="HardcodedText">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:src="@drawable/ic_tip_warn_unbind" />
<TextView
android:id="@+id/tv_tip_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="余额不足"
android:textColor="#ffffffff"
android:textSize="28sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="请通过小程序或到柜台充值后使用"
android:textColor="#ff999999"
android:textSize="24sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="30dp"
android:background="#ff333333" />
<TextView
android:id="@+id/tv_confirm"
android:layout_width="match_parent"
android:layout_height="90dp"
android:gravity="center"
android:text="知道了"
android:textColor="#ffffcc99"
android:textSize="30sp"
android:textStyle="bold" />
</LinearLayout>
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/dialogContainer"
android:layout_width="480dp"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:layout_marginTop="0dp"
android:background="@drawable/bg_dialog"
android:gravity="center"
android:orientation="vertical"
tools:ignore="HardcodedText">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:src="@drawable/ic_tip_warn_unbind" />
<TextView
android:id="@+id/tv_tip_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="温馨提示"
android:textColor="#ffffffff"
android:textSize="28sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvBindContent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="请通过小程序或到柜台充值后使用"
android:textColor="#ff999999"
android:textSize="24sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="30dp"
android:background="#ff333333" />
<TextView
android:id="@+id/tv_confirm"
android:layout_width="match_parent"
android:layout_height="90dp"
android:gravity="center"
android:text="知道了"
android:textColor="#ffffcc99"
android:textSize="30sp"
android:textStyle="bold" />
</LinearLayout>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="240dp"
android:layout_height="80dp"
android:background="@drawable/dialog_waiting">
<TextView
android:id="@+id/tvTip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textColor="@color/white"
android:text="请稍等"
android:textSize="22sp" />
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginRight="30dp"
android:layout_toLeftOf="@id/tvTip"
android:indeterminateTint="@color/teal_200"
android:indeterminateTintMode="src_atop" />
</RelativeLayout>
</FrameLayout>

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