package com.sw.platecabinet.activity import android.app.Dialog import android.content.Context import android.os.Build import android.os.Bundle import android.os.Handler import android.os.Looper 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.core.view.WindowCompat 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.arcface.facedb.FaceDatabase import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel import com.sw.plate.utils.comn.SerialApi import com.sw.plate.utils.comn.SerialPortManager import com.sw.platecabinet.member.R import com.sw.platecabinet.member.databinding.ItemTitleTimeBinding import com.sw.platecabinet.dialog.BalanceNotEnoughDialog import com.sw.platecabinet.ext.setClickListeners import com.sw.platecabinet.model.response.EquipmentUserInfo import com.sw.platecabinet.utils.IntervalExecutor import com.sw.platecabinet.utils.PermissionHelper import com.sw.platecabinet.utils.SpTool import com.sw.platecabinet.view.CustomLoadingDialog import com.sw.platecabinet.viewmodel.NetViewModelV2 import com.sw.platecabinet.viewmodel.SettingViewModel import com.sw.platecabinet.viewmodel.UserViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber /** * activity 基类 */ abstract class BaseActivity : AppCompatActivity() { protected lateinit var binding: VB private var headerBinding: ItemTitleTimeBinding? = null protected lateinit var context: Context private var timeJob: Job? = null private var mDialogWaiting: CustomLoadingDialog? = null private val permissionHelpers = mutableMapOf() protected var keyEventHelper: ScanGunKeyEventHelper? = null // 管理员对应的viewmodel private val settingViewModel by viewModels() // 用户对应的viewModel protected val viewModel by viewModels() // V2 网络请求 viewModel(人脸缓存/增量同步/设备配置) val netViewModelV2 by viewModels() private var startTime: Long = 0 override fun onCreate(savedInstanceState: Bundle?) { startTime = System.currentTimeMillis() 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() val durationTime = System.currentTimeMillis() - startTime Timber.d("启动时间:$durationTime") } /** * 监听扫描枪扫描事件 */ protected fun registerKeyEvent() { keyEventHelper = ScanGunKeyEventHelper(context, object : ScanGunKeyEventHelper.OnScanSuccessListener { override fun onScanSuccess(barcode: String?) { Timber.d("onScanSuccess barcode = $barcode") if (barcode == null) return handleScanKeyInfo(barcode) } }) } /** * 处理扫描枪数据 */ protected open fun handleScanKeyInfo(scanInfo: String) { settingViewModel.findByPlateNumber(plateNumber = scanInfo) } override fun dispatchKeyEvent(event: KeyEvent): Boolean { if (keyEventHelper != null) { if (keyEventHelper!!.isScanGunEvent(event)) { keyEventHelper!!.analysisKeyEvent(event) return true } } return super.dispatchKeyEvent(event) } fun updateTime() { headerBinding?.let { it.tvLeftDate.setClickListeners( onDoubleClick = { onLeftDoubleClick() } ) it.tvRightTime.setClickListeners( onDoubleClick = { onRightDoubleClick() } ) val scope = CoroutineScope(Dispatchers.Main) timeJob = scope.launch { DateTimeUtils.realTimeChineseDateFlow() .collect { (date, 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, permissionHelper = helper ) if (allGranted) { permissionHelpers.remove(requestCode) } } /** * 注册PermissionHelper以便处理结果 */ fun registerPermissionHelper(helper: PermissionHelper) { permissionHelpers[helper.requestCode] = helper } override fun onResume() { super.onResume() // 关键点2:防止某些场景下系统栏恢复 enforceImmersiveMode() } override fun onWindowFocusChanged(hasFocus: Boolean) { super.onWindowFocusChanged(hasFocus) if (hasFocus) { disableSystemUICompletely() } } private fun disableSystemUICompletely() { Timber.d("disableSystemUICompletely") // 禁用系统手势(Android 10+) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.insetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE } // 全屏+隐藏导航栏(所有版本通用) window.decorView.systemUiVisibility = ( View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE ) // 禁止窗口扩展至系统栏区域(彻底锁定) window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) } private fun enforceImmersiveMode() { Timber.d("enforceImmersiveMode") // 持续强制隐藏系统栏(防止手势触发) window.decorView.postDelayed({ window.decorView.systemUiVisibility = ( View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY ) }, 100) // 延迟100ms确保覆盖手势触发 } /** * 显示等待提示框 */ fun showWaitingDialog(tip: String?): Dialog? { hideWaitingDialog() val view = View.inflate(this, R.layout.dialog_waiting, null) if (!TextUtils.isEmpty(tip)) (view.findViewById(R.id.tvTip) as TextView).setText(tip) mDialogWaiting = CustomLoadingDialog(this, view, R.style.MyDialog) mDialogWaiting!!.show() mDialogWaiting!!.setCancelable(true) return mDialogWaiting } /** * 隐藏等待提示框 */ fun hideWaitingDialog() { mDialogWaiting?.dismiss() mDialogWaiting = null } protected abstract fun inflateViewBinding(): VB protected abstract fun inflateTitleBinding(): ItemTitleTimeBinding? protected abstract fun initialize() /** * 注册数据监听 */ protected open fun registerDataChange() { Timber.d("registerDataChange") lifecycleScope.launch { viewModel.showLoading.collect { Timber.d("registerDataChange 用户 showLoading = $it") if (it) { showWaitingDialog("") } else { hideWaitingDialog() } } } lifecycleScope.launch { viewModel.currentUserInfo.collect { if (it == null) return@collect it.showBalanceNotEnoughDialog = true handleLoginSuccess(it, false) } } lifecycleScope.launch { settingViewModel.showLoading.collect { Timber.d("registerDataChange 管理员 showLoading = $it") if (it) { showWaitingDialog("") } else { hideWaitingDialog() } } } lifecycleScope.launch { settingViewModel.searchUserInfo.collect { Timber.d("registerDataChange 管理员 currentUserInfo = $it") if (it == null) return@collect handleLoginSuccess(it, true) } } // lifecycleScope.launch { // settingViewModel.equipmentList.collect { // loadEquipmentList(it) // } // } } private fun loadEquipmentList(items: List) { if (items.isEmpty()) return val unbindList = items.filter { !it.isBound() } if (unbindList.isEmpty()) { MainActivity.start(context = context, pageType = PageType.PLATE_CABINET_FULL) } else { val firstInfo = unbindList[0] val currentUserInfo = settingViewModel.searchUserInfo.value firstInfo.plateNumber = currentUserInfo?.plateNumber ?: "" MainActivity.start( context = context, pageType = PageType.BIND_PLATE, equipmentUserInfo = firstInfo ) } } /** * 处理登录成功操作 * @param isAdmin true 管理员 执行绑盘此操作 false 用户,提示错误 */ open fun handleLoginSuccess(equipmentUserInfo: EquipmentUserInfo, isAdmin: Boolean) { Timber.d("handleLoginSuccess isAdmin = $isAdmin") equipmentUserInfo.isIntercept = false val cardBalance = equipmentUserInfo.cardBalance ?: 0.toDouble() val balanceIsNotEnough = cardBalance <= 0.toDouble() if (balanceIsNotEnough && equipmentUserInfo.showBalanceNotEnoughDialog) { //提示余额不足弹窗 equipmentUserInfo.isIntercept = true BalanceNotEnoughDialog(this).show() return } viewModel.resetUserInfo() // if (equipmentUserInfo.isOtherEquipment()) { // MainActivity.start( // context, // pageType = PageType.PLATE_TIP, // equipmentUserInfo = equipmentUserInfo, // isAdmin // ) // return // } if (equipmentUserInfo.equipmentBoxCode?.isNotEmpty() == true) { SerialApi.openPlate( equipmentUserInfo.equipmentBoxCode!!.toInt(), object : SerialPortManager.SendCallback { override fun onSuccess() { MainActivity.start( context, pageType = PageType.PLATE_TIP, isAdmin = isAdmin ) } override fun onFail(e: Exception?) { ToastUtils.showToast("柜门打开失败") } }) } else { if (isAdmin) { settingViewModel.getEquipmentList { loadEquipmentList(it) } } else { Timber.d("无餐盘信息") MainActivity.start( context, pageType = PageType.PLATE_TIP, equipmentUserInfo = equipmentUserInfo, isAdmin = isAdmin ) } } } override fun onDestroy() { timeJob?.cancel() keyEventHelper?.onDestroy() super.onDestroy() } private val intervalExecutor by lazy { IntervalExecutor() } private var faceTaskJob: Job? = null // private val initialDelay = 5 * 60 * 1000L // private val dealyMillis = 10 * 60 * 1000L private val initialDelay = 5 * 60 * 1000L private val dealyMillis = 5 * 60 * 1000L val recognizeViewModel by viewModels() /** * 引擎内存刷新防抖:合并短时间内连续的人脸变更(MQTT 实时 + HTTP 增量补拉 + 定时轮询), * 避免多次全量重载 ArcSoft 引擎内存(removeFaceFeature(-1) + registerFaceFeature 非原子)。 */ private val faceRefreshHandler = Handler(Looper.getMainLooper()) private val faceRefreshRunnable = Runnable { recognizeViewModel.refreshFaceList() } /** 防抖调度引擎内存刷新(2s 内连续变更合并为一次) */ protected fun scheduleFaceRefresh() { faceRefreshHandler.removeCallbacks(faceRefreshRunnable) faceRefreshHandler.postDelayed(faceRefreshRunnable, 2000) } fun startFaceTask() { faceTaskJob = intervalExecutor.startIntervalTaskWithInitialDelay(initialDelay, dealyMillis) { getFaceIncrementList() } } /** * 人脸增量同步(V2,同步逻辑在 NetViewModelV2 内部处理) */ private fun getFaceIncrementList(pageNo: Int = 1) { val timestamp = SpTool.getLastFaceTimestamp() if (timestamp == 0L) { return } netViewModelV2.getFaceIncrementList( pageNo = pageNo, timestamp = timestamp ) { scheduleFaceRefresh() } } /** * 清空本地人脸库 */ fun clearAllFace(block: () -> Unit) { lifecycleScope.launch(Dispatchers.IO) { val faceDao = FaceDatabase.getInstance(this@BaseActivity).faceDao() faceDao.deleteAll() faceDao.resetId() recognizeViewModel.refreshFaceList() withContext(Dispatchers.Main) { block() } } } }