Files
StallDualScreen/app/src/main/java/com/sw/dualscreen/activity/MainActivity.kt
T

715 lines
26 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.sw.dualscreen.activity
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.graphics.Outline
import android.hardware.display.DisplayManager
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.text.TextUtils
import android.view.View
import android.view.ViewOutlineProvider
import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.GridLayoutManager
import com.google.common.util.concurrent.ListenableFuture
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.GlobalKey
import com.sw.dualscreen.adapter.SearchFoodAdapter
import com.sw.dualscreen.databinding.ActivityMainBinding
import com.sw.dualscreen.ext.dp
import com.sw.dualscreen.ext.gone
import com.sw.dualscreen.ext.load
import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.FoodOrder
import com.sw.dualscreen.objbox.FoodModule
import com.sw.dualscreen.objbox.FoodModule.IdNameScore
import com.sw.dualscreen.presentation.MainScreenPresentation
import com.sw.dualscreen.sdk.SensorScaleUtils
import com.sw.dualscreen.utils.BitmapSaver
import com.sw.dualscreen.utils.Debouncer
import com.sw.dualscreen.utils.GsonUtils
import com.sw.dualscreen.utils.ImageUtil
import com.sw.dualscreen.utils.SPUtil
import com.sw.dualscreen.view.CustomBottomSheetDialog
import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel
import com.sw.plate.utils.LightManager
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import timber.log.Timber
import java.io.File
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Locale
import java.util.concurrent.Executors
import kotlin.math.roundToInt
/**
* 服务员显示界面
*/
@SuppressLint("NotifyDataSetChanged")
class MainActivity : BaseActivity<ActivityMainBinding>() {
companion object {
const val TIME_OUT = 5 * 60 * 1000L
}
private val viewModel by viewModels<UserViewModel>()
private var imageCapture: ImageCapture? = null
var isAnalyzing = true // 控制是否进行图像分析
// private var presentation: SecondaryScreenPresentation? = null
private var presentation: MainScreenPresentation? = null
private val executor = Executors.newSingleThreadExecutor()
private val handler by lazy { Handler(Looper.getMainLooper()) }
private val searchFoodList = mutableListOf<FoodInfo>()
private val adapter by lazy {
SearchFoodAdapter(searchFoodList).apply {
setOnItemClickListener { adapter, view, position ->
searchFoodList.forEachIndexed { index, info ->
searchFoodList[index].isChecked = index == position
}
val item = searchFoodList[position]
Timber.d("itemClick ${item.foodName}, position = $position")
item.photoUri = null
checkedItem = item
notifyDataSetChanged()
updateCurrentFood(item)
}
}
}
var checkedItem: FoodInfo? = null
private var imageAnalysis: ImageAnalysis? = null
private var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>? = null
private var canIdentify: Boolean = false
private var bottomSheetDialog: CustomBottomSheetDialog? = null
private var lastWeight: Double = 0.0
private var lastPhotoUri: Uri? = null // 最后拍照的图片
private var debouncer = Debouncer(2000)
private var isRecognitionFood = true
override fun getViewModel(): BaseViewModel {
return viewModel
}
override fun inflateViewBinding(): ActivityMainBinding {
return ActivityMainBinding.inflate(layoutInflater)
}
override fun initialize() {
super.initialize()
addBackEventListener()
FoodModule.init(this)
initView()
setupSecondaryDisplay()
setupCamera()
initData()
}
private fun initData() {
// viewModel.getEquipmentToken()
viewModel.getUserFaceCache(pageNo = 1)
// presentation?.step1()
presentation?.step1FoodRecognizing()
}
private fun initView() {
// // 在Activity中获取DPI
// val metrics = getResources()?.displayMetrics
// val screenDPI = metrics?.densityDpi
// val density = metrics?.density
//
// Log.d("ScreenInfo", "DPI: $screenDPI, density: $density");
viewModel.activeEngine()
updateDateTime()
binding.ivSetting.setOnClickListener {
startActivity(Intent(this, SettingActivity::class.java))
finish()
}
//binding.foodDisplayLayout.setOnClickListener {
// val intent = Intent(this, FoodCollectionActivity::class.java)
// startActivity(intent)
// finish()
//}
binding.previewView.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setRoundRect(0, 0, view.width, view.height, 12f.dp)
}
}
binding.previewView.clipToOutline = true
binding.tvToSearch.setOnClickListener {
bottomSheetDialog = CustomBottomSheetDialog.newInstance(viewModel) {
checkedItem = it
it.photoUri = null
updateCurrentFood(it)
searchFoodList.forEachIndexed { index, info ->
searchFoodList[index].isChecked = false
}
adapter.notifyDataSetChanged()
}
bottomSheetDialog!!.show(supportFragmentManager, "CustomBottomSheetDialog")
}
//val spinnerAdapter = ArrayAdapter<String>(
// this,
// R.layout.item_for_custom_spinner,
// arrayOf("即放即取", "余量取餐")
//)
//binding.spinner.adapter = spinnerAdapter
// val mode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0)
// //binding.spinner.setSelection(mode ?: 0)
// presentation?.updateMealPickupMode(mode ?: 0)
//binding.spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
// override fun onItemSelected(
// parent: AdapterView<*>?,
// view: View?,
// position: Int,
// id: Long
// ) {
// SPUtil.getInstance().put(GlobalKey.KEY_PICKUP_MODE, position)
// updateCurrentFood(null)
// isRecognitionFood = true
// presentation?.updateMealPickupMode(position)
//
// SensorScaleUtils.zero()
// }
//
// override fun onNothingSelected(parent: AdapterView<*>?) {}
//}
//adapter = createAdapter()
binding.recyclerview.let {
it.layoutManager = GridLayoutManager(this, 2)
it.adapter = adapter
}
// 添加间距装饰(12dp)
//binding.recyclerview.addItemDecoration(
// GridSpacingItemDecoration(
// spanCount = 2,
// spacing = dpToPx(30),
// includeEdge = false // 包含边缘间距
// )
//)
//binding.recyclerview.adapter = adapter
binding.tvFoodName.setOnClickListener {
SensorScaleUtils.zero()
}
// binding.tvRescan.setOnClickListener {
// debouncer.debounce { recognizeFood() }
// }
binding.btnPay.let {
it.run {
val mode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
if (mode == 0) visible() else gone()
}
it.setOnClickListener { v ->
if (checkedItem == null) {
ToastUtils.showToast("暂无识别数据,请搜索选择")
return@setOnClickListener
}
createOrder(checkedItem!!)
}
}
}
fun updateCurrentFood(foodInfo: FoodInfo?) {
if (foodInfo == null) {
binding.tvFoodName.text = "-"
binding.previewView.visibility = View.VISIBLE
binding.ivImg.visibility = View.GONE
resumeAnalysis()
viewModel.cleanIdentifiedFoodInfoList()
return
}
pauseAnalysis()
presentation?.updateFood(foodInfo)
binding.tvFoodName.text = foodInfo.foodName
binding.previewView.visibility = View.GONE
binding.ivImg.visibility = View.VISIBLE
if (foodInfo.foodImg.isNullOrBlank()) {
takePhoto { imgUri ->
binding.ivImg.load(imgUri)
presentation?.updateFood(foodInfo.copy(foodImg = imgUri.toString()))
}
} else {
binding.ivImg.load(foodInfo.photoUri ?: foodInfo.foodImg)
}
//pauseAnalysis()
}
override fun registerDataChange() {
super.registerDataChange()
//// lifecycleScope.launch {
//// viewModel.identifiedFoodInfoList2.collect { list ->
//// updateFoodInfo(list)
//// }
//// }
// lifecycleScope.launch {
// viewModel.identifiedFoodInfoList.collect { list ->
// //adapter.updateData(list)
// searchFoodList.clear()
// searchFoodList.addAll(list)
// adapter.notifyDataSetChanged()
//
// if (list.isNotEmpty()) {
// checkedItem = list[0]
// checkedItem!!.photoUri = lastPhotoUri
// updateCurrentFood(checkedItem)
// lastPhotoUri = null
// }
// }
// }
SensorScaleUtils.addWeightListener { weight ->
Timber.d("registerDataChange weight = $weight")
presentation?.updateWeight(weight)
if (weight <= 0.005) {//余量取餐,检测到秤上没有东西,重新启动识别菜品 && presentation?.mealPickupMode == 1
isRecognitionFood = true
}
if (weight - lastWeight > 0.05 && isRecognitionFood) { // 大于50g
if (presentation?.mealPickupMode == 1) {
isRecognitionFood = false
}
debouncer.debounce {
recognizeFood()
}
}
lastWeight = weight
}
}
fun recognizeFood() {
LightManager.openRedLight()
LightManager.openGreenLight()
takePhoto { photoUri ->
lastPhotoUri = photoUri
Timber.d("registerDataChange photoUri = ${photoUri.path}")
// viewModel.getIdentifiedFoodList()
ImageUtil.uriToBitmap(this, photoUri)?.let { bitmap ->
Timber.d("registerDataChange photoUri 拿到bitmap")
// val bmp = BitmapCropper.cropCenter(bitmap, 1300, 900)
Timber.d("registerDataChange photoUri bitmap裁剪完成")
val file = BitmapSaver.saveToAppFilesDir(
bitmap,
this,
"IMG_CROP_${System.currentTimeMillis()}.jpg"
)
Timber.d("registerDataChange photoUri bitmap保存文件路径:${file?.absolutePath}")
//val nameList = FoodModule.queryFood(bitmap)
val scoreList = FoodModule.getFoodScoreList(bitmap)
Timber.d(
"registerDataChange photoUri 拿到识别数据:${
GsonUtils.toJson(
scoreList
)
}"
)
val nameList = scoreList.map { it.name }
val foodName = nameList.joinToString(separator = ",")
Timber.d("registerDataChange photoUri 识别数据名称:$foodName")
if (TextUtils.isEmpty(foodName)) {
LightManager.closeGreenLight()
LightManager.closeRedLight()
//binding.layoutRescan.visibility = View.VISIBLE
binding.flPay.visible()
binding.tvToSearch.let {
it.text = "未识别到,去手动搜索"
it.visible()
}
} else {
//binding.layoutRescan.visibility = View.GONE
viewModel.getFoodInfo(foodName) { list ->
runOnUiThread {
updateFoodInfo(list.toMutableList(), scoreList)
}
}
}
}
}
}
@SuppressLint("NotifyDataSetChanged")
private fun updateFoodInfo(list: MutableList<FoodInfo>, scoreList: List<IdNameScore>) {
Timber.d(
"registerDataChange识别后查询接口数据:${GsonUtils.toJson(list)},识别数据:${
GsonUtils.toJson(
scoreList
)
}"
)
list.forEach { foodInfo ->
val scoreItem = scoreList.firstOrNull { it.name == foodInfo.foodName }
val score = scoreItem?.score ?: 0.0
foodInfo.score = ((1 - score) * 10000).roundToInt()
}
searchFoodList.clear()
searchFoodList.addAll(list)
adapter.notifyDataSetChanged()
//adapter.updateData(list)
if (list.isNotEmpty()) {
checkedItem = list[0]
checkedItem!!.photoUri = lastPhotoUri
checkedItem!!.isChecked = true
adapter.notifyItemChanged(0)
updateCurrentFood(checkedItem)
lastPhotoUri = null
LightManager.closeRedLight()
binding.flPay.visible()
binding.tvToSearch.let {
it.text = "以上都不是,手动搜索"
it.visible()
}
} else {
binding.flPay.gone()
binding.tvToSearch.gone()
}
}
// private fun createAdapter(): GenericItemAdapter<FoodInfo, ItemFoodInfoBinding> {
// return GenericItemAdapter(
// items = emptyList(),
// bindingInflater = ItemFoodInfoBinding::inflate,
// bindCallback = { item, position ->
// val score = (item.score / 100.0).format2String(2)
// this.tvName.text = "${item.foodName}\n${score}%"
// if (item.id != checkedItem?.id) {
// this.tvName.typeface = Typeface.defaultFromStyle(Typeface.NORMAL)
// this.tvName.setTextColor(resources.getColor(R.color.identify_normal))
// this.llRoot.setBackgroundResource(R.drawable.grid_item_normal)
// } else {
// this.tvName.typeface = Typeface.defaultFromStyle(Typeface.BOLD)
// this.tvName.setTextColor(resources.getColor(R.color.identify_checked))
// this.llRoot.setBackgroundResource(R.drawable.grid_item_checked)
// }
////
// this.llRoot.setOnClickListener {
// Timber.d("itemClick ${item.foodName}, position = $position")
// item.photoUri = null
// checkedItem = item
// adapter.notifyDataSetChanged()
// updateCurrentFood(item)
// }
// }
// )
// }
private fun setupCamera() {
Timber.d("setupCamera")
cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture!!.addListener({
val cameraProvider = cameraProviderFuture!!.get()
// 1. 创建预览用例
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(binding.previewView.surfaceProvider)
}
// 2. 创建图像分析用例(用于副屏显示)
imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
// .setTargetResolution(Size(640, 480)) // 降低分辨率减轻负担
.build()
.also { analysis ->
analysis.setAnalyzer(executor) { imageProxy ->
// 仅在步骤1时传递
if (isAnalyzing) {
val bitmap = imageProxy.toBitmap()
presentation?.updateImage(bitmap) // 更新副屏
}
imageProxy.close() // 必须关闭以释放资源
}
}
// 3. 创建拍照用例
imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
// .setTargetResolution(Size(1920, 1080)) // 设置拍照分辨率
.build()
// 4. 选择摄像头
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// 解绑所有用例
cameraProvider.unbindAll()
// 绑定三个用例到生命周期
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageAnalysis, imageCapture
)
} catch (exc: Exception) {
Timber.e(exc.message)
}
}, ContextCompat.getMainExecutor(this))
}
fun pauseAnalysis() {
Timber.d("pauseAnalysis")
isAnalyzing = false
imageAnalysis?.clearAnalyzer()
}
fun resumeAnalysis() {
Timber.d("resumeAnalysis isAnalyzing = $isAnalyzing")
if (isAnalyzing) return
// isAnalyzing = true
imageAnalysis?.setAnalyzer(executor) { imageProxy ->
if (presentation?.currentStep == 1) {
val bitmap = imageProxy.toBitmap()
presentation?.updateImage(bitmap)
}
imageProxy.close()
}
}
/**
* 停止相机识别
*/
private fun shutdownCamera() {
Timber.d("shutdownCamera")
try {
// 1. 首先停止图像分析
imageAnalysis?.clearAnalyzer()
// 2. 解除所有绑定
cameraProviderFuture?.get()?.unbindAll()
// 3. 关闭相关资源
imageCapture = null
imageAnalysis = null
// 4. 停止副屏更新
isAnalyzing = false
} catch (e: Exception) {
Timber.e(e, "Error shutting down camera")
}
}
// 拍照功能
private fun takePhoto(callback: (Uri) -> Unit) {
Timber.d("takePhoto")
// 临时停止分析以避免干扰
isAnalyzing = false
// 创建输出选项
val executor = ContextCompat.getMainExecutor(this)
val cacheDir = cacheDir
val photoFile = File.createTempFile(
"IMG_${System.currentTimeMillis()}",
".jpg",
cacheDir
)
val cacheOutputOptions =
ImageCapture.OutputFileOptions.Builder(photoFile).build()
// 执行拍照
imageCapture?.takePicture(
cacheOutputOptions,
executor,
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
runOnUiThread {
val savedUri = outputFileResults.savedUri
Timber.d("takePhoto savedUri = $savedUri")
// ToastUtils.showToast("照片保存成功: $savedUri")
if (savedUri != null) {
callback(savedUri)
}
}
// 恢复图像分析
isAnalyzing = true
}
override fun onError(exception: ImageCaptureException) {
Timber.e("拍照失败: ${exception.message}")
runOnUiThread {
ToastUtils.showToast("拍照失败: ${exception.message}")
}
// 恢复图像分析
isAnalyzing = true
}
}
)
}
private fun setupSecondaryDisplay() {
val displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
val displays = displayManager.displays
// 查找副屏(通常索引为1
if (displays.size > 1) {
val secondaryDisplay = displays[1]
// 创建副屏的Presentation
presentation = MainScreenPresentation(
activity = this,
display = secondaryDisplay,
userViewModel = viewModel,
recognizeViewModel = viewModels<RecognizeViewModel>().value
).apply {
setStepChangeCallback { step ->
if (step == 1) {
Timber.d("setStepChangeCallback${step}")
updateCurrentFood(null)
bottomSheetDialog?.dismiss()
}
}
}
//SecondaryScreenPresentation(
// activity = this,
// this,
// display = secondaryDisplay,
// viewModel = viewModel,
// recognizeViewModel = viewModels<RecognizeViewModel>().value
//)
// presentation!!.setStepChangeCallback { step ->
// if (step == 1) {
// Timber.d("setStepChangeCallback${step}")
// updateCurrentFood(null)
// bottomSheetDialog?.dismiss()
// }
// }
presentation!!.show()
} else {
ToastUtils.showToast("获取副屏失败")
}
}
@SuppressLint("NotifyDataSetChanged")
fun clearFoodList() {
//adapter.updateData(listOf())
searchFoodList.clear()
adapter.notifyDataSetChanged()
}
override fun onDestroy() {
//SensorScaleUtils.closeScale()
presentation?.dismiss()
super.onDestroy()
shutdownCamera()
}
private fun updateDateTime() {
val formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 E", Locale.CHINA)
val dateTime = formatter.format(LocalDateTime.now(ZoneId.of("Asia/Shanghai")))
binding.tvTitleTime.text = dateTime
}
override fun onResume() {
super.onResume()
// 启动定时器
handler.postDelayed(timeoutRunnable, TIME_OUT)
if (presentation?.isShowing == false) {
setupSecondaryDisplay()
}
val mode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0)
presentation?.updateMealPickupMode(mode ?: 0)
}
override fun onPause() {
super.onPause()
// 取消定时器
handler.removeCallbacks(timeoutRunnable)
}
private var lastTouchTime = 0L
private val timeoutRunnable = Runnable {
//显示待机页面
presentation?.loadStandbyScreen()
}
override fun onUserInteraction() {
super.onUserInteraction()
if (System.currentTimeMillis() - lastTouchTime >= TIME_OUT) {
//隐藏待机页面
presentation?.hideStandbyScreen()
}
// 重置定时器
handler.removeCallbacks(timeoutRunnable)
handler.postDelayed(timeoutRunnable, TIME_OUT)
lastTouchTime = System.currentTimeMillis()
}
fun resetTouchTime() {
handler.removeCallbacks(timeoutRunnable)
handler.postDelayed(timeoutRunnable, TIME_OUT)
lastTouchTime = System.currentTimeMillis()
}
private fun createOrder(foodInfo: FoodInfo) {
val mode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
val realWeight = (lastWeight * 1000).roundToInt()
val order = FoodOrder(
deviceId = GlobalData.deviceId,
foodId = foodInfo.foodId,
foodName = foodInfo.foodName ?: "",
foodMaterialId = foodInfo.foodMaterialId ?: "",
specId = foodInfo.specId ?: "",
foodWeight = realWeight,
eatWeight = realWeight,
//根据specId对应规格重量计算
eatNum = getEatNum(realWeight, foodInfo.specWeight ?: 0.0),
userId = null,
notPay = mode != 0,
)
viewModel.createOrder(order) { orderId ->
runOnUiThread {
if (orderId.isBlank()) {
ToastUtils.showToast("订单id为空")
return@runOnUiThread
}
foodOrderId = orderId
presentation?.dismiss()
startActivity(Intent(this, PayActivity::class.java).apply {
putExtra(PayActivity.FOOD_INFO, foodInfo)
putExtra(PayActivity.FOOD_ORDER_ID, orderId)
})
// ToastUtils.showToast("订单已生成")
}
}
}
private fun getEatNum(realWeight: Int, specWeight: Double): Int {
if (realWeight == 0 || specWeight == 0.0) return 0
return (realWeight / specWeight).roundToInt()
}
private var foodOrderId: String = ""
private var isExist = false
fun addBackEventListener() {
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (isExist.not()) {
ToastUtils.showToast("再按一次退出")
isExist = true
binding.root.postDelayed({
isExist = false
}, 2000)
return
}
finish()
}
})
}
}