diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c6f91cb..12b6289 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -25,10 +25,6 @@ android:exported="true"> - @@ -37,6 +33,12 @@ + + + diff --git a/app/src/main/java/com/sw/platecabinet/GlobalData.kt b/app/src/main/java/com/sw/platecabinet/GlobalData.kt index ce742a2..60523d5 100644 --- a/app/src/main/java/com/sw/platecabinet/GlobalData.kt +++ b/app/src/main/java/com/sw/platecabinet/GlobalData.kt @@ -1,7 +1,25 @@ package com.sw.platecabinet object GlobalData { + /** + * 同一设备全局使用的设备编号 + */ + var globalEquipmentCode: String = "202501171634" + /** + * 横排数量 + */ + var arrayCross: Int = 2 + + /** + * 竖排数量 + */ + var arrayVertical: Int = 11 + + /** + * 排列方式 + */ + var arrayMode: Int = 0 } /** diff --git a/app/src/main/java/com/sw/platecabinet/activity/BaseActivity.kt b/app/src/main/java/com/sw/platecabinet/activity/BaseActivity.kt index 477f7a5..5eaf62b 100644 --- a/app/src/main/java/com/sw/platecabinet/activity/BaseActivity.kt +++ b/app/src/main/java/com/sw/platecabinet/activity/BaseActivity.kt @@ -2,12 +2,16 @@ 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.View +import android.view.WindowInsetsController +import android.view.WindowManager import android.widget.TextView import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.WindowCompat import androidx.viewbinding.ViewBinding import com.sw.inbound.utils.DateTimeUtils import com.sw.platecabinet.R @@ -19,7 +23,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import timber.log.Timber /** * activity 基类 @@ -30,54 +33,123 @@ abstract class BaseActivity : AppCompatActivity() { protected lateinit var context: Context private var timeJob: Job? = null private var mDialogWaiting: CustomDialog? = null + private val permissionHelpers = mutableMapOf() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() context = this + disableSystemUICompletely() + // 确保内容延伸到导航栏区域 + WindowCompat.setDecorFitsSystemWindows(window, false) + binding = inflateViewBinding() headerBinding = inflateTitleBinding() setContentView(binding.root) updateTime() + registerDataChange() initialize() } 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, 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,6 +177,8 @@ abstract class BaseActivity : AppCompatActivity() { protected abstract fun initialize() + protected open fun registerDataChange() {} + override fun onDestroy() { timeJob?.cancel() super.onDestroy() diff --git a/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt b/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt index 1ca3f53..27f4896 100644 --- a/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt +++ b/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt @@ -2,30 +2,53 @@ 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.launch import timber.log.Timber /** * 人脸识别 */ -class LoginByFaceActivity : BaseActivity() { - private lateinit var cameraProviderFuture: ListenableFuture +class LoginByFaceActivity : BaseActivity(), + ViewTreeObserver.OnGlobalLayoutListener { private val viewModel by viewModels() + private val recognizeViewModel by viewModels() private val CAMERA_PERMISSION_REQUEST_CODE = 100 - private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA) + private val REQUIRED_PERMISSIONS: Array = arrayOf( + Manifest.permission.CAMERA, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE + ) override fun inflateViewBinding(): ActivityLoginFaceBinding { return ActivityLoginFaceBinding.inflate(layoutInflater) @@ -38,7 +61,10 @@ class LoginByFaceActivity : BaseActivity() { override fun initialize() { // 请求权限 checkCameraPermission() - initView() + initArcViewModel() + initArcView() + openRectInfoDraw = true + recognizeViewModel.setDrawRectInfoTextValue(true) viewModel.generateToken() binding.llToPwd.setOnClickListener { val intent = Intent(this, LoginByPwdActivity::class.java) @@ -46,53 +72,314 @@ class LoginByFaceActivity : BaseActivity() { } } - private fun checkCameraPermission() { - PermissionHelper.with(this, REQUIRED_PERMISSIONS, CAMERA_PERMISSION_REQUEST_CODE) - .onGranted { - // 权限已授予 - startCamera() + override fun registerDataChange() { + lifecycleScope.launch { + viewModel.currentUserInfo.collect { + Timber.d("currentUserInfo it = $it") + if (it != null) { + ToastUtils.showToast("登录成功") + MainActivity.start(context, pageType = PageType.PLATE_CABINET_FULL) + finish() + } } - .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) + } + lifecycleScope.launch { + viewModel.showLoading.collect { + if (it) { + showWaitingDialog("") + } else { + hideWaitingDialog() + } } } - 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() { + val permissionHelper = + PermissionHelper.with(this, REQUIRED_PERMISSIONS, CAMERA_PERMISSION_REQUEST_CODE) + .onGranted { + // 权限已授予 + 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) + }) + } + + 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的宽高,使2个预览同时显示 + * + * @param previewView 显示预览数据的view + * @param faceRectView 画框的view + * @param previewSize 预览大小 + * @param displayOrientation 相机旋转角度 + * @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, 1.0f + ) + 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? = + 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(90) // 角度 + .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) { + if (rgbFaceRectTransformer != null) { + val rgbDrawInfoList: MutableList? = 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) + recognizeViewModel.init() + initRgbCamera() + resumeCamera() + // countDownTimer.start(); + } + + + // @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) } } \ No newline at end of file diff --git a/app/src/main/java/com/sw/platecabinet/activity/LoginByPwdActivity.kt b/app/src/main/java/com/sw/platecabinet/activity/LoginByPwdActivity.kt index a7d1a05..d6030a6 100644 --- a/app/src/main/java/com/sw/platecabinet/activity/LoginByPwdActivity.kt +++ b/app/src/main/java/com/sw/platecabinet/activity/LoginByPwdActivity.kt @@ -1,10 +1,18 @@ 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.platecabinet.databinding.ActivityLoginByPwdBinding import com.sw.platecabinet.databinding.ItemTitleTimeBinding +import com.sw.platecabinet.viewmodel.UserViewModel +import kotlinx.coroutines.launch +import timber.log.Timber class LoginByPwdActivity : BaseActivity() { + private val viewModel by viewModels() + override fun inflateViewBinding(): ActivityLoginByPwdBinding { return ActivityLoginByPwdBinding.inflate(layoutInflater) } @@ -15,7 +23,13 @@ class LoginByPwdActivity : BaseActivity() { 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("手机或校验码不能为空") + return@setOnClickListener + } + viewModel.loginWithPwd(phone = phone, password = pwd) } binding.tvFaceRec.setOnClickListener { val intent = Intent(this, LoginByFaceActivity::class.java) @@ -23,4 +37,26 @@ class LoginByPwdActivity : BaseActivity() { } } + override fun registerDataChange() { + lifecycleScope.launch { + viewModel.currentUserInfo.collect { + Timber.d("currentUserInfo it = $it") + if (it != null) { + ToastUtils.showToast("登录成功") + MainActivity.start(context, pageType = PageType.PLATE_CABINET_FULL) + finish() + } + } + } + lifecycleScope.launch { + viewModel.showLoading.collect { + if (it) { + showWaitingDialog("") + } else { + hideWaitingDialog() + } + } + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/sw/platecabinet/activity/MainActivity.kt b/app/src/main/java/com/sw/platecabinet/activity/MainActivity.kt index cb3ec95..5d5373a 100644 --- a/app/src/main/java/com/sw/platecabinet/activity/MainActivity.kt +++ b/app/src/main/java/com/sw/platecabinet/activity/MainActivity.kt @@ -15,6 +15,7 @@ import com.sw.platecabinet.utils.FragmentHelper class MainActivity : BaseActivity() { private lateinit var fragmentHelper: FragmentHelper + private var pageType = PageType.SETTING_LIST override fun inflateViewBinding(): ActivityMainBinding { return ActivityMainBinding.inflate(layoutInflater) @@ -43,7 +44,7 @@ class MainActivity : BaseActivity() { override fun initialize() { val param = intent.getStringExtra(PARAM_PAGE_TYPE) val equipmentUserInfo = intent.getParcelableExtra(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,6 +55,16 @@ class MainActivity : BaseActivity() { } fragmentHelper.addFragment(page) } + + override fun onLeftDoubleClick() { + if (pageType == PageType.SETTING_LIST) { + finish() + } + } + + override fun onRightDoubleClick() { + + } } /** diff --git a/app/src/main/java/com/sw/platecabinet/adapter/GenericPageAdapter.kt b/app/src/main/java/com/sw/platecabinet/adapter/GenericPageAdapter.kt index 5f0bc85..fe53a35 100644 --- a/app/src/main/java/com/sw/platecabinet/adapter/GenericPageAdapter.kt +++ b/app/src/main/java/com/sw/platecabinet/adapter/GenericPageAdapter.kt @@ -40,17 +40,16 @@ class GenericPageAdapter( fun bind(items: List) { childRecyclerView.layoutManager = GridLayoutManager(itemView.context, 2) - // 添加间距装饰(12dp) childRecyclerView.addItemDecoration( GridSpacingItemDecoration( spanCount = 2, // 2列 - spacing = itemView.context.dpToPx(12), // 12dp + spacing = itemView.context.dpToPx(6), // 12dp includeEdge = true // 包含边缘间距 ) ) childRecyclerView.adapter = - GenericChildAdapter(items, itemBindingInflater, itemBindCallback) + GenericItemAdapter(items, itemBindingInflater, itemBindCallback) } } } @@ -58,11 +57,11 @@ class GenericPageAdapter( /** * 通用子项适配器(支持ViewBinding) */ -class GenericChildAdapter( - private val items: List, +class GenericItemAdapter( + private var items: List, private val bindingInflater: (LayoutInflater, ViewGroup, Boolean) -> VB, private val bindCallback: VB.(item: T, position: Int) -> Unit, -) : RecyclerView.Adapter.ViewHolder>() { +) : RecyclerView.Adapter.ViewHolder>() { inner class ViewHolder(private val binding: VB) : RecyclerView.ViewHolder(binding.root) { fun bind(item: T, position: Int) { @@ -70,6 +69,11 @@ class GenericChildAdapter( } } + fun updateData(newItem: List) { + 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 +81,6 @@ class GenericChildAdapter( override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.bind(items[position], position) - } override fun getItemCount(): Int = items.size diff --git a/app/src/main/java/com/sw/platecabinet/ext/StringExt.kt b/app/src/main/java/com/sw/platecabinet/ext/StringExt.kt new file mode 100644 index 0000000..bc558f4 --- /dev/null +++ b/app/src/main/java/com/sw/platecabinet/ext/StringExt.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/platecabinet/fragment/BaseFragment.kt b/app/src/main/java/com/sw/platecabinet/fragment/BaseFragment.kt index 64d45f3..8017f08 100644 --- a/app/src/main/java/com/sw/platecabinet/fragment/BaseFragment.kt +++ b/app/src/main/java/com/sw/platecabinet/fragment/BaseFragment.kt @@ -1,19 +1,23 @@ 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.viewbinding.ViewBinding +import com.sw.platecabinet.R +import com.sw.platecabinet.view.CustomDialog abstract class BaseFragment( 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 override fun onCreateView( inflater: LayoutInflater, @@ -22,16 +26,37 @@ abstract class BaseFragment( ): View? { _binding = bindingInflater(inflater, container, false) initialize() + registerDataChange() return binding.root } + /** + * 注册数据变化监听 + */ + open fun registerDataChange() {} + abstract fun initialize() - class ViewModelFactory(private val creator: () -> T) : - ViewModelProvider.Factory { - override fun create(modelClass: Class): 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(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() { diff --git a/app/src/main/java/com/sw/platecabinet/fragment/BindPlateFragment.kt b/app/src/main/java/com/sw/platecabinet/fragment/BindPlateFragment.kt index 063f024..0917ae6 100644 --- a/app/src/main/java/com/sw/platecabinet/fragment/BindPlateFragment.kt +++ b/app/src/main/java/com/sw/platecabinet/fragment/BindPlateFragment.kt @@ -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.TextWatcher +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.sw.plate.utils.ToastUtils import com.sw.platecabinet.R -import com.sw.platecabinet.adapter.GenericChildAdapter +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 com.sw.platecabinet.viewmodel.SettingViewModel +import kotlinx.coroutines.launch import timber.log.Timber - /** * 餐盘柜绑定 */ class BindPlateFragment : BaseFragment( FragmentBindPlateBinding::inflate ) { - private lateinit var adapter: GenericChildAdapter + private lateinit var adapter: GenericItemAdapter + private val viewModel by viewModels() 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,147 @@ class BindPlateFragment : BaseFragment( 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}" + } } + 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 + ) + } + } + }) + 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) + } + }) + } + + private fun initListener() { + 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() { + lifecycleScope.launch { + viewModel.searchMemberList.collect { + adapter.updateData(it) + } + } + lifecycleScope.launch { + viewModel.bindStateChange.collect { + if (it.first == null) return@collect // 解绑状态过滤 + val errorInfo = it.second + if (errorInfo.isSuccess()) { + ToastUtils.showToast("绑定成功") + activity?.finish() + } else { + ToastUtils.showToast(errorInfo.msg) + } + } + } + lifecycleScope.launch { + viewModel.showLoading.collect { + if (it) { + showWaitingDialog("") + } else { + hideWaitingDialog() + } + } + } } - private fun createAdapter(): GenericChildAdapter { - return GenericChildAdapter( + private fun createAdapter(): GenericItemAdapter { + 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 +199,8 @@ class BindPlateFragment : BaseFragment( this.llRoot.setOnClickListener { Timber.d("itemClick ${item.name}, position = $position") + checkedItem = item + adapter.notifyDataSetChanged() } } ) diff --git a/app/src/main/java/com/sw/platecabinet/fragment/PlateCabinetFullFragment.kt b/app/src/main/java/com/sw/platecabinet/fragment/PlateCabinetFullFragment.kt index 5af0cb6..f10bbfe 100644 --- a/app/src/main/java/com/sw/platecabinet/fragment/PlateCabinetFullFragment.kt +++ b/app/src/main/java/com/sw/platecabinet/fragment/PlateCabinetFullFragment.kt @@ -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() } diff --git a/app/src/main/java/com/sw/platecabinet/fragment/SettingListFragment.kt b/app/src/main/java/com/sw/platecabinet/fragment/SettingListFragment.kt index 8a3181c..d1ff7d7 100644 --- a/app/src/main/java/com/sw/platecabinet/fragment/SettingListFragment.kt +++ b/app/src/main/java/com/sw/platecabinet/fragment/SettingListFragment.kt @@ -5,14 +5,18 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager 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.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.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.launch @@ -25,31 +29,41 @@ import kotlin.math.ceil class SettingListFragment : BaseFragment(FragmentSettingListBinding::inflate) { private val viewModel: SettingViewModel by viewModels() - - private lateinit var pageAdapter: GenericPageAdapter + private lateinit var itemAdapter: GenericItemAdapter private val itemsPerPage = 22 // 每行2个,每列11个,共22个 - private fun registerDateChange() { + override fun registerDataChange() { lifecycleScope.launch { - // 使用 repeatOnLifecycle 确保只在特定生命周期状态收集 -// repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.equipmentList.collect { it -> Timber.d("registerDateChange updateAdapter it = $it") updateAdapter(it) } -// } + } + lifecycleScope.launch { + viewModel.showLoading.collect { + if (it) { + showWaitingDialog("") + } else { + hideWaitingDialog() + } + } } } override fun initialize() { binding.mainRecyclerView.apply { layoutManager = LinearLayoutManager(context) + pageAdapter = createAdapter() adapter = pageAdapter } - registerDateChange() - viewModel.getEquipmentList(EquipmentParam(equipmentCode = "202501171634")) + + } + + override fun onResume() { + super.onResume() + viewModel.getEquipmentList() } private fun createAdapter(): GenericPageAdapter { @@ -76,7 +90,7 @@ class SettingListFragment : this.llBindInfo.visibility = View.VISIBLE this.tvUnbind.visibility = View.GONE - this.tvName.text = item.name + this.tvName.text = item.name.maskName() this.tvLastTime.text = DateTimeUtils.getTimeAgo(date) } else { this.llRoot.setBackgroundResource(R.drawable.grid_item_unbind) @@ -103,15 +117,29 @@ class SettingListFragment : } } this.llOpen.setOnClickListener { - Timber.d("itemClick llOpen ${item.equipmentCode}") - MainActivity.start(requireContext(), pageType = PageType.PLATE_OPEN) + Timber.d("itemClick llOpen equipmentBoxCode = ${item.equipmentBoxCode}") + val equipmentBoxCode: Int = item.equipmentBoxCode?.toInt() ?: -1 + if (equipmentBoxCode < 0) { + ToastUtils.showToast("餐盘柜编号错误") + return@setOnClickListener + } + SerialApi.openPlate(equipmentBoxCode, object : SerialPortManager.SendCallback { + override fun onSuccess() { + MainActivity.start(requireContext(), pageType = PageType.PLATE_OPEN) + } + + override fun onFail(e: Exception?) { + ToastUtils.showToast("柜门打开失败") + } + }) } } ) } fun updateAdapter(items: List) { - val pages = if (items.size > itemsPerPage) { +// if (GlobalData.arrayCross > 2) { + var pages = if (items.size > itemsPerPage) { items.chunked(itemsPerPage) { convertToColumnFirst(it, 11) } @@ -123,6 +151,9 @@ class SettingListFragment : ) } pageAdapter.updatePages(pages) +// }else{ +// +// } } /** diff --git a/app/src/main/java/com/sw/platecabinet/fragment/UnBindPlateFragment.kt b/app/src/main/java/com/sw/platecabinet/fragment/UnBindPlateFragment.kt index 8887439..84fe1d4 100644 --- a/app/src/main/java/com/sw/platecabinet/fragment/UnBindPlateFragment.kt +++ b/app/src/main/java/com/sw/platecabinet/fragment/UnBindPlateFragment.kt @@ -1,9 +1,16 @@ package com.sw.platecabinet.fragment import android.os.Bundle +import androidx.fragment.app.viewModels +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 com.sw.platecabinet.viewmodel.SettingViewModel +import kotlinx.coroutines.launch /** * 餐盘柜绑定 @@ -12,8 +19,8 @@ class UnBindPlateFragment private constructor() : BaseFragment() companion object { @JvmStatic @@ -31,15 +38,46 @@ class UnBindPlateFragment private constructor() : BaseFragment /** * 获取人脸数据 */ - @GET("scales/getUserFaceCache") + @GET("/shuwei-zhct/scales/getUserFaceCache") suspend fun getUserFaceCache(): ApiResponse> /** * 餐盘用户信息获取 */ - @POST("swEquipmentRelUser/equipmentBoxLogin") + @POST("/shuwei-zhct/swEquipmentRelUser/equipmentBoxLogin") suspend fun equipmentBoxLogin(@Body param: LoginParam): ApiResponse /** * 餐盘用户信息列表查询 */ - @POST("swEquipmentRelUser/list") + @POST("/shuwei-zhct/swEquipmentRelUser/list") suspend fun getEquipmentList(@Body param: EquipmentParam): ApiResponse> /** * 餐盘用户信息绑定解绑 */ - @POST("swEquipmentRelUser/addOrEdit") + @POST("/shuwei-zhct/swEquipmentRelUser/addOrEdit") suspend fun bindEquipment(@Body param: BindParam): ApiResponse /** * 用户信息模糊搜索 */ - @POST("swclientUserInfoShop/selectList") + @POST("/shuwei-user/swclientUserInfoShop/selectList") suspend fun searchUser(@Body param: SearchParam): ApiResponse } \ No newline at end of file diff --git a/app/src/main/java/com/sw/platecabinet/repository/RemoteRepository.kt b/app/src/main/java/com/sw/platecabinet/repository/RemoteRepository.kt index 0ac59cd..af1ca38 100644 --- a/app/src/main/java/com/sw/platecabinet/repository/RemoteRepository.kt +++ b/app/src/main/java/com/sw/platecabinet/repository/RemoteRepository.kt @@ -9,7 +9,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 /** * 远程数据处理 @@ -55,7 +54,6 @@ class RemoteRepository constructor( /** * 用户信息模糊搜索 */ - @POST("/swclientUserInfoShop/selectList") suspend fun searchUser(param: SearchParam): ApiResponse { return safeApiCall { apiService.searchUser(param) } } diff --git a/app/src/main/java/com/sw/platecabinet/utils/PermissionHelper.kt b/app/src/main/java/com/sw/platecabinet/utils/PermissionHelper.kt index 40b0ef4..a8f635b 100644 --- a/app/src/main/java/com/sw/platecabinet/utils/PermissionHelper.kt +++ b/app/src/main/java/com/sw/platecabinet/utils/PermissionHelper.kt @@ -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, - 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, requestCode: Int): Builder { return Builder(context, permissions, requestCode) @@ -43,15 +41,17 @@ class PermissionHelper private constructor( /** * 处理权限请求结果(供BaseActivity/BaseFragment调用) + * @return 是否所有权限都已授予 */ fun handlePermissionResult( context: Context, requestCode: Int, permissions: Array, grantResults: IntArray, - onPermissionPermanentlyDenied: (List) -> 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) } } @@ -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) } } diff --git a/app/src/main/java/com/sw/platecabinet/viewmodel/SettingViewModel.kt b/app/src/main/java/com/sw/platecabinet/viewmodel/SettingViewModel.kt index f3001ab..4157d4b 100644 --- a/app/src/main/java/com/sw/platecabinet/viewmodel/SettingViewModel.kt +++ b/app/src/main/java/com/sw/platecabinet/viewmodel/SettingViewModel.kt @@ -1,7 +1,12 @@ package com.sw.platecabinet.viewmodel +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 +15,99 @@ class SettingViewModel : BaseViewModel() { private val _equipmentList = MutableStateFlow>(emptyList()) val equipmentList: StateFlow> = _equipmentList - fun getEquipmentList(param: EquipmentParam) { + private val _searchMemberList = MutableStateFlow>(emptyList()) + val searchMemberList: StateFlow> = _searchMemberList + + var currentPage = 1 + var isLoading = false + var canLoadMore = true + + /** + * 绑定/解绑状态 <绑定/解绑, 错误信息,> + */ + private val _bindStateChange = MutableStateFlow>(null to ErrorInfo()) + val bindStateChange: StateFlow> = _bindStateChange + + + /** + * 获取设备绑定用户信息列表 + */ + fun getEquipmentList(equipmentCode: String = globalEquipmentCode) { launchWithLoading { + val param = EquipmentParam(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( + 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( + equipmentBoxCode = equipmentBoxCode, + equipmentCode = equipmentCode, + memberId = memberId, + plateNumber = plateNumber + ) + val response = repository.bindEquipment(bindParam) + if (parseResponse(response)) { + _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( + 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()) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/sw/platecabinet/viewmodel/UserViewModel.kt b/app/src/main/java/com/sw/platecabinet/viewmodel/UserViewModel.kt index f26f0bb..bdf4a19 100644 --- a/app/src/main/java/com/sw/platecabinet/viewmodel/UserViewModel.kt +++ b/app/src/main/java/com/sw/platecabinet/viewmodel/UserViewModel.kt @@ -1,10 +1,31 @@ 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.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 kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import timber.log.Timber class UserViewModel : BaseViewModel() { + // 通过人脸获取到的用户信息 + private val _currentUserInfo = MutableStateFlow(null) + val currentUserInfo: StateFlow = _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 +40,91 @@ 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 = response.data ?: emptyList() + val faceEntity = list.map { + FaceEntity(it.userId, null, Base64.decode(it.faceFeatureString)) + } + 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() + private fun 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") + when (activeCode) { + ErrorInfo.MOK -> { + ToastUtils.showToast("激活引擎成功") + } + + ErrorInfo.MERR_ASF_ALREADY_ACTIVATED -> { + ToastUtils.showToast("引擎已激活,无需再次激活") + } + + else -> { + ToastUtils.showToast("激活引擎失败($activeCode)") + } + } + } + + override fun onFail(e: Exception?) { + ToastUtils.showToast("激活引擎异常,${e?.message}") + } + }) } + /** + * 校验码登录 + */ + fun loginWithPwd(equipmentCode: String = globalEquipmentCode, phone: String, password: String) { + launchWithLoading { + val loginParam = LoginParam( + 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( + equipmentCode = equipmentCode, + memberId = memberId + ) + val response = repository.equipmentBoxLogin(loginParam) + if (parseResponse(response)) { + _currentUserInfo.value = response.data + } + } + } } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_login_by_pwd.xml b/app/src/main/res/layout/activity_login_by_pwd.xml index 1f241f4..069549d 100644 --- a/app/src/main/res/layout/activity_login_by_pwd.xml +++ b/app/src/main/res/layout/activity_login_by_pwd.xml @@ -40,6 +40,7 @@ android:src="@mipmap/ic_phone" /> @@ -34,9 +35,19 @@ android:padding="10dp" android:visibility="visible"> - + + + + + + + + diff --git a/app/src/main/res/layout/fragment_bind_plate.xml b/app/src/main/res/layout/fragment_bind_plate.xml index da4d746..6b43ce2 100644 --- a/app/src/main/res/layout/fragment_bind_plate.xml +++ b/app/src/main/res/layout/fragment_bind_plate.xml @@ -1,5 +1,6 @@ + android:textStyle="bold" + tools:text="1-12" /> @@ -71,6 +72,7 @@ android:layout_height="match_parent" android:background="@color/transparent" android:gravity="center" + android:maxLines="1" android:textColor="#fff0c8b4" android:textSize="26sp" android:textStyle="bold" @@ -80,7 +82,7 @@ @@ -117,6 +119,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,7 +131,9 @@ 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" /> - - + android:textStyle="bold" + android:visibility="gone" /> + android:textSize="14sp" /> diff --git a/app/src/main/res/layout/item_title_time.xml b/app/src/main/res/layout/item_title_time.xml index caea9e9..5a8a1dc 100644 --- a/app/src/main/res/layout/item_title_time.xml +++ b/app/src/main/res/layout/item_title_time.xml @@ -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"> - SmartPlateCabinet + 餐盘柜 Hello blank fragment + + %s 初始化失败,错误码:%d\n错误码常量名:%s \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 16128de..485a7e8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" diff --git a/lib_face/build.gradle.kts b/lib_face/build.gradle.kts index 7f672df..8ff6f19 100644 --- a/lib_face/build.gradle.kts +++ b/lib_face/build.gradle.kts @@ -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", diff --git a/lib_face/libs/armeabi-v7a/libmupdf.so b/lib_face/libs/armeabi-v7a/libmupdf.so deleted file mode 100644 index 3004980..0000000 Binary files a/lib_face/libs/armeabi-v7a/libmupdf.so and /dev/null differ diff --git a/lib_face/libs/armeabi-v7a/libneonui_shared.so b/lib_face/libs/armeabi-v7a/libneonui_shared.so deleted file mode 100644 index b533ad8..0000000 Binary files a/lib_face/libs/armeabi-v7a/libneonui_shared.so and /dev/null differ diff --git a/lib_face/libs/armeabi-v7a/libneonuijni_public.so b/lib_face/libs/armeabi-v7a/libneonuijni_public.so deleted file mode 100644 index c1e783b..0000000 Binary files a/lib_face/libs/armeabi-v7a/libneonuijni_public.so and /dev/null differ diff --git a/lib_face/src/main/java/com/sw/plate/utils/AppUtil.java b/lib_face/src/main/java/com/sw/plate/utils/AppUtil.java new file mode 100644 index 0000000..911374a --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/AppUtil.java @@ -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 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; + } + + /** + * 打电话 + *

+ * 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地址 + * 必须的权限 + * + * @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 + * 必须的权限 + * + * @return + */ + public static String getMacFromHardware() { + try { + ArrayList 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中加入权限即可 + } + +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/Base64.java b/lib_face/src/main/java/com/sw/plate/utils/Base64.java new file mode 100644 index 0000000..eaf28da --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/Base64.java @@ -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; + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/comn/SerialApi.java b/lib_face/src/main/java/com/sw/plate/utils/comn/SerialApi.java new file mode 100644 index 0000000..61b91ea --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/comn/SerialApi.java @@ -0,0 +1,38 @@ +package com.sw.plate.utils.comn; + +import static com.sw.plate.utils.CabinetLockCommand.generateOpenCommand; + +import android.serialport.SerialPort; + +import com.sw.plate.utils.ToastUtils; + +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) { + return; + } + serialPortManager.sendCommand(generateOpenCommand(boxNumber), callback); + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/comn/SerialPortManager.java b/lib_face/src/main/java/com/sw/plate/utils/comn/SerialPortManager.java index 9b98d01..6835200 100644 --- a/lib_face/src/main/java/com/sw/plate/utils/comn/SerialPortManager.java +++ b/lib_face/src/main/java/com/sw/plate/utils/comn/SerialPortManager.java @@ -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); + } } diff --git a/lib_face/src/main/res/layout/toast_bg.xml b/lib_face/src/main/res/layout/toast_bg.xml index e3ba3f5..67e932f 100644 --- a/lib_face/src/main/res/layout/toast_bg.xml +++ b/lib_face/src/main/res/layout/toast_bg.xml @@ -1,5 +1,5 @@ - - \ No newline at end of file + \ No newline at end of file