Files
StallDualScreen/app/src/main/java/com/sw/dualscreen/activity/MainActivity.kt
T
2026-03-30 18:03:01 +08:00

1202 lines
43 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.Bitmap
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.Display
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.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.common.util.concurrent.ListenableFuture
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.GlobalKey
import com.sw.dualscreen.adapter.FoodOrderAdapter
import com.sw.dualscreen.adapter.MainFoodListAdapter
import com.sw.dualscreen.databinding.ActivityMainBinding
import com.sw.dualscreen.dialog.RemindDialog
import com.sw.dualscreen.ext.dp
import com.sw.dualscreen.ext.gone
import com.sw.dualscreen.ext.invisible
import com.sw.dualscreen.ext.load
import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.model.response.ChargeModeEvent
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.FoodOrder
import com.sw.dualscreen.model.response.PaySuccessEvent
import com.sw.dualscreen.model.response.ResetRecognizeEvent
import com.sw.dualscreen.model.response.UpdateRefreshEvent
import com.sw.dualscreen.model.response.UserFaceModel
import com.sw.dualscreen.objbox.Food
import com.sw.dualscreen.objbox.FoodModule
import com.sw.dualscreen.objbox.FoodModule.IdNameScore
import com.sw.dualscreen.objbox.ObjectBox
import com.sw.dualscreen.presentation.MainScreenPresentation
import com.sw.dualscreen.sdk.SensorScaleUtils
import com.sw.dualscreen.socket.TcpClient
import com.sw.dualscreen.utils.ActivityManager
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.IntervalExecutor
import com.sw.dualscreen.utils.SPUtil
import com.sw.dualscreen.utils.SpTool
import com.sw.dualscreen.view.CustomBottomSheetDialog
import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel
import com.sw.plate.utils.Base64
import com.sw.plate.utils.LightManager
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.facedb.FaceDatabase
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.json.JSONObject
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 {
private const val TAG = "MainActivity"
const val TIME_OUT = 5 * 60 * 1000L
const val WEIGHT_CHANGE_VALUE = 25
const val WEIGHT_RESET_RECOGNIZE = 10
}
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 adapter by lazy {
MainFoodListAdapter(mutableListOf()).apply {
setOnItemClickListener { adapter, view, position ->
if (items[position].isChecked) {
return@setOnItemClickListener
}
items.forEachIndexed { index, info ->
items[index].isChecked = index == position
}
val item = items[position]
log("itemClick ${item.foodName}, position = $position")
item.photoUri = null
checkedItem = item
notifyDataSetChanged()
updateCurrentFood(item)
if (settlementMode == 0) {
//联合支付,切换菜品重新请求接口
presentation?.let {
it.pauseCamera()
it.step2FaceRecognizing(item, true)
}
// presentation?.step3ShowRecognizeResult(presentation!!.currentUserId?:"")
} else {
//0-计费,1-不计费
val mode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
//不计费模式切换菜品,重新加载数据
if (mode == 1) {
presentation?.isAgainLoadData = false
presentation?.loadUnbilledMode()
}
}
}
}
}
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: Int = 0
private var lastPhotoUri: Uri? = null // 最后拍照的图片
private var debouncer = Debouncer(5000)
private var isRecognitionFood = true
private var isFirstOpen = true
private var isStartRecognize = false
//0-联合支付,1-独立支付
var settlementMode: Int = 1
// val settlementMode by lazy {
// SPUtil.getInstance().get(GlobalKey.KEY_SETTLEMENT_MODE, 1)
// }
override fun getViewModel(): BaseViewModel {
return viewModel
}
override fun inflateViewBinding(): ActivityMainBinding {
return ActivityMainBinding.inflate(layoutInflater)
}
override fun initialize() {
super.initialize()
isPageVisible = true
// 拦截扫码枪按键事件,防止触发页面按钮点击
registerKeyEvent()
addBackEventListener()
initView()
if (SpTool.firstGetFace) {
viewModel.getUserFaceCache(pageNo = 1)
}
setupCamera()
initData()
}
private fun initData() {
startFaceTask()
}
private fun initView() {
viewModel.getDeviceConfig { deviceConfig ->
runOnUiThread {
if (deviceConfig == null) {
ToastUtils.showToast("获取设备配置数据失败")
return@runOnUiThread
}
//结算模式:1-独立支付,2-联合结算
settlementMode = if (deviceConfig.payType == 2) 0 else 1
SPUtil.getInstance().put(GlobalKey.KEY_SETTLEMENT_MODE, settlementMode)
GlobalData.appId = deviceConfig.arcsoftAppId ?: ""
GlobalData.sdkKey = deviceConfig.arcsoftSdkKey ?: ""
GlobalData.activeKey = deviceConfig.arcsoftActiveKey ?: ""
viewModel.activeEngine()
}
}
updateDateTime()
binding.ivSetting.setOnClickListener {
isRefreshPage = false
startActivity(Intent(this, SettingActivity::class.java))
}
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)
adapter.items.forEachIndexed { index, info ->
adapter.items[index].isChecked = false
}
adapter.notifyDataSetChanged()
}
bottomSheetDialog!!.show(supportFragmentManager, "CustomBottomSheetDialog")
}
binding.recyclerview.let {
it.layoutManager = GridLayoutManager(this, 2, GridLayoutManager.VERTICAL, false)
it.adapter = adapter
}
binding.tvFoodName.setOnClickListener {
SensorScaleUtils.zero()
}
binding.btnPay.setOnClickListener {
clickPayButton()
}
}
private var recognizeWeight = 0
fun updateCurrentFood(foodInfo: FoodInfo?) {
if (foodInfo == null) {
binding.tvFoodName.text = "-"
binding.previewView.visibility = View.VISIBLE
binding.ivImg.visibility = View.GONE
resumeAnalysis()
return
}
pauseAnalysis()
recognizeWeight = lastWeight
log("registerDataChange,副屏updateFood调用前耗时:${System.currentTimeMillis() - startTime}")
startTime = System.currentTimeMillis()
presentation?.updateFood(foodInfo)
binding.tvFoodName.text = foodInfo.foodName
binding.previewView.gone()
binding.ivImg.visible()
binding.ivImg.load(foodInfo.foodImg)
//pauseAnalysis()
}
fun updateImage(imgUrl: Any?) {
binding.ivImg.load(imgUrl)
}
/**
* 联合支付-即放即取模式
*/
private fun loadJFJQBySettlement(weight: Int) {
//即放即取+联合支付人脸查询订单完成
if (presentation!!.jointPaymentQueryFinish) {
if (weight > WEIGHT_RESET_RECOGNIZE) {
//已识别完成人脸并查询到订单数据,增加重量不再重新识别
return
}
//重量小于等于5克时改为识别中页面
isAnalyzing = false
presentation?.step1FoodRecognizing()
return
}
if (weight <= WEIGHT_RESET_RECOGNIZE) {
//秤上重量过下,显示识别中页面
presentation?.step1FoodRecognizing()
return
}
// val isWeightChange = abs(weight - lastWeight) > 0.02
val isWeightChange = weight - lastWeight > WEIGHT_CHANGE_VALUE
if (weight != lastWeight) {
log("recognizeByWeight---00001,weight=$weight,lastWeight=$lastWeight,isWeightChange=$isWeightChange")
}
recognizeByWeight(weight, isWeightChange)
}
/**
* 联合支付-余量计量模式
*/
private fun loadYLJLBySettlement(weight: Int) {
if (weight <= WEIGHT_RESET_RECOGNIZE) {
//秤上重量过小,显示识别中页面
presentation?.step1FoodRecognizing()
lastWeight = weight
return
}
//余量计量+联合支付人脸查询订单完成
if (presentation!!.jointPaymentQueryFinish) {
presentation?.updateWeight(weight)
lastWeight = weight
return
}
val isWeightChange = weight - lastWeight > WEIGHT_CHANGE_VALUE
recognizeByWeight(weight, isWeightChange)
}
private fun readWeight(weight: Int) {
log("registerDataChange weight = $weight")
binding.tvShowWeight.let {
if (it.text.toString() != weight.toString()) {
it.text = "$weight"
}
}
if (weight <= WEIGHT_RESET_RECOGNIZE) {
//检测到秤上没有东西,重新启动识别菜品 && presentation?.mealPickupMode == 1
checkedItem = null
}
if (settlementMode == 0) {
//联合支付-------------------------------------------
//0-即放即取,1-余量计量
val mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
if (mealPickupMode == 1) {
loadYLJLBySettlement(weight)
return
}
loadJFJQBySettlement(weight)
return
}
//独立支付-------------------------------------------
if (lastWeight - weight > 5) {
//及放及取,从秤上拿走超过5克
if (presentation?.mealPickupMode == 0) {
presentation?.updateWeight(weight)
lastWeight = weight
return
}
}
if (isPageVisible.not() || isStartRecognize.not()) {
if (ActivityManager.currentActivity() is MainActivity) {
isPageVisible = true
isStartRecognize = true
}
return
}
val isWeightChange = weight - lastWeight > WEIGHT_CHANGE_VALUE
recognizeByWeight(weight, isWeightChange)
}
override fun registerDataChange() {
super.registerDataChange()
SensorScaleUtils.addWeightListener { value ->
runOnUiThread {
readWeight(value)
}
}
}
private fun recognizeByWeight(weight: Int, isWeightChange: Boolean, block: () -> Unit = {}) {
if (weight != lastWeight) {
val payTypeDesc = if (settlementMode == 0) "联合支付" else "独立支付"
log("readWeight-${payTypeDesc},weight=$weight,lastWeight=$lastWeight,isWeightChange=$isWeightChange")
}
presentation?.updateWeight(weight)
if (weight <= WEIGHT_RESET_RECOGNIZE) {
log("recognizeByWeight---00002,秤重未超过${WEIGHT_RESET_RECOGNIZE}克")
//检测到秤上没有东西,重新启动识别菜品 && presentation?.mealPickupMode == 1
isRecognitionFood = true
return
}
if (isWeightChange && isRecognitionFood) {
log("recognizeByWeight---00003,mealPickupMode=${presentation?.mealPickupMode}")
if (presentation?.mealPickupMode == 1 && checkedItem != null) {
isRecognitionFood = false
return
}
debouncer.debounce {
log("recognizeByWeight---00004")
recognizeFood()
if (presentation?.mealPickupMode == 0) {
lastWeight = weight
}
}
} else {
block()
}
if (presentation?.mealPickupMode == 1 || settlementMode == 0) {
lastWeight = weight
}
}
private var startTime = 0L
fun recognizeFood() {
startTime = System.currentTimeMillis()
// LightManager.openRedLight()
LightManager.closeRedLight()
LightManager.openGreenLight()
showWaitingDialog("识别中,请稍后……")
takePhoto(successCallback = { photoUri ->
Thread {
onTakePhotoSuccess(photoUri)
}.start()
failCount = 0
}, failureCallback = {
//ToastUtils.showToast("拍照异常,请重试")
failCount++
hideWaitingDialog()
shutdownCamera()
setupCamera()
binding.root.postDelayed({
recognizeFood()
}, 1000)
})
}
/**
* 识别相机异常次数
*/
private var failCount = 0
@SuppressLint("NotifyDataSetChanged")
private fun updateFoodInfo(list: MutableList<FoodInfo>, scoreList: List<IdNameScore>) {
val queryData = GsonUtils.toJson(list)
val recData = GsonUtils.toJson(scoreList)
log("registerDataChange识别后查询接口数据:$queryData,识别数据:$recData")
if (list.isEmpty()) {
binding.tvToSearch.let {
it.text = "未查询到,手动搜索"
it.visible()
}
return
}
list.forEach { foodInfo ->
val scoreItem = scoreList.firstOrNull { it.name == foodInfo.foodName }
val score = scoreItem?.score ?: 0.0
foodInfo.score = ((1 - score) * 10000).roundToInt()
}
//接口已处理排序
// val list2 = try {
// val orderList = scoreList.map { it.name.trim() }
// list.sortedBy { orderList.indexOf(it.foodName) }
// } catch (e: Exception) {
// e.printStackTrace()
// list
// }
adapter.submitList(list)
binding.recyclerview.visible()
checkedItem = list[0].also {
it.photoUri = lastPhotoUri
it.isChecked = true
}
adapter.notifyItemChanged(0)
updateCurrentFood(checkedItem)
// lastPhotoUri = null
LightManager.closeRedLight()
binding.tvToSearch.let {
it.text = "以上都不是,手动搜索"
it.visible()
}
}
private fun setupCamera() {
log("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) // 更新副屏
isStartRecognize = true
}
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 (e: Exception) {
log("setupCamera方法异常:${e.message}")
}
}, ContextCompat.getMainExecutor(this))
}
fun pauseAnalysis() {
log("pauseAnalysis")
isAnalyzing = false
imageAnalysis?.clearAnalyzer()
}
fun resumeAnalysis() {
log("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() {
log("shutdownCamera")
try {
// 1. 首先停止图像分析
imageAnalysis?.clearAnalyzer()
// 2. 解除所有绑定
cameraProviderFuture?.get()?.unbindAll()
// 3. 关闭相关资源
imageCapture = null
imageAnalysis = null
cameraProviderFuture = null
// 4. 停止副屏更新
isAnalyzing = false
} catch (e: Exception) {
log("Error shutting down camera:${e.message}")
}
}
// 拍照功能
private fun takePhoto(successCallback: (Uri) -> Unit, failureCallback: () -> Unit) {
log("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) {
val savedUri = outputFileResults.savedUri
log("takePhoto savedUri = $savedUri")
if (savedUri != null) {
successCallback(savedUri)
}
// 恢复图像分析
isAnalyzing = true
}
override fun onError(exception: ImageCaptureException) {
log("takePhoto拍照失败: ${exception.message}")
failureCallback()
//ToastUtils.showToast("拍照失败: ${exception.message}")
// 恢复图像分析
isAnalyzing = true
}
})
}
private fun getSecondaryDisplay(): Display? {
val displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
val displays = displayManager.displays
// 查找副屏(通常索引为1
if (displays.size > 1) return displays[1]
return null
}
private val recognizeViewModel by lazy { viewModels<RecognizeViewModel>().value }
private fun setupSecondaryDisplay() {
val secondaryDisplay = getSecondaryDisplay()
if (secondaryDisplay == null) {
ToastUtils.showToast("获取副屏失败")
return
}
// 创建副屏的Presentation
presentation = MainScreenPresentation(
activity = this,
display = secondaryDisplay,
userViewModel = viewModel,
recognizeViewModel = recognizeViewModel
).apply {
setStepChangeCallback { step ->
if (step == 1) {
log("setStepChangeCallback${step}")
updateCurrentFood(null)
bottomSheetDialog?.dismiss()
}
}
show()
}
}
@SuppressLint("NotifyDataSetChanged")
fun clearRecognizeFood() {
log("clearRecognizeFood")
binding.recyclerview.invisible()
adapter.submitList(null)
binding.tvToSearch.gone()
binding.previewView.visible()
binding.ivImg.gone()
}
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
}
public var isPageVisible = true
override fun onResume() {
super.onResume()
isPageVisible = true
// shutdownCamera 后 cameraProviderFuture 置为 null,需重新初始化相机
if (cameraProviderFuture == null) {
setupCamera()
}
// 启动定时器
handler.postDelayed(timeoutRunnable, TIME_OUT)
// ToastUtils.showToast("isPageVisible=$isPageVisible")
if (isFirstOpen) {
//首次
setupSecondaryDisplay()
presentation?.step1FoodRecognizing()
isFirstOpen = false
} else {
//副屏未显示重新加载
isRecognitionFood = true
log("onResume-00001isRefreshPage=$isRefreshPage")
if (presentation == null || !presentation!!.isShowing) {
log("onResume-00002,副屏未显示,加载副屏")
lastWeight = 0
setupSecondaryDisplay()
presentation?.step1FoodRecognizing()
} else {
log("onResume-00003,副屏显示,更新副屏, isRefreshPage=$isRefreshPage")
if (isRefreshPage) {
isRefreshPage = false
//已显示副屏,则进行更新
val weight = lastWeight
lastWeight = 0
presentation?.pauseCamera()
presentation?.hideRecImage()
presentation?.step1FoodRecognizing()
log("onResume-recognizeByWeight")
binding.root.postDelayed({
recognizeByWeight(weight, true)
}, 500)
}
}
}
//根据付费模式确认是否显示按钮
binding.btnPay.run {
//0-计费,1-不计费
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
if (settlementMode == 1 && chargeMode == 0) visible() else gone()
}
}
override fun onPause() {
super.onPause()
isPageVisible = false
// 取消定时器
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()
}
fun createOrder(
foodInfo: FoodInfo,
foodWeight: Int,
eatWeight: Int,
eatNum: Int,
userId: String?,
isMember: Boolean,
notEnoughOneBlock: () -> Unit = {},
successBlock: () -> Unit
) {
log("foodWeight=$foodWeight,eatWeight=$eatWeight")
//0-计费,1-不计费
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
//0-即放即取,1-余量计量
val pickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
// if (pickupMode == 0) {
// if (foodWeight <= 5) {
// ToastUtils.showToast("餐品重量不足")
// return
// }
// if (eatWeight <= 5) {
// ToastUtils.showToast("取餐重量过小")
// return
// }
// }
if (eatNum <= 0) {
ToastUtils.showToast("您好,当前重量不足一份")
notEnoughOneBlock()
return
}
showWaitingDialog("加载中,请稍候……")
val order = FoodOrder(
deviceId = GlobalData.deviceId,
foodId = foodInfo.foodId,
foodName = foodInfo.foodName ?: "",
foodMaterialId = foodInfo.foodMaterialId ?: "",
specId = foodInfo.specId ?: "",
foodWeight = foodWeight,
eatWeight = eatWeight,
//根据specId对应规格重量计算
eatNum = eatNum,
userId = userId,
notPay = chargeMode != 0,
//即放即取-1,称重-2
mode = if (pickupMode == 1) 1 else 2,
paymentFrom = if (settlementMode == 0) 1 else 2,
member = isMember
)
submitOrder(order, successBlock)
}
private fun submitOrder(order: FoodOrder, successBlock: () -> Unit) {
viewModel.createOrder(order) { orderId ->
runOnUiThread {
binding.root.postDelayed({
hideWaitingDialog()
if (orderId.isBlank()) {
//ToastUtils.showToast("订单id为空")
return@postDelayed
}
foodOrderId = orderId
successBlock()
if (checkedItem!!.isFromSearch == true) {
//当前菜名为手动搜索选择,非识别结果,保存向量数据
Thread {
saveFoodVector(checkedItem!!)
}.start()
}
}, 300)
}
}
}
fun getEatNum(realWeight: Double?, specWeight: Double?): Int {
if (realWeight == null || realWeight <= 0.0 || specWeight == null || 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()
}
})
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onUpdateRefreshEvent(event: UpdateRefreshEvent) {
isRefreshPage = true
checkedItem = null
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onPaySuccessEvent(event: PaySuccessEvent) {
isRefreshPage = true
checkedItem = null
//0-计费,1-不计费
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0) ?: 0
presentation?.hideNutritionView(chargeMode)
presentation?.cancel()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onResetRecognizeState(event: ResetRecognizeEvent) {
// presentation?.step1FoodRecognizing()
isRefreshPage = true
checkedItem = null
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onChargeModeEvent(event: ChargeModeEvent) {
isRefreshPage = true
isStartRecognize = false
presentation?.hideNutritionView(event.chargeMode)
// presentation?.cancel()
if (event.chargeMode == 1) {
presentation?.let {
it.onStop()
it.setupArcCamera()
}
}
}
private var isRefreshPage = false
// @Subscribe(threadMode = ThreadMode.MAIN)
// fun onClickBackEvent(event: ClickBackEvent) {
// isRefreshPage = false
// }
private fun clickPayButton() {
if (checkedItem == null) {
ToastUtils.showToast("暂无识别数据,请搜索选择")
return
}
//val realWeight = (lastWeight * 1000).roundToInt()
//0-即放即取,1-余量计量
val mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
//0-计费,1-不计费
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0) ?: 0
var foodWeight = 0
var eatWeight = 0
if (chargeMode == 0) {
//计费
if (mealPickupMode == 0) {
//即放即取
eatWeight = lastWeight
foodWeight = lastWeight
} else {
//余量计量
eatWeight = recognizeWeight - lastWeight
foodWeight = recognizeWeight
log("eatWeight=$eatWeight, recognizeWeight=$recognizeWeight, lastWeight=$lastWeight")
}
} else {
//不计费
foodWeight = recognizeWeight
eatWeight = if (mealPickupMode == 0) {
//即放即取
recognizeWeight
} else {
//余量计量
recognizeWeight - lastWeight
}
}
val eatNum = getEatNum(eatWeight.toDouble(), checkedItem!!.specWeight)
createOrder(
foodInfo = checkedItem!!,
foodWeight = foodWeight,
eatWeight = eatWeight,
eatNum = eatNum,
userId = null,
isMember = false
) {
isRefreshPage = false
shutdownCamera()
startActivity(Intent(this, PayActivity::class.java).apply {
putExtra(PayActivity.FOOD_INFO, checkedItem!!)
putExtra(PayActivity.FOOD_EAT_NUM, eatNum)
putExtra(PayActivity.FOOD_ORDER_ID, foodOrderId)
})
}
}
/**
* 将uri转为文件和向量数据并提交
*/
private fun saveFoodVector(foodInfo: FoodInfo) {
try {
if (lastPhotoUri == null) {
return
}
uri2File(lastPhotoUri!!) { imageFile, imageVector ->
runBlocking {
uploadCollectFoodPics(foodInfo, imageFile, imageVector)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
/**
* 上传菜品信息
*/
private suspend fun uploadCollectFoodPics(
foodInfo: FoodInfo, imageFile: File?, imageVector: FloatArray?
) {
if (imageFile == null || imageVector == null) {
log("是否null判断,imageFile == null:${imageFile == null},imageVector == null:${imageVector == null}")
return
}
val foodId = foodInfo.foodId
val foodName = foodInfo.foodName ?: ""
val foodModelVersion = GlobalData.foodModelVersion
val params = HashMap<String, RequestBody>()
params["foodId"] = foodId.toRequestBody()
params["foodName"] = foodName.toRequestBody()
params["version"] = foodModelVersion.toRequestBody()
val files = listOf(imageFile)
val vectors = listOf(imageVector)
val foodVectorList = vectors.map {
it.joinToString(
separator = ",", prefix = "[", postfix = "]"
)
}
val foodVectorJson =
foodVectorList.joinToString(separator = ",", prefix = "[", postfix = "]")
log("json=$foodVectorJson")
params["foodVector"] = foodVectorJson.toRequestBody()
val idList = viewModel.uploadCollectFoodPics(files, params)
if (idList.isNullOrEmpty()) {
loadRemindDialog("上传失败,未返回向量数据id")
log("idList为空")
return
}
val all = ObjectBox.getAll()
val size = all.filter { it.isDel.not() }.size
log("uploadCollectFoodPics-已采集向量总数:${size}条")
val filterList = all.filter { it.collectId == idList[0] }
if (filterList.isNotEmpty()) {
loadRemindDialog("返回的id${idList[0]}已存在")
return
}
ObjectBox.put(
Food(
collectId = idList[0],
foodId = foodId,
foodName = foodName,
foodVector = imageVector,
version = foodModelVersion
)
)
}
private fun loadRemindDialog(msg: String) {
runOnUiThread {
RemindDialog(
context = context, content = msg, confirmBlock = {}).show()
}
}
private fun uri2File(uri: Uri, block: (File?, FloatArray?) -> Unit) {
ImageUtil.uriToBitmap(this, uri)?.let { bitmap ->
val imageVector = try {
FoodModule.bitmap2FloatArray(bitmap, false)
} catch (e: Exception) {
e.printStackTrace()
return@let
}
val imageFile = BitmapSaver.saveToAppFilesDir(
bitmap, this, "IMG_CROP_${System.currentTimeMillis()}.jpg"
)
log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${imageFile?.absolutePath}")
block(imageFile, imageVector)
if (bitmap.isRecycled.not()) {
bitmap.recycle()
}
}
}
override fun onDestroy() {
//SensorScaleUtils.closeScale()
faceTaskJob?.cancel()
presentation?.cancel()
shutdownCamera()
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 = 10 * 1000L
private val dealyMillis = 5 * 1000L
// private var taskPageNo = 1
fun startFaceTask() {
faceTaskJob =
intervalExecutor.startIntervalTaskWithInitialDelay(initialDelay, dealyMillis) {
val timestamp = SpTool.lastFaceTimestamp
//if (timestamp == 0L) {
// return@startIntervalTaskWithInitialDelay
//}
viewModel.getFaceIncrementList(
// pageNo = taskPageNo,
timestamp = timestamp,
onAllQueryFinished = {
// taskPageNo = 1
},
onPageQueryFinished = { list ->
runOnUiThread {
if (list.isEmpty()) {
return@runOnUiThread
}
updateFaceData(list)
}
})
}
}
private fun updateFaceData(list: List<UserFaceModel>) {
Thread {
val faceList = mutableListOf<FaceEntity>()
try {
list.forEach { model ->
if (model.faceDeleted == true) {
//删除数据
FaceDatabase.getInstance(this).faceDao().deleteFaceById(model.userId)
} else {
//保存数据
val faceEntity = FaceEntity(
model.userId, null, Base64.decode(model.faceFeatureStr)
).also {
//1-会员、2-临时用户
it.userType = if (model.member) "1" else "2"
it.cardNo = model.cardNo
}
faceList.add(faceEntity)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
try {
if (faceList.isNotEmpty()) {
FaceDatabase.getInstance(this).faceDao().insert(faceList)
}
recognizeViewModel.refreshFaceList();
} catch (e: Exception) {
e.printStackTrace()
}
}.start()
}
private fun sendFaceData(faceData: String) {
// 给某个客户端发送
val jsonObject = JSONObject().also {
it.put("type", "faceFeature")
it.put("content", faceData)
}
tcpClient?.send(jsonObject)
}
private var tcpClient: TcpClient? = null
private fun onTakePhotoSuccess(photoUri: Uri) {
lastPhotoUri = photoUri
log("registerDataChange main,takePhoto耗时:${System.currentTimeMillis() - startTime}")
startTime = System.currentTimeMillis()
log("registerDataChange photoUri = ${photoUri.path}")
// viewModel.getIdentifiedFoodList()
lifecycleScope.launch {
ImageUtil.uriToBitmap(this@MainActivity, photoUri)?.let { bitmap ->
queryFoodData(bitmap)
}
}
}
private suspend fun queryFoodData(bitmap: Bitmap) {
log("registerDataChange main,uriToBitmap耗时:${System.currentTimeMillis() - startTime}")
startTime = System.currentTimeMillis()
log("registerDataChange photoUri 拿到bitmap")
// val bmp = BitmapCropper.cropCenter(bitmap, 1300, 900)
log("registerDataChange registerDataChange photoUri bitmap裁剪完成")
val file = BitmapSaver.saveToAppFilesDir(
bitmap, this, "IMG_CROP_${System.currentTimeMillis()}.jpg"
)
log("registerDataChange main,saveToAppFilesDir耗时:${System.currentTimeMillis() - startTime}")
startTime = System.currentTimeMillis()
log("registerDataChange photoUri bitmap保存文件路径:${file?.absolutePath}")
//val nameList = FoodModule.queryFood(bitmap)
val scoreList = FoodModule.getFoodScoreList(bitmap)
val foodName = if (scoreList.isNotEmpty()) {
val recDataJson = GsonUtils.toJson(scoreList)
log("registerDataChange main,getFoodScoreList耗时:${System.currentTimeMillis() - startTime}")
startTime = System.currentTimeMillis()
log("registerDataChange photoUri 拿到识别数据:$recDataJson")
val nameList = scoreList.map { it.name }
nameList.joinToString(separator = ",")
} else ""
hideWaitingDialog()
log("registerDataChange photoUri 识别数据名称:$foodName")
runOnUiThread {
binding.flPay.run {
//0-计费,1-不计费
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
//独立支付+计费模式才显示“去结算”按钮,否则使用人脸识别
if (settlementMode == 1 && chargeMode == 0) visible() else gone()
}
}
if (TextUtils.isEmpty(foodName)) {
LightManager.closeGreenLight()
LightManager.closeRedLight()
//binding.layoutRescan.visibility = View.VISIBLE
runOnUiThread {
binding.tvToSearch.let {
it.text = "未识别到,手动搜索"
it.visible()
}
}
} else {
//binding.layoutRescan.visibility = View.GONE
showWaitingDialog("正在查询菜品信息,请稍后……")
viewModel.getFoodInfo(foodName) { list ->
runOnUiThread {
hideWaitingDialog()
log("registerDataChange main,getFoodInfo耗时:${System.currentTimeMillis() - startTime}")
startTime = System.currentTimeMillis()
updateFoodInfo(list.toMutableList(), scoreList)
}
}
}
}
// @Subscribe(threadMode = ThreadMode.MAIN)
// public fun onResetBox(event: ResetBoxEvent) {
// ObjectBox.boxStore.close()
// ObjectBox.init(this)
// }
}