4 Commits
Author SHA1 Message Date
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
75 changed files with 2882 additions and 402 deletions
+14 -2
View File
@@ -8,6 +8,12 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<application
android:name=".MyApp"
@@ -25,8 +31,14 @@
android:exported="true"></activity>
<activity
android:name=".activity.LoginByPwdActivity"
android:exported="false"
android:launchMode="singleTask" />
android:exported="true"
android:launchMode="singleTask">
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.MAIN" />-->
<!-- <category android:name="android.intent.category.LAUNCHER" />-->
<!-- </intent-filter>-->
</activity>
<activity
android:name=".activity.LoginByFaceActivity"
android:exported="true"
@@ -1,6 +1,31 @@
package com.sw.platecabinet
object GlobalData {
/**
* 同一设备全局使用的设备编号
*/
var globalEquipmentCode: String = "202501171634"
/**
* app版本号
*/
var appVersion: String = ""
/**
* 横排数量
*/
var arrayCross: Int = 3
/**
* 竖排数量
*/
var arrayVertical: Int = 11
/**
* 排列方式 0 垂直 1 水平
*/
var arrayMode: Int = 0
}
@@ -1,6 +1,7 @@
package com.sw.platecabinet
import com.sw.plate.App
import com.sw.plate.utils.AppUtil
import timber.log.Timber
class MyApp : App() {
@@ -12,5 +13,14 @@ class MyApp : App() {
super.onCreate()
Timber.plant(Timber.DebugTree())
Timber.d("初始化")
initGlobalData()
}
/**
* 初始化全局数据
*/
private fun initGlobalData() {
GlobalData.appVersion = AppUtil.getAppVersionName(this)
GlobalData.globalEquipmentCode = "202501171634"
}
}
@@ -2,22 +2,34 @@ package com.sw.platecabinet.activity
import android.app.Dialog
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
import android.view.KeyEvent
import android.view.View
import android.view.WindowInsetsController
import android.view.WindowManager
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import com.sw.inbound.utils.DateTimeUtils
import com.sw.plate.utils.ScanGunKeyEventHelper
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.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.ext.setClickListeners
import com.sw.platecabinet.utils.PermissionHelper
import com.sw.platecabinet.view.CustomDialog
import com.sw.platecabinet.viewmodel.SettingViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.launch
import timber.log.Timber
@@ -30,54 +42,167 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
protected lateinit var context: Context
private var timeJob: Job? = null
private var mDialogWaiting: CustomDialog? = null
private val permissionHelpers = mutableMapOf<Int, PermissionHelper>()
protected var keyEventHelper: ScanGunKeyEventHelper? = null
private val viewModel by viewModels<SettingViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
context = this
// disableSystemUICompletely()
// 确保内容延伸到导航栏区域
// WindowCompat.setDecorFitsSystemWindows(window, false)
//保持亮屏
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
binding = inflateViewBinding()
headerBinding = inflateTitleBinding()
setContentView(binding.root)
updateTime()
registerDataChange()
initialize()
registerKeyEvent()
}
/**
* 监听扫描枪扫描事件
*/
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) {
viewModel.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() {
headerBinding?.let {
headerBinding!!.tvRightTime.setClickListeners(
it.tvLeftDate.setClickListeners(
onDoubleClick = {
MainActivity.start(context, pageType = PageType.SETTING_LIST)
onLeftDoubleClick()
}
)
it.tvRightTime.setClickListeners(
onDoubleClick = {
onRightDoubleClick()
}
)
val scope = CoroutineScope(Dispatchers.Main)
timeJob = scope.launch {
DateTimeUtils.realTimeChineseDateFlow()
.collect { (date, time) ->
headerBinding!!.tvLeftDate.text = date
headerBinding!!.tvRightTime.text = time
it.tvLeftDate.text = date
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
) { permanentlyDeniedPermissions ->
// 权限被永久拒绝的统一处理
Timber.e("用户永久拒绝权限 $permanentlyDeniedPermissions")
grantResults,
permissionHelper = helper
)
if (allGranted) {
permissionHelpers.remove(requestCode)
}
}
/**
* 注册PermissionHelper以便处理结果
*/
fun registerPermissionHelper(helper: PermissionHelper) {
permissionHelpers[helper.requestCode] = helper
}
override fun onResume() {
super.onResume()
// 关键点2:防止某些场景下系统栏恢复
enforceImmersiveMode()
}
private fun 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() {
// 持续强制隐藏系统栏(防止手势触发)
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确保覆盖手势触发
}
/**
* 显示等待提示框
*/
@@ -105,8 +230,62 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
protected abstract fun initialize()
/**
* 注册数据监听
*/
protected open fun registerDataChange() {
lifecycleScope.launch {
viewModel.showLoading.collect {
if (it) {
showWaitingDialog("")
} else {
hideWaitingDialog()
}
}
}
lifecycleScope.launch {
viewModel.currentUserInfo.drop(1).collect {
if (it == null) return@collect
if (it.equipmentBoxCode?.isNotEmpty() == true) {
SerialApi.openPlate(
it.equipmentBoxCode.toInt(),
object : SerialPortManager.SendCallback {
override fun onSuccess() {
MainActivity.start(context, pageType = PageType.PLATE_OPEN)
}
override fun onFail(e: Exception?) {
ToastUtils.showToast("柜门打开失败")
}
})
} else {
viewModel.getEquipmentList()
}
}
}
lifecycleScope.launch {
viewModel.equipmentList.drop(1).collect {
if (it.isEmpty()) return@collect
val unbindList = it.filter { !it.isBound() }
if (unbindList.isEmpty()) {
MainActivity.start(context = context, pageType = PageType.PLATE_CABINET_FULL)
} else {
val firstInfo = unbindList[0]
val currentUserInfo = viewModel.currentUserInfo.value
firstInfo.plateNumber = currentUserInfo?.plateNumber ?: ""
MainActivity.start(
context = context,
pageType = PageType.BIND_PLATE,
equipmentUserInfo = firstInfo
)
}
}
}
}
override fun onDestroy() {
timeJob?.cancel()
keyEventHelper?.onDestroy()
super.onDestroy()
}
}
@@ -2,30 +2,54 @@ package com.sw.platecabinet.activity
import android.Manifest
import android.content.Intent
import android.graphics.Outline
import android.graphics.Point
import android.hardware.Camera
import android.os.Build
import android.util.DisplayMetrics
import android.view.View
import android.view.ViewOutlineProvider
import android.view.ViewGroup
import android.view.ViewTreeObserver
import androidx.activity.viewModels
import androidx.camera.core.CameraSelector
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import com.google.common.util.concurrent.ListenableFuture
import androidx.annotation.RequiresApi
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
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.viewmodel.RecognizeViewModel
import com.sw.platecabinet.R
import com.sw.platecabinet.databinding.ActivityLoginFaceBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.utils.PermissionHelper
import com.sw.platecabinet.viewmodel.UserViewModel
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.launch
import timber.log.Timber
/**
* 人脸识别
*/
class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>() {
private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
ViewTreeObserver.OnGlobalLayoutListener {
private val viewModel by viewModels<UserViewModel>()
private val recognizeViewModel by viewModels<RecognizeViewModel>()
private val CAMERA_PERMISSION_REQUEST_CODE = 100
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
private val REQUIRED_PERMISSIONS: Array<String> = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
override fun inflateViewBinding(): ActivityLoginFaceBinding {
return ActivityLoginFaceBinding.inflate(layoutInflater)
@@ -36,9 +60,11 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>() {
}
override fun initialize() {
// 请求权限
checkCameraPermission()
initView()
viewModel.activeEngine()
initArcViewModel()
initArcView()
openRectInfoDraw = true
recognizeViewModel.setDrawRectInfoTextValue(true)
viewModel.generateToken()
binding.llToPwd.setOnClickListener {
val intent = Intent(this, LoginByPwdActivity::class.java)
@@ -46,53 +72,311 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>() {
}
}
private fun checkCameraPermission() {
PermissionHelper.with(this, REQUIRED_PERMISSIONS, CAMERA_PERMISSION_REQUEST_CODE)
.onGranted {
// 权限已授予
startCamera()
}
.checkAndRequest()
}
private fun initView() {
binding.previewView.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
// outline.setRoundRect(0, 0, view.width, view.height, 12f.dp)
outline.setOval(0, 0, view.width, view.height)
override fun registerDataChange() {
super.registerDataChange()
lifecycleScope.launch {
viewModel.currentUserInfo.drop(1).collect {
Timber.d("currentUserInfo it = $it")
if (it != null) {
ToastUtils.showToast("登录成功")
MainActivity.start(context, pageType = PageType.PLATE_CABINET_FULL)
finish()
}
}
}
binding.previewView.clipToOutline = true
}
private fun startCamera() {
Timber.d("startCamera")
cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
// 创建预览用例
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(binding.previewView.surfaceProvider)
private fun checkCameraPermission() {
Timber.i("checkCameraPermission")
val permissionHelper =
PermissionHelper.with(this, REQUIRED_PERMISSIONS, CAMERA_PERMISSION_REQUEST_CODE)
.onGranted {
Timber.d("checkCameraPermission 权限已允许")
// 权限已授予
recognizeViewModel.init()
initRgbCamera()
resumeCamera()
}
.build()
registerPermissionHelper(permissionHelper)
permissionHelper.checkAndRequest()
}
// 选择后置摄像头
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
private var isRecognition = false
private var rgbCameraHelper: DualCameraHelper? = null
private var rgbFaceRectTransformer: FaceRectTransformer? = null
private val livenessType = LivenessType.RGB
private var openRectInfoDraw = false
try {
// 解绑所有用例
cameraProvider.unbindAll()
private fun initArcViewModel() {
// 绑定到生命周期
cameraProvider.bindToLifecycle(
this, cameraSelector, preview
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(exc, "Use case binding failed")
Timber.e("ftInitCode observe = $error")
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?.toInt() ?: 0)
})
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() {
if (rgbCameraHelper != null) {
rgbCameraHelper!!.release()
rgbCameraHelper = null
}
recognizeViewModel.destroy()
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
) {
runOnUiThread({
val previewSizeRgb = camera.getParameters().getPreviewSize()
val layoutParams = adjustPreviewViewSize(
binding.dualCameraTexturePreviewRgb,
binding.dualCameraTexturePreviewRgb, binding.dualCameraFaceRectView,
previewSizeRgb, displayOrientation, 1f
)
Timber.d("initRgbCamera previewSizeRgb = ${previewSizeRgb.width}-${previewSizeRgb.height}")
Timber.d("initRgbCamera layoutParams = ${layoutParams.width}-${layoutParams.height}")
rgbFaceRectTransformer = FaceRectTransformer(
previewSizeRgb.width,
previewSizeRgb.height,
layoutParams.width,
layoutParams.height,
displayOrientation,
cameraId,
isMirror,
ConfigUtil.isDrawRgbRectHorizontalMirror(context),
ConfigUtil.isDrawRgbRectVerticalMirror(context)
)
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("onCameraClosed: ")
}
override fun onCameraError(e: java.lang.Exception) {
Timber.i("onCameraError: %s", e.message)
e.printStackTrace()
}
override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) {
Timber.i("onCameraConfigurationChanged: threadName = ${Thread.currentThread().name}")
if (rgbFaceRectTransformer != null) {
rgbFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation
}
Timber.i("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() {
binding.dualCameraTexturePreviewRgb.getViewTreeObserver().removeOnGlobalLayoutListener(this)
// 请求权限
checkCameraPermission()
}
// @Override
// protected void onResume() {
// super.onResume();
// resumeCamera();
// }
private fun resumeCamera() {
isRecognition = true
if (rgbCameraHelper != null && rgbCameraHelper!!.isStopped) {
rgbCameraHelper!!.start()
}
}
protected override fun onPause() {
pauseCamera()
super.onPause()
}
private fun pauseCamera() {
isRecognition = false
recognizeViewModel.onPreviewFrame(ByteArray(1382400), true)
}
}
@@ -1,10 +1,20 @@
package com.sw.platecabinet.activity
import android.content.Intent
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.face.Test
import com.sw.platecabinet.databinding.ActivityLoginByPwdBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.viewmodel.UserViewModel
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.launch
import timber.log.Timber
class LoginByPwdActivity : BaseActivity<ActivityLoginByPwdBinding>() {
private val viewModel by viewModels<UserViewModel>()
override fun inflateViewBinding(): ActivityLoginByPwdBinding {
return ActivityLoginByPwdBinding.inflate(layoutInflater)
}
@@ -15,11 +25,39 @@ class LoginByPwdActivity : BaseActivity<ActivityLoginByPwdBinding>() {
override fun initialize() {
binding.btnLogin.setOnClickListener {
MainActivity.start(context, pageType = PageType.PLATE_CABINET_FULL)
val phone = binding.etPhone.text.toString()
val pwd = binding.etPwd.text.toString()
if (phone.isEmpty() || pwd.isEmpty()) {
ToastUtils.showToast("手机或校验码不能为空")
val test = Test()
for (i in 1..1000) {
test.test1()
}
return@setOnClickListener
}
viewModel.loginWithPwd(phone = phone, password = pwd)
}
binding.tvFaceRec.setOnClickListener {
val intent = Intent(this, LoginByFaceActivity::class.java)
startActivity(intent)
// handleScanKeyInfo(binding.etPwd.text.toString())
}
}
override fun registerDataChange() {
super.registerDataChange()
lifecycleScope.launch {
viewModel.currentUserInfo
.drop(1)
.collect {
Timber.d("currentUserInfo it = $it")
if (it != null) {
// ToastUtils.showToast("登录成功")
MainActivity.start(context, pageType = PageType.PLATE_OPEN)
finish()
}
}
}
}
@@ -13,8 +13,12 @@ import com.sw.platecabinet.fragment.UnBindPlateFragment
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.utils.FragmentHelper
typealias Callback = (String) -> Unit
class MainActivity : BaseActivity<ActivityMainBinding>() {
private lateinit var fragmentHelper: FragmentHelper
private var pageType = PageType.SETTING_LIST
private var callback: Callback? = null
override fun inflateViewBinding(): ActivityMainBinding {
return ActivityMainBinding.inflate(layoutInflater)
@@ -43,7 +47,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
override fun initialize() {
val param = intent.getStringExtra(PARAM_PAGE_TYPE)
val equipmentUserInfo = intent.getParcelableExtra<EquipmentUserInfo>(PARAM_EQUIPMENT_INFO)
val pageType = param?.let { PageType.valueOf(param) } ?: PageType.SETTING_LIST
pageType = param?.let { PageType.valueOf(param) } ?: PageType.SETTING_LIST
fragmentHelper = FragmentHelper(supportFragmentManager, R.id.fragment_container)
val page = when (pageType) {
PageType.SETTING_LIST -> SettingListFragment()
@@ -54,8 +58,33 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
}
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()
}
}
/**
* 界面类型
*/
@@ -25,7 +25,18 @@ class GenericPageAdapter<T, VB : ViewBinding>(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageViewHolder {
val view = LayoutInflater.from(parent.context).inflate(pageLayoutId, parent, false)
return PageViewHolder(view)
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) {
@@ -35,22 +46,15 @@ class GenericPageAdapter<T, VB : ViewBinding>(
override fun getItemCount(): Int = pages.size
inner class PageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val childRecyclerView: RecyclerView = itemView.findViewById(R.id.childRecyclerView)
val childRecyclerView: RecyclerView = itemView.findViewById(R.id.childRecyclerView)
init {
childRecyclerView.layoutManager = GridLayoutManager(itemView.context, 2)
}
fun bind(items: List<T>) {
childRecyclerView.layoutManager = GridLayoutManager(itemView.context, 2)
// 添加间距装饰(12dp)
childRecyclerView.addItemDecoration(
GridSpacingItemDecoration(
spanCount = 2, // 2列
spacing = itemView.context.dpToPx(12), // 12dp
includeEdge = true // 包含边缘间距
)
)
childRecyclerView.adapter =
GenericChildAdapter(items, itemBindingInflater, itemBindCallback)
GenericItemAdapter(items, itemBindingInflater, itemBindCallback)
}
}
}
@@ -58,11 +62,11 @@ class GenericPageAdapter<T, VB : ViewBinding>(
/**
* 通用子项适配器(支持ViewBinding
*/
class GenericChildAdapter<T, VB : ViewBinding>(
private val items: List<T>,
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<GenericChildAdapter<T, VB>.ViewHolder>() {
) : RecyclerView.Adapter<GenericItemAdapter<T, VB>.ViewHolder>() {
inner class ViewHolder(private val binding: VB) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: T, position: Int) {
@@ -70,6 +74,11 @@ class GenericChildAdapter<T, VB : ViewBinding>(
}
}
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)
@@ -77,7 +86,6 @@ class GenericChildAdapter<T, VB : ViewBinding>(
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position], position)
}
override fun getItemCount(): Int = items.size
@@ -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()
}
@@ -1,19 +1,28 @@
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.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import com.sw.platecabinet.R
import com.sw.platecabinet.view.CustomDialog
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: CustomDialog? = null
protected val viewModel by viewModels<SettingViewModel>()
override fun onCreateView(
inflater: LayoutInflater,
@@ -22,16 +31,47 @@ abstract class BaseFragment<VB : ViewBinding>(
): 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()
class ViewModelFactory<T : ViewModel>(private val creator: () -> T) :
ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return creator() as T
}
/**
* 显示等待提示框
*/
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 = CustomDialog(requireContext(), view, R.style.MyDialog)
mDialogWaiting!!.show()
mDialogWaiting!!.setCancelable(true)
return mDialogWaiting
}
/**
* 隐藏等待提示框
*/
fun hideWaitingDialog() {
mDialogWaiting?.dismiss()
mDialogWaiting = null
}
override fun onDestroyView() {
@@ -1,23 +1,42 @@
package com.sw.platecabinet.fragment
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.Editable
import android.text.TextUtils
import android.text.TextWatcher
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.platecabinet.R
import com.sw.platecabinet.adapter.GenericChildAdapter
import com.sw.platecabinet.activity.MainActivity
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.ext.maskName
import com.sw.platecabinet.ext.maskPhone
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.SearchResult
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.launch
import timber.log.Timber
/**
* 餐盘柜绑定
*/
class BindPlateFragment : BaseFragment<FragmentBindPlateBinding>(
FragmentBindPlateBinding::inflate
) {
private lateinit var adapter: GenericChildAdapter<EquipmentUserInfo, ItemSearchUserInfoBinding>
private lateinit var adapter: GenericItemAdapter<SearchResult.Member, ItemSearchUserInfoBinding>
internal val ARG_PARAM1 = "param1"
private var info: EquipmentUserInfo? = null
private var checkedItem: SearchResult.Member? = null
private var lastText = ""
companion object {
@JvmStatic
@@ -32,25 +51,145 @@ class BindPlateFragment : BaseFragment<FragmentBindPlateBinding>(
override fun initialize() {
arguments?.let {
info = it.getParcelable(ARG_PARAM1)
info?.let { binding.tvNum.text = "${it.equipmentName}-${it.equipmentBoxCode}" }
info?.let {
binding.tvNum.text = "${it.equipmentName}-${it.equipmentBoxCode}"
if (!TextUtils.isEmpty(it.plateNumber)) {
binding.etCode.text = Editable.Factory.getInstance().newEditable(it.plateNumber)
}
}
}
initView()
initListener()
}
private fun initView() {
adapter = createAdapter()
registerDateChange()
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 = Editable.Factory.getInstance().newEditable(it)
}
}
private fun initListener() {
binding.etUserInfo.addTextChangedListener(object : TextWatcher {
private val handler = Handler(Looper.getMainLooper())
private val debounceDelay = 500L // 延迟 500 毫秒
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) {
}
override fun onTextChanged(
s: CharSequence?,
start: Int,
before: Int,
count: Int
) {
}
override fun afterTextChanged(s: Editable?) {
val currentText = s.toString()
if (currentText == lastText) return // 内容未变化时不处理
lastText = currentText
handler.removeCallbacksAndMessages(null) // 取消之前的延迟任务
handler.postDelayed({
// if (currentText.isNotEmpty()) {
viewModel.getSearchMemberList(param = currentText, pageNum = 1)
// }
}, debounceDelay)
}
})
binding.llBind.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
}
viewModel.bindPlate(
equipmentCode = info!!.equipmentCode!!,
equipmentBoxCode = info!!.equipmentBoxCode!!,
memberId = checkedItem!!.id!!,
plateNumber = plateNumber
)
}
binding.tvBack.setOnClickListener { activity?.finish() }
}
private fun registerDateChange() {
/**
* 监听数据变化
*/
override fun registerDataChange() {
super.registerDataChange()
lifecycleScope.launch {
viewModel.searchMemberList.drop(1).collect {
adapter.updateData(it)
}
}
lifecycleScope.launch {
viewModel.bindStateChange.drop(1).collect {
if (it.first == null) return@collect // 解绑状态过滤
val errorInfo = it.second
if (errorInfo.isSuccess()) {
ToastUtils.showToast("绑定成功")
activity?.finish()
} else {
ToastUtils.showToast(errorInfo.msg)
}
}
}
}
private fun createAdapter(): GenericChildAdapter<EquipmentUserInfo, ItemSearchUserInfoBinding> {
return GenericChildAdapter(
private fun createAdapter(): GenericItemAdapter<SearchResult.Member, ItemSearchUserInfoBinding> {
return GenericItemAdapter(
items = emptyList(),
bindingInflater = ItemSearchUserInfoBinding::inflate,
bindCallback = { item, position ->
this.tvName.text = item.name
this.tvPhone.text = item.phone
if (item.isBound()) {
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)
@@ -58,6 +197,8 @@ class BindPlateFragment : BaseFragment<FragmentBindPlateBinding>(
this.llRoot.setOnClickListener {
Timber.d("itemClick ${item.name}, position = $position")
checkedItem = item
adapter.notifyDataSetChanged()
}
}
)
@@ -2,6 +2,7 @@ package com.sw.platecabinet.fragment
import android.os.CountDownTimer
import com.sw.platecabinet.databinding.FragmentPlateCabinetFullBinding
import timber.log.Timber
/**
* 餐盘柜已满
@@ -11,6 +12,7 @@ class PlateCabinetFullFragment :
private var totalTimeInMillis: Long = 5 * 1000
override fun initialize() {
Timber.d("initialize")
initCountTime()
}
@@ -1,20 +1,28 @@
package com.sw.platecabinet.fragment
import android.view.View
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.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.model.request.EquipmentParam
import com.sw.platecabinet.ext.maskName
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.viewmodel.SettingViewModel
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.launch
import timber.log.Timber
import kotlin.math.ceil
@@ -24,105 +32,165 @@ import kotlin.math.ceil
*/
class SettingListFragment :
BaseFragment<FragmentSettingListBinding>(FragmentSettingListBinding::inflate) {
private val viewModel: SettingViewModel by viewModels<SettingViewModel>()
/**
* 需要横向滚动的adapter
*/
private var pageAdapter: GenericPageAdapter<EquipmentUserInfo, ItemBindViewBinding>? = null
private lateinit var pageAdapter: GenericPageAdapter<EquipmentUserInfo, ItemBindViewBinding>
private val itemsPerPage = 22 // 每行2个,每列11个,共22个
/**
* 垂直滚动的adapter
*/
private var itemAdapter: GenericItemAdapter<EquipmentUserInfo, ItemBindViewBinding>? = null
private val itemsPerPage = GlobalData.arrayCross * GlobalData.arrayVertical // 每行2个,每列11个,共22个
private fun registerDateChange() {
override fun registerDataChange() {
super.registerDataChange()
lifecycleScope.launch {
// 使用 repeatOnLifecycle 确保只在特定生命周期状态收集
// repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.equipmentList.collect { it ->
viewModel.equipmentList.drop(1).collect { it ->
Timber.d("registerDateChange updateAdapter it = $it")
updateAdapter(it)
}
// }
}
}
override fun initialize() {
binding.mainRecyclerView.apply {
layoutManager = LinearLayoutManager(context)
pageAdapter = createAdapter()
adapter = pageAdapter
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
}
}
registerDateChange()
viewModel.getEquipmentList(EquipmentParam(equipmentCode = "202501171634"))
}
private fun createAdapter(): GenericPageAdapter<EquipmentUserInfo, ItemBindViewBinding> {
override fun onResume() {
super.onResume()
viewModel.getEquipmentList()
}
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 ->
this.tvNum.text = formatNumber(item.equipmentBoxCode ?: "")
if (item.isBound()) {
val date = DateTimeUtils.parseDateTime(item.updateTime)
val timeInMillis = date?.time ?: 0L
val isOldTime = DateTimeUtils.isMoreThan36HoursFromNow(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_F0C8B4))
this.llBindInfo.visibility = View.VISIBLE
this.tvUnbind.visibility = View.GONE
this.tvName.text = item.name
this.tvLastTime.text = DateTimeUtils.getTimeAgo(date)
} else {
this.llRoot.setBackgroundResource(R.drawable.grid_item_unbind)
this.tvNum.setTextColor(resources.getColor(R.color.bind_F0C8B4))
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 ${item.equipmentCode}")
MainActivity.start(requireContext(), pageType = PageType.PLATE_OPEN)
}
setItemInfo(item, position)
}
)
}
fun updateAdapter(items: List<EquipmentUserInfo>) {
val pages = if (items.size > itemsPerPage) {
items.chunked(itemsPerPage) {
convertToColumnFirst(it, 11)
}
} else {
listOf(
// 根据条件判断是否需要重新排列
// items
convertToColumnFirst(items, 11)
private fun ItemBindViewBinding.setItemInfo(
item: EquipmentUserInfo,
position: Int
) {
this.tvNum.text = formatNumber(item.equipmentBoxCode ?: "")
if (item.isBound()) {
val date = DateTimeUtils.parseDateTime(item.updateTime)
val timeInMillis = date?.time ?: 0L
val isOldTime = DateTimeUtils.isMoreThan36HoursFromNow(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_F0C8B4))
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_F0C8B4))
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
}
pageAdapter.updatePages(pages)
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_OPEN)
}
override fun onFail(e: Exception?) {
hideWaitingDialog()
ToastUtils.showToast("柜门打开失败")
}
})
}
}
fun updateAdapter(items: List<EquipmentUserInfo>) {
if (GlobalData.arrayCross > 2) {
var pages = if (items.size > itemsPerPage) {
items.chunked(itemsPerPage) {
convertToColumnFirst(it, 11)
}
} else {
listOf(
// 根据条件判断是否需要重新排列
// items
convertToColumnFirst(items, 11)
)
}
pageAdapter?.updatePages(pages)
} else {
itemAdapter?.updateData(items)
}
binding.mainRecyclerView.scrollToPosition(0)
}
/**
@@ -1,9 +1,15 @@
package com.sw.platecabinet.fragment
import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import com.sw.inbound.utils.DateTimeUtils
import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.databinding.FragmentUnbindPlateBinding
import com.sw.platecabinet.ext.maskName
import com.sw.platecabinet.ext.maskPhone
import com.sw.platecabinet.model.response.EquipmentUserInfo
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.launch
/**
* 餐盘柜绑定
@@ -12,7 +18,6 @@ class UnBindPlateFragment private constructor() : BaseFragment<FragmentUnbindPla
FragmentUnbindPlateBinding::inflate
) {
internal val ARG_PARAM1 = "param1"
private var param1: EquipmentUserInfo? = null
private var info: EquipmentUserInfo? = null
companion object {
@@ -31,15 +36,38 @@ class UnBindPlateFragment private constructor() : BaseFragment<FragmentUnbindPla
info?.let {
binding.tvNum.text = "${it.equipmentName}-${it.equipmentBoxCode}"
binding.tvPlateNumberValue.text = it.plateNumber
binding.tvNameValue.text = it.name
binding.tvPhoneValue.text = it.phone
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
viewModel.unbindPlate(
equipmentBoxCode = info!!.equipmentBoxCode!!,
equipmentCode = info!!.equipmentCode!!
)
}
binding.tvBack.setOnClickListener {
activity?.finish()
}
}
override fun registerDataChange() {
super.registerDataChange()
lifecycleScope.launch {
viewModel.bindStateChange.drop(1).collect {
if (it.first == null) return@collect // 绑定状态过滤
val errorInfo = it.second
if (errorInfo.isSuccess()) {
ToastUtils.showToast("解绑成功")
activity?.finish()
} else {
ToastUtils.showToast(errorInfo.msg)
}
}
}
}
}
@@ -0,0 +1,10 @@
package com.sw.platecabinet.model
/**
* 错误信息 code == 200 成功
*/
data class ErrorInfo(val code: Int = 200, val msg: String = "") {
fun isSuccess(): Boolean {
return code == 200
}
}
@@ -11,25 +11,29 @@ import kotlinx.parcelize.Parcelize
*/
@Parcelize
data class BindParam(
/**
* app版本号
*/
var appVersion: String = "",
/**
* 设备ID
*/
@SerializedName("equipmentCode")
var equipmentCode: String = "",
/**
*设备盒子编号
*/
@SerializedName("equipmentBoxCode")
val equipmentBoxCode: String? = "",
/**
*设备编号
*/
@SerializedName("equipmentCode")
val equipmentCode: String? = "",
val equipmentBoxCode: String? = null,
/**
*
* 会员Id
*/
@SerializedName("memberId")
val memberId: Int? = 0,
val memberId: Int? = null,
/**
*餐盘编号
*/
@SerializedName("plateNumber")
val plateNumber: String? = ""
val plateNumber: String? = null
) : Parcelable
@@ -1,6 +1,7 @@
package com.sw.platecabinet.model.request
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
/**
@@ -8,8 +9,13 @@ import kotlinx.parcelize.Parcelize
*/
@Parcelize
data class EquipmentParam(
/**
* app版本号
*/
var appVersion: String = "",
/**
* 设备ID
*/
var equipmentCode: String = ""
@SerializedName("equipmentCode")
var equipmentCode: String = "",
) : Parcelable
@@ -10,24 +10,28 @@ import kotlinx.parcelize.Parcelize
*/
@Parcelize
data class LoginParam(
/**
* app版本号
*/
var appVersion: String = "",
/**
* 设备ID
*/
@SerializedName("equipmentCode")
val equipmentCode: String? = "",
var equipmentCode: String = "",
/**
* 会员信息
*/
@SerializedName("memberId")
val memberId: String? = "",
val memberId: Int? = null,
/**
* 密码
*/
@SerializedName("password")
val password: String? = "",
val password: String? = null,
/**
* 手机号
*/
@SerializedName("phone")
val phone: String? = ""
val phone: String? = null
) : Parcelable
@@ -10,6 +10,15 @@ 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")
@@ -3,6 +3,7 @@ package com.sw.platecabinet.model.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.sw.platecabinet.GlobalData
import kotlinx.parcelize.Parcelize
/**
@@ -50,7 +51,7 @@ data class EquipmentUserInfo(
* 餐盘编号
*/
@SerializedName("plateNumber")
val plateNumber: String? = "",
var plateNumber: String? = "",
/**
*
* 更新时间
@@ -59,7 +60,17 @@ data class EquipmentUserInfo(
val updateTime: String? = ""
) : Parcelable {
/**
* 是否已绑定
*/
fun isBound(): Boolean {
return memberId != null
}
/**
* 是否是其他设备
*/
fun isOtherEquipment(): Boolean {
return GlobalData.globalEquipmentCode != equipmentCode
}
}
@@ -5,6 +5,9 @@ import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
/**
* 搜索会员信息结果
*/
@Parcelize
data class SearchResult(
@SerializedName("endRow")
@@ -11,7 +11,7 @@ import timber.log.Timber
import java.util.concurrent.TimeUnit
object ApiClient {
private const val BASE_URL = "https://vip.shuziweidao.com/shuwei-zhct/"
private const val BASE_URL = "https://vip.shuziweidao.com"
// private const val BASE_URL = "http://192.168.1.8:9092"
private const val TIME_OUT = 30L // 超时时间(秒)
@@ -18,37 +18,49 @@ interface ApiService {
/**
* 生成token
*/
@GET("scales/generateToken")
suspend fun generateToken(@Query("deviceId") deviceId: String): ApiResponse<String>
@GET("/shuwei-zhct/scales/generateToken")
suspend fun generateToken(
@Query("deviceId") deviceId: String,
@Query("appVersion") appVersion: String,
@Query("equipmentCode") equipmentCode: String
): ApiResponse<String>
/**
* 获取人脸数据
*/
@GET("scales/getUserFaceCache")
suspend fun getUserFaceCache(): ApiResponse<List<UserFaceModel>>
@GET("/shuwei-zhct/scales/getUserFaceCache")
suspend fun getUserFaceCache(
@Query("appVersion") appVersion: String,
@Query("equipmentCode") equipmentCode: String
): ApiResponse<List<UserFaceModel>>
/**
* 餐盘用户信息获取
*/
@POST("swEquipmentRelUser/equipmentBoxLogin")
@POST("/shuwei-zhct/swEquipmentRelUser/equipmentBoxLogin")
suspend fun equipmentBoxLogin(@Body param: LoginParam): ApiResponse<EquipmentUserInfo>
/**
* 餐盘用户信息列表查询
*/
@POST("swEquipmentRelUser/list")
@POST("/shuwei-zhct/swEquipmentRelUser/list")
suspend fun getEquipmentList(@Body param: EquipmentParam): ApiResponse<List<EquipmentUserInfo>>
/**
* 餐盘用户信息绑定解绑
*/
@POST("swEquipmentRelUser/addOrEdit")
@POST("/shuwei-zhct/swEquipmentRelUser/addOrEdit")
suspend fun bindEquipment(@Body param: BindParam): ApiResponse<EquipmentUserInfo?>
/**
* 用户信息模糊搜索
*/
@POST("swclientUserInfoShop/selectList")
@POST("/shuwei-user/swclientUserInfoShop/selectList")
suspend fun searchUser(@Body param: SearchParam): ApiResponse<SearchResult>
/**
* 通过餐盘号获取信息
*/
@POST("/shuwei-zhct/swEquipmentRelUser/findByPlateNumber")
suspend fun findByPlateNumber(@Body param: BindParam): ApiResponse<EquipmentUserInfo>
}
@@ -1,5 +1,6 @@
package com.sw.platecabinet.repository
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.model.request.BindParam
import com.sw.platecabinet.model.request.EquipmentParam
import com.sw.platecabinet.model.request.LoginParam
@@ -9,7 +10,6 @@ import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.SearchResult
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.network.api.ApiService
import retrofit2.http.POST
/**
* 远程数据处理
@@ -21,14 +21,25 @@ class RemoteRepository constructor(
* 生成token
*/
suspend fun generateToken(deviceId: String): ApiResponse<String> {
return safeApiCall { apiService.generateToken(deviceId) }
return safeApiCall {
apiService.generateToken(
deviceId,
GlobalData.appVersion,
GlobalData.globalEquipmentCode
)
}
}
/**
* 获取人脸数据
*/
suspend fun getUserFaceCache(): ApiResponse<List<UserFaceModel>> {
return safeApiCall { apiService.getUserFaceCache() }
return safeApiCall {
apiService.getUserFaceCache(
GlobalData.appVersion,
GlobalData.globalEquipmentCode
)
}
}
/**
@@ -55,8 +66,14 @@ class RemoteRepository constructor(
/**
* 用户信息模糊搜索
*/
@POST("/swclientUserInfoShop/selectList")
suspend fun searchUser(param: SearchParam): ApiResponse<SearchResult> {
return safeApiCall { apiService.searchUser(param) }
}
/**
* 通过餐盘编号获取绑定信息
*/
suspend fun findByPlateNumber(param: BindParam): ApiResponse<EquipmentUserInfo> {
return safeApiCall { apiService.findByPlateNumber(param) }
}
}
@@ -19,6 +19,13 @@ object DateTimeUtils {
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
*/
@@ -102,8 +109,6 @@ object DateTimeUtils {
return when {
hours < 1 -> "刚刚"
hours < 24 -> "${hours}小时前"
// hours < 48 -> "昨天"
// hours < 72 -> "前天"
else -> {
val days = hours / 24
"${days}天前"
@@ -9,21 +9,19 @@ 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>,
private val requestCode: Int,
private val rationale: String? = null
internal val requestCode: Int,
private val rationale: String? = null,
private val onGranted: (() -> Unit)?
) {
private var onGranted: (() -> Unit)? = null
companion object {
/**
* 创建权限请求构建器
* @param context 上下文(Activity或Fragment
* @param permissions 需要请求的权限数组
* @param requestCode 请求码
*/
fun with(context: Context, permissions: Array<String>, requestCode: Int): Builder {
return Builder(context, permissions, requestCode)
@@ -32,7 +30,7 @@ class PermissionHelper private constructor(
/**
* 检查是否已授予所有权限
*/
fun areAllPermissionsGranted(context: Context, permissions: Array<String>): Boolean {
fun checkPermissions(context: Context, permissions: Array<String>): Boolean {
return permissions.all { permission ->
ContextCompat.checkSelfPermission(
context,
@@ -43,15 +41,17 @@ class PermissionHelper private constructor(
/**
* 处理权限请求结果(供BaseActivity/BaseFragment调用)
* @return 是否所有权限都已授予
*/
fun handlePermissionResult(
context: Context,
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray,
onPermissionPermanentlyDenied: (List<String>) -> Unit
permissionHelper: PermissionHelper? = null
): Boolean {
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
permissionHelper?.onGranted?.invoke()
return true
}
@@ -67,10 +67,6 @@ class PermissionHelper private constructor(
}
}
}
if (permanentlyDeniedPermissions.isNotEmpty()) {
onPermissionPermanentlyDenied(permanentlyDeniedPermissions)
}
return false
}
@@ -83,6 +79,8 @@ class PermissionHelper private constructor(
}
if (context is Activity) {
context.startActivity(intent)
} else if (context is Fragment) {
context.startActivity(intent)
}
}
}
@@ -93,9 +91,10 @@ class PermissionHelper private constructor(
private val requestCode: Int
) {
private var rationale: String? = null
private var onGranted: (() -> Unit)? = null
/**
* 设置权限说明(当需要向用户解释权限用途时显示)
* 设置权限说明
*/
fun setRationale(rationale: String): Builder {
this.rationale = rationale
@@ -105,10 +104,16 @@ class PermissionHelper private constructor(
/**
* 设置权限授予回调
*/
fun onGranted(callback: () -> Unit): PermissionHelper {
val helper = PermissionHelper(context, permissions, requestCode, rationale)
helper.onGranted = callback
return helper
fun onGranted(callback: () -> Unit): Builder {
this.onGranted = callback
return this
}
/**
* 构建PermissionHelper实例
*/
fun build(): PermissionHelper {
return PermissionHelper(context, permissions, requestCode, rationale, onGranted)
}
}
@@ -116,7 +121,7 @@ class PermissionHelper private constructor(
* 检查并请求权限
*/
fun checkAndRequest() {
if (areAllPermissionsGranted(context, permissions)) {
if (checkPermissions(context, permissions)) {
onGranted?.invoke()
} else {
requestPermissions()
@@ -126,13 +131,11 @@ class PermissionHelper private constructor(
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)
@@ -142,7 +145,6 @@ class PermissionHelper private constructor(
.setNegativeButton("取消", null)
.show()
} else {
// 直接请求权限
doRequestPermissions(activity)
}
}
@@ -0,0 +1,124 @@
package com.sw.platecabinet.utils
import android.os.Handler
import android.os.Looper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import kotlin.coroutines.CoroutineContext
/**
* 多功能线程工具类
* 结合协程、Handler和线程池实现线程切换
*/
object ThreadUtils : CoroutineScope {
// 主线程Handler
private val mainHandler by lazy { Handler(Looper.getMainLooper()) }
// 后台线程池(IO密集型任务)
private val ioThreadPool: ExecutorService by lazy {
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2)
}
// CPU密集型线程池
private val cpuThreadPool: ExecutorService by lazy {
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
}
// 协程Job管理
private val job = Job()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
// ========== Handler相关方法 ==========
/**
* 在主线程执行任务
* @param delayMillis 延迟时间(毫秒)
*/
fun runOnUiThread(delayMillis: Long = 0, block: () -> Unit) {
if (delayMillis > 0) {
mainHandler.postDelayed(block, delayMillis)
} else {
if (isOnMainThread()) {
block()
} else {
mainHandler.post(block)
}
}
}
/**
* 移除主线程任务
*/
fun removeUiThreadTask(block: () -> Unit) {
mainHandler.removeCallbacks(block)
}
// ========== 线程池相关方法 ==========
/**
* 在IO线程执行任务
*/
fun runOnIoThread(block: () -> Unit) {
ioThreadPool.execute(block)
}
/**
* 在CPU计算线程执行任务
*/
fun runOnCpuThread(block: () -> Unit) {
cpuThreadPool.execute(block)
}
// ========== 协程相关方法 ==========
/**
* 启动协程(默认在主线程)
*/
fun launch(block: suspend CoroutineScope.() -> Unit): Job {
return launch(coroutineContext, block = block)
}
/**
* 在IO线程启动协程
*/
fun launchOnIo(block: suspend CoroutineScope.() -> Unit): Job {
return launch(Dispatchers.IO, block = block)
}
/**
* 切换到主线程(协程环境)
*/
suspend fun <T> switchToMain(block: suspend CoroutineScope.() -> T): T {
return withContext(Dispatchers.Main, block)
}
/**
* 切换到IO线程(协程环境)
*/
suspend fun <T> switchToIo(block: suspend CoroutineScope.() -> T): T {
return withContext(Dispatchers.IO, block)
}
/**
* 是否在主线程
*/
fun isOnMainThread(): Boolean {
return Looper.myLooper() == Looper.getMainLooper()
}
/**
* 释放资源
*/
fun release() {
job.cancel()
ioThreadPool.shutdown()
cpuThreadPool.shutdown()
}
}
@@ -1,7 +1,15 @@
package com.sw.platecabinet.viewmodel
import com.sw.inbound.utils.DateTimeUtils
import com.sw.plate.utils.ToastUtils
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 kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -10,12 +18,144 @@ class SettingViewModel : BaseViewModel() {
private val _equipmentList = MutableStateFlow<List<EquipmentUserInfo>>(emptyList())
val equipmentList: StateFlow<List<EquipmentUserInfo>> = _equipmentList
fun getEquipmentList(param: EquipmentParam) {
private val _searchMemberList = MutableStateFlow<List<SearchResult.Member>>(emptyList())
val searchMemberList: StateFlow<List<SearchResult.Member>> = _searchMemberList
private val _currentUserInfo = MutableStateFlow<EquipmentUserInfo?>(null)
val currentUserInfo: StateFlow<EquipmentUserInfo?> = _currentUserInfo
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()
}
}
}
/**
* 搜索会员信息列表
*/
fun getSearchMemberList(param: String, pageNum: Int = 1, pageSize: Int = 50) {
currentPage = pageNum
launch {
val param = SearchParam(
appVersion = GlobalData.appVersion,
param = param,
pageNum = pageNum,
pageSize = pageSize
)
isLoading = true
val response = repository.searchUser(param)
isLoading = false
if (parseResponse(response)) {
response.data?.let {
canLoadMore = it.hasNextPage != false
val newList = it.list ?: emptyList()
if (pageNum == 1) {
_searchMemberList.value = newList
} else {
_searchMemberList.value = _searchMemberList.value.plus(newList)
}
currentPage++
}
}
}
}
fun bindPlate(
equipmentBoxCode: String,
equipmentCode: String = globalEquipmentCode,
memberId: Int,
plateNumber: String
) {
launchWithLoading {
val bindParam = BindParam(
appVersion = GlobalData.appVersion,
equipmentBoxCode = equipmentBoxCode,
equipmentCode = equipmentCode,
memberId = memberId,
plateNumber = plateNumber
)
val response = repository.bindEquipment(bindParam)
if (parseResponse(response)) {
val userInfo = response.data
userInfo?.let {
if (it.isOtherEquipment()) {
ToastUtils.showToast("餐盘已在${it.equipmentName}绑定")
return@launchWithLoading
}
}
_bindStateChange.value = true to ErrorInfo()
} 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 findByPlateNumber(
plateNumber: String,
equipmentCode: String = globalEquipmentCode
) {
launchWithLoading {
val bindParam = BindParam(
appVersion = GlobalData.appVersion,
plateNumber = plateNumber,
equipmentCode = equipmentCode
)
val response = repository.findByPlateNumber(bindParam)
if (response.code == 200) {
val userInfo = response.data
userInfo?.let {
if (it.isOtherEquipment()) {
ToastUtils.showToast("餐盘已在${it.equipmentName}绑定")
return@launchWithLoading
}
}
_currentUserInfo.value = response.data
} else {
_currentUserInfo.value = EquipmentUserInfo(
plateNumber = plateNumber,
equipmentCode = equipmentCode,
updateTime = DateTimeUtils.getDateTimeString()
)
}
}
}
}
@@ -1,10 +1,35 @@
package com.sw.platecabinet.viewmodel
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.request.LoginParam
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.utils.ThreadUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
import timber.log.Timber
class UserViewModel : BaseViewModel() {
// 通过人脸获取到的用户信息
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)
@@ -19,25 +44,100 @@ class UserViewModel : BaseViewModel() {
}
}
fun getUserFaceCache() {
/**
* 获取人脸数据
*/
fun getUserFaceCache(index: Int = 0) {
launchWithLoading {
val response = repository.getUserFaceCache()
if (parseResponse(response)) {
// 获取成功一次后缓存状态
SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true)
val list: List<UserFaceModel> = response.data ?: emptyList()
val faceEntity = list.map {
FaceEntity(it.userId, null, Base64.decode(it.faceFeatureString))
}
withContext(Dispatchers.Default) {
faceApi.updateFaceData(index, faceEntity)
activeEngine()
}
}
}
}
/**
* 激活 arcsoft 人脸
* 激活人脸识别引擎
*/
fun activeEngine(
appId: String = "Hkz1rBk6PZXbS8KwKr67K2eZtsz8bRoMHLg64bUdgCZj",
sdkKey: String = "AabXs3sHM8UhhE7oCGf4LVMLrdkGb1nJNksbjjTdVt7k",
activeKey: String = "085F-118G-Q391-53YL"
) {
// val runtimeABI: RuntimeABI? = FaceEngine.getRuntimeABI()
fun activeEngine() {
Timber.d("activeEngine")
var appId = "Hkz1rBk6PZXbS8KwKr67K2eZtsz8bRoMHLg64bUdgCZj"
var sdkKey = "AabXs3sHM8UhhE7oCGf4LVMLrdkGb1nJNksbjjTdVt7k"
var activeKey = "085F-118G-Q391-53YL"
faceApi.activeEngine(
App.getContext(),
appId,
sdkKey,
activeKey,
object : FaceApi.ActiveCallback {
override fun onSuccess(activeCode: Int) {
Timber.d("activeEngine activeCode = $activeCode")
ThreadUtils.launch {
when (activeCode) {
ErrorInfo.MOK -> {
ToastUtils.showToast("激活引擎成功")
}
ErrorInfo.MERR_ASF_ALREADY_ACTIVATED -> {
// ToastUtils.showToast("引擎已激活,无需再次激活")
}
else -> {
ToastUtils.showToast("激活引擎失败($activeCode)")
}
}
}
}
override fun onFail(e: Exception?) {
ThreadUtils.launch {
ToastUtils.showToast("激活引擎异常,${e?.message}")
}
}
})
}
/**
* 校验码登录
*/
fun loginWithPwd(equipmentCode: String = globalEquipmentCode, phone: String, password: String) {
launchWithLoading {
val loginParam = LoginParam(
appVersion = GlobalData.appVersion,
equipmentCode = equipmentCode,
phone = phone,
password = password
)
val response = repository.equipmentBoxLogin(loginParam)
if (parseResponse(response)) {
_currentUserInfo.value = response.data
}
}
}
/**
* 通过用户id获取用户信息
*/
fun getUserInfoById(equipmentCode: String = globalEquipmentCode, memberId: Int) {
launchWithLoading {
val loginParam = LoginParam(
appVersion = GlobalData.appVersion,
equipmentCode = equipmentCode,
memberId = memberId
)
val response = repository.equipmentBoxLogin(loginParam)
if (parseResponse(response)) {
_currentUserInfo.value = response.data
}
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

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: 467 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 927 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

@@ -2,7 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#14141E"
android:background="@drawable/bg"
android:orientation="vertical">
<include
@@ -33,13 +33,14 @@
android:gravity="center_vertical">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="55dp"
android:src="@mipmap/ic_phone" />
android:src="@drawable/ic_phone" />
<EditText
android:id="@+id/et_phone"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="20dp"
@@ -72,19 +73,20 @@
android:gravity="center_vertical">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="55dp"
android:src="@mipmap/ic_password" />
android:src="@drawable/ic_pwd" />
<EditText
android:id="@+id/et_pwd"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="20dp"
android:background="@android:color/transparent"
android:hint="输入校验码"
android:inputType="textPassword"
android:inputType="numberPassword"
android:maxLines="1"
android:textColor="#F3D2BD"
android:textColorHint="#7B6D6A"
+48 -35
View File
@@ -4,8 +4,9 @@
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#14141E"
android:fitsSystemWindows="false"
android:orientation="vertical"
android:background="@drawable/bg"
tools:context=".activity.LoginByFaceActivity">
<include
@@ -22,53 +23,65 @@
android:id="@+id/tv_tip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="159dp"
android:layout_marginTop="129dp"
android:text="请正视屏幕"
android:textColor="#fff0c8b4"
android:textSize="36sp" />
<FrameLayout
android:layout_width="380dp"
android:layout_height="380dp"
android:layout_marginTop="119dp"
android:padding="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="129dp"
android:gravity="center_vertical"
android:visibility="visible">
<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_margin="10dp"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<FrameLayout
android:layout_width="380dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingTop="1dp">
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="0dp"
android:background="@drawable/circle_dashed_border" />
<TextureView
android:id="@+id/dual_camera_texture_preview_rgb"
android:layout_width="380dp"
android:visibility="visible"
android:layout_height="wrap_content" />
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@mipmap/bg_face" />
</FrameLayout>
<LinearLayout
android:id="@+id/ll_to_pwd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="120dp"
android:background="@drawable/btn_outline_887872"
android:gravity="center"
android:paddingHorizontal="45dp"
android:paddingVertical="18dp">
<com.sw.plate.utils.arcface.FaceRectView
android:id="@+id/dual_camera_face_rect_view"
android:layout_width="match_parent"
android:layout_height="380dp" />
</FrameLayout>
<TextView
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="密码登录"
android:textColor="#ff887872"
android:textSize="24sp" />
</LinearLayout>
android:background="@drawable/bg_face" />
<LinearLayout
android:id="@+id/ll_to_pwd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="208dp"
android:layout_marginTop="500dp"
android:background="@drawable/btn_outline_887872"
android:gravity="center"
android:paddingHorizontal="45dp"
android:paddingVertical="18dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="密码登录"
android:textColor="#ff887872"
android:textSize="24sp" />
</LinearLayout>
</FrameLayout>
</LinearLayout>
</LinearLayout>
+1 -1
View File
@@ -5,7 +5,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#14141E"
android:background="@drawable/bg"
tools:context=".activity.MainActivity">
<include
@@ -15,6 +15,7 @@
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textColor="@color/white"
android:text="请稍等"
android:textSize="22sp" />
<ProgressBar
+26 -16
View File
@@ -1,5 +1,6 @@
<?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"
@@ -11,7 +12,7 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="80dp"
android:layout_marginTop="60dp"
android:text="柜体编号"
android:textColor="#ff7c6d6a"
android:textSize="20sp" />
@@ -20,22 +21,22 @@
android:id="@+id/tv_num"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="23dp"
tools:text="1-12"
android:layout_marginTop="20dp"
android:textColor="#fff0c8b4"
android:textSize="36sp"
android:textStyle="bold" />
android:textStyle="bold"
tools:text="1-12" />
<ImageView
android:layout_width="280dp"
android:layout_height="24dp"
android:layout_marginTop="24dp"
android:src="@mipmap/ic_bind_img" />
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="22dp"
android:src="@drawable/ic_bind_img" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="54dp"
android:layout_marginTop="48dp"
android:orientation="horizontal">
<TextView
@@ -61,7 +62,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_marginTop="23dp"
android:layout_marginTop="21dp"
android:background="@drawable/shape_pwd_bg"
android:gravity="center_vertical">
@@ -71,16 +72,20 @@
android:layout_height="match_parent"
android:background="@color/transparent"
android:gravity="center"
android:maxLines="1"
android:hint="请录入餐盘码"
android:inputType="number"
android:textColor="#fff0c8b4"
android:textColorHint="#ff7c6d6a"
android:textSize="26sp"
android:textStyle="bold"
tools:text="100888" />
tools:text="" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="25dp"
android:layout_marginTop="22dp"
android:orientation="horizontal">
<TextView
@@ -106,7 +111,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_marginTop="23dp"
android:layout_marginTop="22dp"
android:background="@drawable/shape_pwd_bg"
android:gravity="center_vertical">
@@ -117,6 +122,9 @@
android:background="@color/transparent"
android:gravity="center"
android:hint="请输入会员姓名或手机号"
android:imeOptions="actionSearch"
android:inputType="text"
android:maxLines="1"
android:textColor="#F0C8B4"
android:textColorHint="#ff7c6d6a"
android:textSize="26sp" />
@@ -126,13 +134,15 @@
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="188dp"
android:layout_marginTop="24dp" />
android:layout_marginTop="24dp"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2" />
<LinearLayout
android:id="@+id/ll_bind"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_marginTop="28dp"
android:layout_marginTop="24dp"
android:background="@drawable/shape_btn_bg"
android:gravity="center">
@@ -149,7 +159,7 @@
android:id="@+id/tv_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:layout_marginTop="46dp"
android:paddingHorizontal="20dp"
android:text="返回"
android:textColor="#ff887872"
@@ -5,12 +5,13 @@
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:paddingBottom="80dp"
tools:context=".fragment.PlateCabinetFullFragment">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@mipmap/ic_full" />
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_full" />
<TextView
android:layout_width="wrap_content"
@@ -5,6 +5,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:paddingBottom="80dp"
android:orientation="vertical">
<FrameLayout
@@ -27,9 +28,9 @@
android:orientation="vertical">
<ImageView
android:layout_width="60dp"
android:layout_height="60dp"
android:src="@mipmap/ic_plate_open" />
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_plate_open" />
<TextView
android:layout_width="wrap_content"
@@ -7,12 +7,9 @@
android:orientation="vertical"
tools:context=".fragment.SettingListFragment">
<include
android:id="@+id/includeHeader"
layout="@layout/item_title_time" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/mainRecyclerView"
android:layout_marginTop="8dp"
android:layout_width="match_parent"
android:layout_height="match_parent" />
@@ -10,10 +10,10 @@
<ImageView
android:layout_width="280dp"
android:layout_height="24dp"
android:layout_marginTop="92dp"
android:src="@mipmap/ic_unbind_img" />
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="80dp"
android:src="@drawable/ic_unbind_img" />
<TextView
android:layout_width="wrap_content"
@@ -179,7 +179,7 @@
<LinearLayout
android:id="@+id/ll_unbind"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_height="70dp"
android:layout_marginTop="187dp"
android:background="@drawable/shape_btn_bg"
android:gravity="center">
+6 -6
View File
@@ -1,8 +1,8 @@
<?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="75dp"
android:id="@+id/ll_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/grid_item_unbind"
android:gravity="center_vertical"
android:orientation="horizontal"
@@ -27,10 +27,10 @@
android:layout_marginStart="24dp"
android:layout_weight="1"
android:text="未绑定"
android:visibility="gone"
android:textColor="#fff0c8b4"
android:textSize="18sp"
android:textStyle="bold" />
android:textStyle="bold"
android:visibility="gone" />
<LinearLayout
android:id="@+id/ll_bind_info"
@@ -55,10 +55,10 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="10dp"
android:text="9小时前使用"
android:textColor="#585868"
android:textSize="18sp"
android:textStyle="bold" />
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
@@ -3,7 +3,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/grid_item_bind"
android:padding="18dp"
android:padding="12dp"
android:id="@+id/ll_root"
android:orientation="vertical">
+3 -1
View File
@@ -4,13 +4,14 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="24dp">
android:paddingHorizontal="24dp">
<TextView
android:id="@+id/tvLeftDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingTop="20dp"
android:textColor="#F0C8B4"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
@@ -22,6 +23,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingTop="20dp"
android:textColor="#F0C8B4"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 610 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

+2
View File
@@ -10,4 +10,6 @@
<color name="bind_F0C8B4">#F0C8B4</color>
<color name="unbind_F0C8B4">#F0C8B4</color>
<color name="unbind_32283C">#32283C</color>
<color name="divColor">#4c91ceff</color>
</resources>
+4 -1
View File
@@ -1,5 +1,8 @@
<resources>
<string name="app_name">SmartPlateCabinet</string>
<string name="app_name">餐盘柜</string>
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
<string name="specific_engine_init_failed">%s 初始化失败,错误码:%d\n错误码常量名:%s</string>
<string name="permission_denied">权限被拒绝!</string>
</resources>
+1 -1
View File
@@ -1,6 +1,6 @@
[versions]
activityKtx = "1.9.0"
agp = "8.10.1"
agp = "8.4.0"
fragmentKtx = "1.5.6"
kotlin = "2.0.21"
coreKtx = "1.10.1"
+1 -4
View File
@@ -1,6 +1,3 @@
import org.gradle.kotlin.dsl.annotationProcessor
import org.gradle.kotlin.dsl.implementation
plugins {
alias(libs.plugins.android.library)
}
@@ -41,7 +38,7 @@ android {
}
dependencies {
implementation(
api(
fileTree(
mapOf(
"dir" to "libs",
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,751 @@
package com.sw.plate.utils;
import static android.content.Context.TELEPHONY_SERVICE;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.provider.Settings;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
import androidx.core.content.FileProvider;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.net.NetworkInterface;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
public class AppUtil {
public static String getAppPackageName(Context context) {
String packageName = "";
try {
PackageManager pm = context.getPackageManager();
PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0);
packageName = pi.packageName;
if (AppUtil.isEmpty(packageName)) {
return "";
}
} catch (Exception e) {
e.printStackTrace();
}
return packageName;
}
public static String getAppVersionName(Context context) {
String versionName = "";
// int versioncode=1;
try {
PackageManager pm = context.getPackageManager();
PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0);
versionName = pi.versionName;
// versioncode = pi.versionCode;表示更新了多少次
if (versionName == null || versionName.length() <= 0) {
return "";
}
} catch (Exception e) {
e.printStackTrace();
}
return versionName;
}
public static int getAppVersionCode(Context context) {
int versioncode = 1;
try {
PackageManager pm = context.getPackageManager();
PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0);
versioncode = pi.versionCode;
} catch (Exception e) {
e.printStackTrace();
}
return versioncode;
}
//判断微信是否安装
public static boolean isWeixinInstalled(Context context) {
final PackageManager packageManager = context.getPackageManager();// 获取packagemanager
List<PackageInfo> pinfo = packageManager.getInstalledPackages(0);// 获取所有已安装程序的包信息
if (pinfo != null) {
for (int i = 0; i < pinfo.size(); i++) {
String pn = pinfo.get(i).packageName;
if (pn.equals("com.tencent.mm")) {
return true;
}
}
}
return false;
}
/**
* 打电话
* <p>
* Intent.ACTION_DIAL Intent.ACTION_CALL
*
* @param context
* @param mobile
*/
public static void callUp(Context context, String mobile) {
Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:"
+ mobile));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
/**
* 获取设备ID
*
* @param context
* @return
*/
public static String getDevId(Context context) {
TelephonyManager TelephonyMgr = (TelephonyManager) context
.getSystemService(Context.TELEPHONY_SERVICE);
return TelephonyMgr.getDeviceId();
}
/**
* 姓名脱敏
*
* @param fullName
* @return
*/
public static String desensitizedName(String fullName) {
if (fullName == null || fullName.length() <= 1) {
return fullName;
}
char[] nameArr = fullName.toCharArray();
if (nameArr.length > 2) {
for (int i = 1; i < nameArr.length - 1; i++) {
nameArr[i] = '*';
}
} else {
nameArr[1] = '*';
}
return new String(nameArr);
}
public static String formatDateGetFull(String date) {
if (isEmpty(date)) {
return "";
}
Date d = new Date(Long.parseLong(date));
SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd HH:mm");
return dateFormat1.format(d);
}
public static String formatDateGetCurrentTime() {
Date d = new Date(System.currentTimeMillis());
SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//.SSS
return dateFormat1.format(d);
}
public static String formatDateGetFull(long date) {
if (date == 0) {
return "";
}
Date d = new Date(date);
SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
return dateFormat1.format(d);
}
public static String formatDateGetDay(long date) {
if (date == 0) {
return "";
}
Date d = new Date(date);
SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd");
return dateFormat1.format(d);
}
public static boolean isEmpty(String s) {
if (TextUtils.isEmpty(s) || s.trim().equals("null")) {
return true;
} else {
return false;
}
}
/**
* 格式化浮点型
*
* @param data
* @return
*/
public static String formatDouble(double data) {
return new DecimalFormat("0.00").format(data);
}
/**
* 格式化分钟
*
* @param minutes
* @return
*/
public static String formatMinutes(int minutes) {
int hour = minutes / 60;
int minute = minutes % 60;
if (hour > 0 && minute > 0) {
return hour + "小时" + minute + "分钟";
} else if (hour > 0) {
return hour + "小时";
} else {
return minute + "分钟";
}
}
//com.fawan.news
public static void goToMarket(Context context, String packageName) {
Uri uri = Uri.parse("market://details?id=" + packageName);
Intent goToMarket = new Intent(Intent.ACTION_VIEW, uri);
try {
context.startActivity(goToMarket);
} catch (ActivityNotFoundException e) {
e.printStackTrace();
}
}
/**
* true为存在,false为不存在
*
* @param context
* @param packageName
* @return
*/
public static boolean isInstallApp(Context context, String packageName) {
try {
context.getPackageManager().getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES);
return true;
} catch (PackageManager.NameNotFoundException e) {
return false;
}
}
/**
* 格式化float 保留两位小数
*
* @param data
* @return
*/
public static float formatFloat2(float data) {
// DecimalFormat decimalFormat = new DecimalFormat("0.00");//构造方法的字符格式这里如果小数不足2位,会以0补足.
// return decimalFormat.format(data);//返回字符串
int scale = 1;//设置位数
int roundingMode = 4;//表示四舍五入,可以选择其他舍值方式,例如去尾,等等.
BigDecimal bd = new BigDecimal((double) data);
bd = bd.setScale(scale, roundingMode);
data = bd.floatValue();
return data;
}
/**
* Android 6.0 之前(不包括6.0)获取mac地址
* 必须的权限 <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"></uses-permission>
*
* @param context * @return
*/
public static String getMacDefault(Context context) {
String mac = "";
if (context == null) {
return mac;
}
WifiManager wifi = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
WifiInfo info = null;
try {
info = wifi.getConnectionInfo();
} catch (Exception e) {
e.printStackTrace();
}
if (info == null) {
return null;
}
mac = info.getMacAddress();
if (!TextUtils.isEmpty(mac)) {
mac = mac.toUpperCase(Locale.ENGLISH);
}
return mac;
}
/**
* Android 6.0-Android 7.0 获取mac地址
*/
public static String getMacAddress() {
String macSerial = null;
String str = "";
try {
Process pp = Runtime.getRuntime().exec("cat/sys/class/net/wlan0/address");
InputStreamReader ir = new InputStreamReader(pp.getInputStream());
LineNumberReader input = new LineNumberReader(ir);
while (null != str) {
str = input.readLine();
if (str != null) {
macSerial = str.trim();//去空格
break;
}
}
} catch (IOException ex) {
// 赋予默认值
ex.printStackTrace();
}
return macSerial;
}
/**
* Android 7.0之后获取Mac地址
* 遍历循环所有的网络接口,找到接口是 wlan0
* 必须的权限 <uses-permission android:name="android.permission.INTERNET"></uses-permission>
*
* @return
*/
public static String getMacFromHardware() {
try {
ArrayList<NetworkInterface> all = Collections.list(NetworkInterface.getNetworkInterfaces());
for (NetworkInterface nif : all) {
if (!nif.getName().equals("wlan0"))
continue;
byte[] macBytes = nif.getHardwareAddress();
if (macBytes == null) return "";
StringBuilder res1 = new StringBuilder();
for (Byte b : macBytes) {
res1.append(String.format("%02X:", b));
}
if (!TextUtils.isEmpty(res1)) {
res1.deleteCharAt(res1.length() - 1);
}
return res1.toString();
}
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
/**
* 获取mac地址(适配所有Android版本)
*
* @return
*/
public static String getMac(Context context) {
String mac = "";
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
mac = getMacDefault(context);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
mac = getMacAddress();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
mac = getMacFromHardware();
}
return mac;
}
//把String转化为float
public static double convertToFloat(String number, double defaultValue) {
if (TextUtils.isEmpty(number)) {
return defaultValue;
}
try {
return Double.parseDouble(number);
} catch (Exception e) {
return defaultValue;
}
}
/**
* 获取AndroidId
*
* @param context
* @return
*/
public static String getAndroidId(Context context) {
String androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
return androidId;
}
/**
* 获取设备唯一 UDID
*
* @param context
* @return
*/
@SuppressLint("MissingPermission")
public static String getUDID(Context context) {
// String androidID = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
// L.e("androidID===" + androidID);
// return androidID;
String androidID = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
if (!androidID.equals("")) {
try {
if (!"9774d56d682e549c".equals(androidID)) {
androidID = UUID.nameUUIDFromBytes(androidID.getBytes("utf8")).toString();
} else {
@SuppressLint("MissingPermission") final String deviceId = ((TelephonyManager) context.getSystemService(TELEPHONY_SERVICE)).getDeviceId();
androidID = deviceId != null ? UUID.nameUUIDFromBytes(deviceId.getBytes("utf8")).toString() : UUID.randomUUID().toString();
}
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
return androidID;
}
//需要权限 android.permission.READ_PHONE_STATE
TelephonyManager TelephonyMgr = (TelephonyManager) context.getSystemService(TELEPHONY_SERVICE);
String szImei = TelephonyMgr.getDeviceId();
if (!szImei.equals("")) {
return szImei;
}
//需要权限 android.permission.ACCESS_WIFI_STATE
WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
String m_szWLANMAC = wm.getConnectionInfo().getMacAddress();
if (!m_szWLANMAC.equals("")) {
return m_szWLANMAC;
}
//需要权限 android.permission.BLUETOOTH
BluetoothAdapter m_BluetoothAdapter = null;
m_BluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
String m_szBTMAC = m_BluetoothAdapter.getAddress();
if (!m_szBTMAC.equals("")) {
return m_szBTMAC;
}
return getUniquePsuedoID();
}
//获得 Psuedo ID
public static String getUniquePsuedoID() {
String serial = null;
String m_szDevIDShort = "35" +
Build.BOARD.length() % 10 + Build.BRAND.length() % 10 +
Build.CPU_ABI.length() % 10 + Build.DEVICE.length() % 10 +
Build.DISPLAY.length() % 10 + Build.HOST.length() % 10 +
Build.ID.length() % 10 + Build.MANUFACTURER.length() % 10 +
Build.MODEL.length() % 10 + Build.PRODUCT.length() % 10 +
Build.TAGS.length() % 10 + Build.TYPE.length() % 10 +
Build.USER.length() % 10; //13 位
try {
serial = Build.class.getField("SERIAL").get(null).toString();
//API>=9 使用serial号
return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();
} catch (Exception exception) {
//serial需要一个初始化,随意值
serial = "serial";
}
//使用硬件信息拼凑出来的15位号码
return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();
}
public static String getCPUSerial() {
String line = "";
String TAG = "aaa";
Log.e(TAG, " get_quck_Sn() ");
Class<?> c = null;
try {
c = Class.forName("android.os.SystemProperties");
Method get = c.getMethod("get", String.class);
line = (String) get.invoke(c, "ro.serialno");
} catch (ClassNotFoundException | NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
Log.e(TAG, " get_quck_Sn() " + line);
System.out.println("设备串号" + line);
return line;
}
/**
* 判断网络连接状态
*
* @param context
* @return
*/
public static boolean isNetworkConnected(Context context) {
if (context != null) {
ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo();
if (mNetworkInfo != null) {
return mNetworkInfo.isAvailable();
}
}
return false;
}
/**
* 判断WiFi连接状态
*
* @param context
* @return
*/
public static boolean isWifiConnected(Context context) {
if (context != null) {
ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo mWiFiNetworkInfo = mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
if (mWiFiNetworkInfo != null) {
return mWiFiNetworkInfo.isAvailable();
}
}
return false;
}
/**
* 判断移动网络状态
*
* @param context
* @return
*/
public static boolean isMobileConnected(Context context) {
if (context != null) {
ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo mMobileNetworkInfo = mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
if (mMobileNetworkInfo != null) {
return mMobileNetworkInfo.isAvailable();
}
}
return false;
}
/**
* 获取网络连接类型
*
* @param context
* @return
*/
public static int getConnectedType(Context context) {
if (context != null) {
ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo();
if (mNetworkInfo != null && mNetworkInfo.isAvailable()) {
return mNetworkInfo.getType();
}
}
return -1;
}
/**
* 根据字符的起始和结束索引提取子串
*
* @param input 原始字符串
* @param startIndex 起始索引(包含,从0开始)
* @param endIndex 结束索引(不包含)
* @return 子串,若输入无效或索引越界则返回空字符串
*/
public static String getSubstringByIndices(String input, int startIndex, int endIndex) {
if (input == null) {
return "";
}
// 处理索引越界问题
int safeStart = Math.max(startIndex, 0);
int safeEnd = Math.min(endIndex, input.length());
if (safeStart > safeEnd) {
return "";
}
return input.substring(safeStart, safeEnd);
}
public static String getSubstringByIndex(String input, int startIndex, int length) {
if (input == null) {
return "";
}
// 处理索引越界问题
int safeStart = Math.max(startIndex, 0);
int safeEnd = Math.min(startIndex + length, input.length());
if (safeStart > safeEnd) {
return "";
}
return input.substring(safeStart, safeEnd);
}
/**
* 十进制转十六进制
*
* @param decimal
* @return
*/
public static String decimalToHexWithPadding(int decimal, int padding) {
// 将十进制转换为十六进制,并转换为字符串
String hex = Integer.toHexString(decimal);
// 确保字符串长度为至少4位,不足部分前面补0
while (hex.length() < padding) {
hex = "0" + hex;
}
return hex.toUpperCase(); // 返回大写形式的十六进制字符串
}
/**
* 十进制转二进制,且返回的二进制为至少7位数
*
* @param decimal
* @return
*/
public static String decimalToBinary(int decimal) {
// 如果输入为0,直接返回"0"
if (decimal == 0) {
return "0";
}
StringBuilder binary = new StringBuilder();
// 除2取余法,将余数加入二进制字符串
while (decimal > 0) {
int remainder = decimal % 2;
binary.insert(0, remainder);
decimal = decimal / 2;
}
int length = binary.length();
if (length < 7) {
int padding = 7 - length;
for (int i = 0; i < padding; i++) {
binary.insert(0, '0');
}
}
return binary.toString();
}
/**
* 将二进制字符串转换为十六进制字符串,每8位转换为两位十六进制,不足两位前面补零
*
* @param binaryStr 输入的二进制字符串(仅包含0和1)
* @return 转换后的十六进制字符串
* @throws IllegalArgumentException 如果输入不是有效的二进制字符串
*/
public static String binaryToHex(String binaryStr) {
// 校验输入合法性
if (binaryStr == null || !binaryStr.matches("[01]+")) {
throw new IllegalArgumentException("Invalid binary string");
}
// 补前导零使长度成为8的倍数
int length = binaryStr.length();
int padding = (8 - (length % 8)) % 8; // 计算需要补零的数量
StringBuilder paddedBinary = new StringBuilder();
for (int i = 0; i < padding; i++) {
paddedBinary.append('0');
}
paddedBinary.append(binaryStr);
// 每8位转换为两位十六进制
StringBuilder hexStr = new StringBuilder();
for (int i = 0; i < paddedBinary.length(); i += 8) {
String byteStr = paddedBinary.substring(i, i + 8);
int decimalValue = Integer.parseInt(byteStr, 2);
hexStr.append(String.format("%02X", decimalValue & 0xFF));
}
return hexStr.toString();
}
/**
* 数据校验 异或处理
*/
public static String getXor(String content) {
int a = 0;
for (int i = 0; i < content.length() / 2; i++) {
a = a ^ Integer.parseInt(content.substring(i * 2, (i * 2) + 2), 16);
}
String result = Integer.toHexString(a).toUpperCase();
if (result.length() == 1) {
return "0" + result;
} else {
return result;
}
}
public static double formatPersonInfo(String input, int startIndex, int length) {
if (input == null) {
return 0;
}
// 处理索引越界问题
int safeStart = Math.max(startIndex, 0);
int safeEnd = Math.min(startIndex + length, input.length());
if (safeStart > safeEnd) {
return 0;
}
String result = input.substring(safeStart, safeEnd);
double num = Integer.parseInt(result, 16);
return num;
}
/**
* 安装apk
*
* @param activity
* @param apkFile
*/
public static void installApk(Context activity, File apkFile) {
//文件有所有者概念,现在是属于当前进程的,需要把这个文件暴露给系统安装程序(其他进程)去安装
//因此,可能会存在权限问题,需要做下面的设置
//如果文件是sdcard上的,就不需要这个操作了
try {
apkFile.setExecutable(true, false);
apkFile.setReadable(true, false);
apkFile.setWritable(true, false);
} catch (Exception e) {
e.printStackTrace();
}
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(Intent.ACTION_VIEW);
Uri uri;
//TODO N FileProvider
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
uri = FileProvider.getUriForFile(activity, activity.getPackageName() + ".fileProvider", apkFile);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
} else {
uri = Uri.fromFile(apkFile);
}
intent.setDataAndType(uri, "application/vnd.android.package-archive");
activity.startActivity(intent);
//TODO 0 INSTALL PERMISSION
//在AndroidManifest中加入权限即可
}
}
@@ -0,0 +1,265 @@
package com.sw.plate.utils;
public final class Base64 {
private static final int BASELENGTH = 128;
private static final int LOOKUPLENGTH = 64;
private static final int TWENTYFOURBITGROUP = 24;
private static final int EIGHTBIT = 8;
private static final int SIXTEENBIT = 16;
private static final int FOURBYTE = 4;
private static final int SIGN = -128;
private static char PAD = '=';
private static byte[] base64Alphabet = new byte[BASELENGTH];
private static char[] lookUpBase64Alphabet = new char[LOOKUPLENGTH];
static {
for (int i = 0; i < BASELENGTH; ++i) {
base64Alphabet[i] = -1;
}
for (int i = 'Z'; i >= 'A'; i--) {
base64Alphabet[i] = (byte) (i - 'A');
}
for (int i = 'z'; i >= 'a'; i--) {
base64Alphabet[i] = (byte) (i - 'a' + 26);
}
for (int i = '9'; i >= '0'; i--) {
base64Alphabet[i] = (byte) (i - '0' + 52);
}
base64Alphabet['+'] = 62;
base64Alphabet['/'] = 63;
for (int i = 0; i <= 25; i++) {
lookUpBase64Alphabet[i] = (char) ('A' + i);
}
for (int i = 26, j = 0; i <= 51; i++, j++) {
lookUpBase64Alphabet[i] = (char) ('a' + j);
}
for (int i = 52, j = 0; i <= 61; i++, j++) {
lookUpBase64Alphabet[i] = (char) ('0' + j);
}
lookUpBase64Alphabet[62] = (char) '+';
lookUpBase64Alphabet[63] = (char) '/';
}
private static boolean isWhiteSpace(char octect) {
return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9);
}
private static boolean isPad(char octect) {
return (octect == PAD);
}
private static boolean isData(char octect) {
return (octect < BASELENGTH && base64Alphabet[octect] != -1);
}
/**
* Encodes hex octects into Base64
*
* @param binaryData Array containing binaryData
* @return Encoded Base64 array
*/
public static String encode(byte[] binaryData) {
if (binaryData == null) {
return null;
}
int lengthDataBits = binaryData.length * EIGHTBIT;
if (lengthDataBits == 0) {
return "";
}
int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP;
int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP;
int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1
: numberTriplets;
char encodedData[] = null;
encodedData = new char[numberQuartet * 4];
byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0;
int encodedIndex = 0;
int dataIndex = 0;
for (int i = 0; i < numberTriplets; i++) {
b1 = binaryData[dataIndex++];
b2 = binaryData[dataIndex++];
b3 = binaryData[dataIndex++];
l = (byte) (b2 & 0x0f);
k = (byte) (b1 & 0x03);
byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2)
: (byte) ((b1) >> 2 ^ 0xc0);
byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4)
: (byte) ((b2) >> 4 ^ 0xf0);
byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6)
: (byte) ((b3) >> 6 ^ 0xfc);
encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)];
encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3];
encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f];
}
// form integral number of 6-bit groups
if (fewerThan24bits == EIGHTBIT) {
b1 = binaryData[dataIndex];
k = (byte) (b1 & 0x03);
byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2)
: (byte) ((b1) >> 2 ^ 0xc0);
encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4];
encodedData[encodedIndex++] = PAD;
encodedData[encodedIndex++] = PAD;
} else if (fewerThan24bits == SIXTEENBIT) {
b1 = binaryData[dataIndex];
b2 = binaryData[dataIndex + 1];
l = (byte) (b2 & 0x0f);
k = (byte) (b1 & 0x03);
byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2)
: (byte) ((b1) >> 2 ^ 0xc0);
byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4)
: (byte) ((b2) >> 4 ^ 0xf0);
encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)];
encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2];
encodedData[encodedIndex++] = PAD;
}
return new String(encodedData);
}
/**
* Decodes Base64 data into octects
*
* @param encoded string containing Base64 data
* @return Array containind decoded data.
*/
public static byte[] decode(String encoded) {
if (encoded == null) {
return null;
}
char[] base64Data = encoded.toCharArray();
// remove white spaces
int len = removeWhiteSpace(base64Data);
if (len % FOURBYTE != 0) {
return null;// should be divisible by four
}
int numberQuadruple = (len / FOURBYTE);
if (numberQuadruple == 0) {
return new byte[0];
}
byte decodedData[] = null;
byte b1 = 0, b2 = 0, b3 = 0, b4 = 0;
char d1 = 0, d2 = 0, d3 = 0, d4 = 0;
int i = 0;
int encodedIndex = 0;
int dataIndex = 0;
decodedData = new byte[(numberQuadruple) * 3];
for (; i < numberQuadruple - 1; i++) {
if (!isData((d1 = base64Data[dataIndex++]))
|| !isData((d2 = base64Data[dataIndex++]))
|| !isData((d3 = base64Data[dataIndex++]))
|| !isData((d4 = base64Data[dataIndex++]))) {
return null;
}// if found "no data" just return null
b1 = base64Alphabet[d1];
b2 = base64Alphabet[d2];
b3 = base64Alphabet[d3];
b4 = base64Alphabet[d4];
decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);
}
if (!isData((d1 = base64Data[dataIndex++]))
|| !isData((d2 = base64Data[dataIndex++]))) {
return null;// if found "no data" just return null
}
b1 = base64Alphabet[d1];
b2 = base64Alphabet[d2];
d3 = base64Data[dataIndex++];
d4 = base64Data[dataIndex++];
if (!isData((d3)) || !isData((d4))) {// Check if they are PAD characters
if (isPad(d3) && isPad(d4)) {
if ((b2 & 0xf) != 0)// last 4 bits should be zero
{
return null;
}
byte[] tmp = new byte[i * 3 + 1];
System.arraycopy(decodedData, 0, tmp, 0, i * 3);
tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4);
return tmp;
} else if (!isPad(d3) && isPad(d4)) {
b3 = base64Alphabet[d3];
if ((b3 & 0x3) != 0)// last 2 bits should be zero
{
return null;
}
byte[] tmp = new byte[i * 3 + 2];
System.arraycopy(decodedData, 0, tmp, 0, i * 3);
tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
return tmp;
} else {
return null;
}
} else { // No PAD e.g 3cQl
b3 = base64Alphabet[d3];
b4 = base64Alphabet[d4];
decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);
}
return decodedData;
}
/**
* remove WhiteSpace from MIME containing encoded Base64 data.
*
* @param data the byte array of base64 data (with WS)
* @return the new length
*/
private static int removeWhiteSpace(char[] data) {
if (data == null) {
return 0;
}
// count characters that's not whitespace
int newSize = 0;
int len = data.length;
for (int i = 0; i < len; i++) {
if (!isWhiteSpace(data[i])) {
data[newSize++] = data[i];
}
}
return newSize;
}
}
@@ -155,12 +155,10 @@ public class ScanGunKeyEventHelper {
* @return
*/
public boolean isScanGunEvent(KeyEvent event) {
// L.e("event===" + event.getDevice().getName() +
// "===Char===" + event.getCharacters() +
// "===Action===" + event.getAction());
// return event.getDevice().getName().equals(mDeviceName);
return true;
L.e("event===" + event.getDevice().getName() +
"===Char===" + event.getCharacters() +
"===Action===" + event.getAction());
return event.getDevice().getName().equals(mDeviceName);
}
@@ -1,6 +1,7 @@
package com.sw.plate.utils.arcface;
import android.content.Context;
import android.util.Log;
import com.arcsoft.face.FaceEngine;
import com.arcsoft.face.enums.RuntimeABI;
@@ -8,12 +9,14 @@ import com.sw.plate.App;
import com.sw.plate.utils.L;
import com.sw.plate.utils.arcface.face.model.FacePreviewInfo;
import com.sw.plate.utils.arcface.facedb.FaceDatabase;
import com.sw.plate.utils.arcface.facedb.dao.FaceDao;
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity;
import java.util.ArrayList;
import java.util.List;
public class FaceApi {
private static final String TAG = "FaceApi";
public interface ActiveCallback {
void onSuccess(int code);
@@ -27,13 +30,13 @@ public class FaceApi {
* @param list
*/
public void updateFaceData(int index, List<FaceEntity> list) {
Log.d(TAG, "updateFaceData: index = " + index + ", listSize = " + list.size());
FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao();
if (index == 0) {
FaceDatabase.getInstance(App.getContext()).faceDao().deleteAll();
FaceDatabase.getInstance(App.getContext()).faceDao().resetId();
faceDao.deleteAll();
faceDao.resetId();
}
List<FaceEntity> faceEntities = new ArrayList<>();
FaceDatabase.getInstance(App.getContext()).faceDao().insert(faceEntities);
faceDao.insert(list);
}
/**
@@ -77,7 +77,7 @@ public class FaceRectTransformer {
rect.bottom *= verticalRatio;
Rect newRect = new Rect();
L.e("cameraDisplayOrientation" + cameraDisplayOrientation + "===" + cameraId);
L.e("cameraDisplayOrientation " + cameraDisplayOrientation + " === " + cameraId);
switch (cameraDisplayOrientation) {
case 0:
if (cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) {
@@ -2,6 +2,8 @@ package com.sw.plate.utils.arcface.face;
import android.graphics.Rect;
import android.hardware.Camera;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import androidx.annotation.IntDef;
@@ -47,7 +49,6 @@ import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import io.reactivex.Observable;
import io.reactivex.Observer;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
@@ -529,7 +530,7 @@ public class FaceHelper implements FaceListener {
if (recognizeInfoMap.containsKey(trackId)) {
onFaceFeatureInfoGet(faceFeature, trackId, errorCode);
}
} catch (InterruptedException e) {
} catch (Exception e) {
Log.e(TAG, "onFaceFeatureInfoGet: 等待活体结果时退出界面会执行,正常现象,可注释异常代码块");
e.printStackTrace();
}
@@ -549,40 +550,35 @@ public class FaceHelper implements FaceListener {
}
}
// 定义一个Handler,用于处理延迟任务
private Handler delayHandler = new Handler(Looper.getMainLooper());
/**
* 延迟 {@link RecognizeConfiguration#getLivenessFailedRetryInterval()}后,重新进行活体检测
*
* @param trackId 人脸ID
*/
private void retryLivenessDetectDelayed(final Integer trackId) {
Observable.timer(recognizeConfiguration.getLivenessFailedRetryInterval(), TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<Long>() {
Disposable disposable;
Log.d(TAG, "retryLivenessDetectDelayed: trackId = " + trackId + ", threadName =" + Thread.currentThread().getName());
Log.d(TAG, "retryLivenessDetectDelayed: Interval = " + recognizeConfiguration.getLivenessFailedRetryInterval());
@Override
public void onSubscribe(Disposable d) {
disposable = d;
delayFaceTaskCompositeDisposable.add(disposable);
}
// 创建延迟任务
Runnable delayTask = new Runnable() {
@Override
public void run() {
// 将该人脸状态置为UNKNOWN,帧回调处理时会重新进行活体检测
changeLiveness(trackId, LivenessInfo.UNKNOWN);
// 从集合中移除这个任务
delayFaceTaskList.remove(this);
}
};
@Override
public void onNext(Long aLong) {
// 将任务添加到集合中管理
delayFaceTaskList.add(delayTask);
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
}
@Override
public void onComplete() {
// 将该人脸状态置为UNKNOWN,帧回调处理时会重新进行活体检测
changeLiveness(trackId, LivenessInfo.UNKNOWN);
delayFaceTaskCompositeDisposable.remove(disposable);
}
});
// 发送延迟任务
delayHandler.postDelayed(delayTask, recognizeConfiguration.getLivenessFailedRetryInterval());
}
/**
@@ -591,39 +587,33 @@ public class FaceHelper implements FaceListener {
* @param trackId 人脸ID
*/
private void retryRecognizeDelayed(final Integer trackId) {
Log.d(TAG, "retryRecognizeDelayed: trackId = " + trackId);
changeRecognizeStatus(trackId, RequestFeatureStatus.FAILED);
Observable.timer(recognizeConfiguration.getRecognizeFailedRetryInterval(), TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<Long>() {
Disposable disposable;
@Override
public void onSubscribe(Disposable d) {
disposable = d;
delayFaceTaskCompositeDisposable.add(disposable);
}
// 创建延迟任务
Runnable delayTask = new Runnable() {
@Override
public void run() {
// 将该人脸特征提取状态置为FAILED,帧回调处理时会重新进行活体检测
changeRecognizeStatus(trackId, RequestFeatureStatus.TO_RETRY);
// 从集合中移除这个任务
delayFaceTaskList.remove(this);
}
};
@Override
public void onNext(Long aLong) {
// 将任务添加到集合中管理
delayFaceTaskList.add(delayTask);
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
}
@Override
public void onComplete() {
// 将该人脸特征提取状态置为FAILED,帧回调处理时会重新进行活体检测
changeRecognizeStatus(trackId, RequestFeatureStatus.TO_RETRY);
delayFaceTaskCompositeDisposable.remove(disposable);
}
});
// 发送延迟任务
delayHandler.postDelayed(delayTask, recognizeConfiguration.getRecognizeFailedRetryInterval());
}
// 需要在类中定义一个集合来管理所有的延迟任务,替代原来的CompositeDisposable
private List<Runnable> delayFaceTaskList = new ArrayList<>();
@Override
public void onFaceLivenessInfoGet(@Nullable LivenessInfo livenessInfo, Integer trackId, Integer errorCode) {
Log.d(TAG, "onFaceLivenessInfoGet: trackId = " + trackId);
if (livenessInfo != null) {
int liveness = livenessInfo.getLiveness();
Log.i(TAG, "onFaceLivenessInfoGet liveness:" + liveness);
@@ -0,0 +1,49 @@
package com.sw.plate.utils.arcface.face;
import android.util.Log;
import java.util.concurrent.TimeUnit;
import io.reactivex.Observable;
import io.reactivex.Observer;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
public class Test {
private static final String TAG = "Test";
private CompositeDisposable delayFaceTaskCompositeDisposable = new CompositeDisposable();
public void test1() {
Log.d(TAG, "test1: ");
Observable.timer(10, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<Long>() {
Disposable disposable;
@Override
public void onSubscribe(Disposable d) {
disposable = d;
delayFaceTaskCompositeDisposable.add(disposable);
}
@Override
public void onNext(Long value) {
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
}
@Override
public void onComplete() {
Log.d(TAG, "onComplete: ");
delayFaceTaskCompositeDisposable.remove(disposable);
}
});
}
}
@@ -9,8 +9,6 @@ import androidx.room.RoomDatabase;
import com.sw.plate.utils.arcface.facedb.dao.FaceDao;
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity;
import java.io.File;
@Database(entities = {FaceEntity.class}, version = 1, exportSchema = false)
public abstract class FaceDatabase extends RoomDatabase {
public abstract FaceDao faceDao();
@@ -22,7 +20,9 @@ public abstract class FaceDatabase extends RoomDatabase {
synchronized (FaceDatabase.class) {
if (faceDatabase == null) {
faceDatabase = Room.databaseBuilder(context, FaceDatabase.class,
context.getExternalFilesDir("database") + File.separator + "faceDB.db").build();
context.getDatabasePath("faceDB.db").getPath()
// context.getExternalFilesDir("database") + File.separator + "faceDB.db"
).build();
}
}
}
@@ -4,7 +4,6 @@ import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.os.Environment;
import android.util.Log;
import com.arcsoft.face.ErrorInfo;
@@ -354,8 +353,11 @@ public class FaceServer {
* @return 存放注册照的文件夹路径
*/
private String getImageDir() {
return App.getContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES)
+ File.separator + "faceDB" + File.separator + "registerFaces";
// return App.getContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES)
// + File.separator + "faceDB" + File.separator + "registerFaces";
return App.getContext().getFilesDir()
+ File.separator + "faceDB"
+ File.separator + "registerFaces";
}
/**
@@ -584,7 +586,7 @@ public class FaceServer {
}
}
} catch (IllegalArgumentException exception) {
Log.i(TAG, "exception:" + exception.getMessage());
Log.i(TAG, "searchFaceFeature exception:" + exception.getMessage());
}
return null;
}
@@ -0,0 +1,37 @@
package com.sw.plate.utils.comn;
import static com.sw.plate.utils.CabinetLockCommand.generateOpenCommand;
import android.serialport.SerialPort;
public class SerialApi {
private static String path = "/dev/ttyS2";
private static int speed = 19200;
private static SerialPortManager serialPortManager;
private static SerialPort serialPort;
public static void init() {
serialPortManager = SerialPortManager.instance();
serialPort = serialPortManager.open(new Device(path, String.valueOf(speed)));
// if (serialPort == null) {
// ToastUtils.showToast("打开串口失败");
// }
}
/**
* 开柜
*
* @param boxNumber
* @param callback
*/
public static void openPlate(int boxNumber, SerialPortManager.SendCallback callback) {
if (serialPort == null) {
init();
}
if (serialPort == null) {
callback.onFail(new Exception("打开串口失败"));
return;
}
serialPortManager.sendCommand(generateOpenCommand(boxNumber), callback);
}
}
@@ -153,7 +153,7 @@ public class SerialPortManager {
/**
* 发送命令包
*/
public void sendCommand(final String command) {
public void sendCommand(final String command, SendCallback callback) {
// TODO: 2018/3/22
L.e("发送命令:" + command);
@@ -168,11 +168,13 @@ public class SerialPortManager {
@Override
public void onNext(Object o) {
// LogManager.instance().post(new SendMessage(command));
callback.onSuccess();
}
@Override
public void onError(Throwable e) {
L.e("发送失败" + e);
callback.onFail(new Exception(e));
}
@Override
@@ -181,4 +183,10 @@ public class SerialPortManager {
}
});
}
public interface SendCallback {
void onSuccess();
void onFail(Exception e);
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -22,4 +22,4 @@
android:textSize="28sp" />
</com.google.android.material.card.MaterialCardView>
</androidx.cardview.widget.CardView>