1068 lines
38 KiB
Kotlin
1068 lines
38 KiB
Kotlin
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 com.google.common.util.concurrent.ListenableFuture
|
||
import com.sw.dualscreen.GlobalData
|
||
import com.sw.dualscreen.GlobalKey
|
||
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.load
|
||
import com.sw.dualscreen.ext.visible
|
||
import com.sw.dualscreen.model.response.ChargeModeEvent
|
||
import com.sw.dualscreen.model.response.ClickBackEvent
|
||
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.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.abs
|
||
import kotlin.math.roundToInt
|
||
|
||
/**
|
||
* 服务员显示界面
|
||
*/
|
||
@SuppressLint("NotifyDataSetChanged")
|
||
class MainActivity : BaseActivity<ActivityMainBinding>() {
|
||
|
||
companion object {
|
||
private const val TAG = "MainActivity"
|
||
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 {
|
||
MainFoodListAdapter(searchFoodList).apply {
|
||
setOnItemClickListener { adapter, view, position ->
|
||
if (searchFoodList[position].isChecked) {
|
||
return@setOnItemClickListener
|
||
}
|
||
searchFoodList.forEachIndexed { index, info ->
|
||
searchFoodList[index].isChecked = index == position
|
||
}
|
||
val item = searchFoodList[position]
|
||
log("itemClick ${item.foodName}, position = $position")
|
||
item.photoUri = null
|
||
checkedItem = item
|
||
notifyDataSetChanged()
|
||
updateCurrentFood(item)
|
||
|
||
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: Double = 0.0
|
||
private var lastPhotoUri: Uri? = null // 最后拍照的图片
|
||
private var debouncer = Debouncer(2000)
|
||
private var isRecognitionFood = true
|
||
private var isFirstOpen = true
|
||
|
||
private var isStartRecognize = false
|
||
|
||
override fun getViewModel(): BaseViewModel {
|
||
return viewModel
|
||
}
|
||
|
||
override fun inflateViewBinding(): ActivityMainBinding {
|
||
return ActivityMainBinding.inflate(layoutInflater)
|
||
}
|
||
|
||
override fun initialize() {
|
||
super.initialize()
|
||
isPageVisible = true
|
||
addBackEventListener()
|
||
initView()
|
||
if (SpTool.firstGetFace) {
|
||
viewModel.getUserFaceCache(pageNo = 1)
|
||
}
|
||
setupCamera()
|
||
initData()
|
||
registerKeyEvent()
|
||
}
|
||
|
||
private fun initData() {
|
||
startFaceTask()
|
||
}
|
||
|
||
private fun initView() {
|
||
viewModel.getDeviceConfig { deviceConfig ->
|
||
runOnUiThread {
|
||
if (deviceConfig == null) {
|
||
ToastUtils.showToast("获取设备配置数据失败")
|
||
return@runOnUiThread
|
||
}
|
||
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)
|
||
searchFoodList.forEachIndexed { index, info ->
|
||
searchFoodList[index].isChecked = false
|
||
}
|
||
adapter.notifyDataSetChanged()
|
||
}
|
||
bottomSheetDialog!!.show(supportFragmentManager, "CustomBottomSheetDialog")
|
||
}
|
||
|
||
binding.recyclerview.let {
|
||
it.layoutManager = GridLayoutManager(this, 2)
|
||
it.adapter = adapter
|
||
}
|
||
binding.tvFoodName.setOnClickListener {
|
||
SensorScaleUtils.zero()
|
||
}
|
||
binding.btnPay.setOnClickListener {
|
||
clickPayButton()
|
||
}
|
||
}
|
||
|
||
private var recognizeWeight = 0.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)
|
||
}
|
||
|
||
override fun registerDataChange() {
|
||
super.registerDataChange()
|
||
SensorScaleUtils.addWeightListener { weight ->
|
||
log("registerDataChange weight = $weight")
|
||
//runOnUiThread { binding.tvShowWeight.text="${weight*1000}克" }
|
||
if (lastWeight - weight > 0.005) {
|
||
//及放及取,从秤上拿走超过5克
|
||
if (presentation?.mealPickupMode == 0) {
|
||
presentation?.updateWeight(weight)
|
||
lastWeight = weight
|
||
return@addWeightListener
|
||
}
|
||
}
|
||
if (isPageVisible.not() || isStartRecognize.not()) {
|
||
if (ActivityManager.currentActivity() is MainActivity) {
|
||
isPageVisible = true
|
||
isStartRecognize = true
|
||
}
|
||
return@addWeightListener
|
||
}
|
||
//0.05
|
||
// val isWeightChange = abs(weight - lastWeight) > 0.02
|
||
val isWeightChange = weight - lastWeight > 0.02
|
||
log("recognizeByWeight---00001,weight=$weight,lastWeight=$lastWeight,isWeightChange=$isWeightChange")
|
||
recognizeByWeight(weight, isWeightChange)
|
||
}
|
||
}
|
||
|
||
private fun recognizeByWeight(weight: Double, isWeightChange: Boolean, block: () -> Unit = {}) {
|
||
presentation?.updateWeight(weight)
|
||
|
||
if (weight <= 0.005) {
|
||
log("recognizeByWeight---00002,秤重未超过5克")
|
||
//检测到秤上没有东西,重新启动识别菜品 && presentation?.mealPickupMode == 1
|
||
isRecognitionFood = true
|
||
return
|
||
}
|
||
|
||
if (isWeightChange && isRecognitionFood) { // 大于50g
|
||
log("recognizeByWeight---00003,mealPickupMode=${presentation?.mealPickupMode}")
|
||
if (presentation?.mealPickupMode == 1) {
|
||
isRecognitionFood = false
|
||
}
|
||
debouncer.debounce {
|
||
log("recognizeByWeight---00004")
|
||
recognizeFood()
|
||
if (presentation?.mealPickupMode == 0) {
|
||
lastWeight = weight
|
||
}
|
||
}
|
||
} else {
|
||
block()
|
||
}
|
||
|
||
if (presentation?.mealPickupMode == 1) {
|
||
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
|
||
// }
|
||
searchFoodList.clear()
|
||
searchFoodList.addAll(list)
|
||
adapter.notifyDataSetChanged()
|
||
|
||
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
|
||
|
||
// 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 clearFoodList() {
|
||
//adapter.updateData(listOf())
|
||
searchFoodList.clear()
|
||
adapter.notifyDataSetChanged()
|
||
}
|
||
|
||
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
|
||
// 启动定时器
|
||
handler.postDelayed(timeoutRunnable, TIME_OUT)
|
||
// ToastUtils.showToast("isPageVisible=$isPageVisible")
|
||
if (isFirstOpen) {
|
||
//首次
|
||
setupSecondaryDisplay()
|
||
presentation?.step1FoodRecognizing()
|
||
isFirstOpen = false
|
||
} else {
|
||
//副屏未显示重新加载
|
||
isRecognitionFood = true
|
||
log("onResume-00001,isRefreshPage=$isRefreshPage")
|
||
if (presentation == null || !presentation!!.isShowing) {
|
||
log("onResume-00002,副屏未显示,加载副屏")
|
||
lastWeight = 0.0
|
||
setupSecondaryDisplay()
|
||
presentation?.step1FoodRecognizing()
|
||
} else {
|
||
log("onResume-00003,副屏显示,更新副屏, isRefreshPage=$isRefreshPage")
|
||
if (isRefreshPage) {
|
||
isRefreshPage = false
|
||
//已显示副屏,则进行更新
|
||
val weight = lastWeight
|
||
lastWeight = 0.0
|
||
presentation?.hideRecImage()
|
||
presentation?.step1FoodRecognizing()
|
||
|
||
binding.root.postDelayed({
|
||
recognizeByWeight(weight, true)
|
||
}, 500)
|
||
}
|
||
}
|
||
}
|
||
|
||
//根据付费模式确认是否显示按钮
|
||
binding.btnPay.run {
|
||
val mode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
|
||
if (mode == 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?,
|
||
notEnoughOneBlock: () -> Unit = {},
|
||
block: () -> Unit
|
||
) {
|
||
log("foodWeight=$foodWeight,eatWeight=$eatWeight")
|
||
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 chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
|
||
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
|
||
)
|
||
submitOrder(order, block)
|
||
}
|
||
|
||
private fun submitOrder(order: FoodOrder, block: () -> Unit) {
|
||
viewModel.createOrder(order) { orderId ->
|
||
runOnUiThread {
|
||
binding.root.postDelayed({
|
||
hideWaitingDialog()
|
||
if (orderId.isBlank()) {
|
||
//ToastUtils.showToast("订单id为空")
|
||
return@postDelayed
|
||
}
|
||
foodOrderId = orderId
|
||
block()
|
||
|
||
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
|
||
}
|
||
|
||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||
fun onPaySuccessEvent(event: PaySuccessEvent) {
|
||
isRefreshPage = true
|
||
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0) ?: 0
|
||
presentation?.hideNutritionView(chargeMode)
|
||
presentation?.cancel()
|
||
}
|
||
|
||
@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()
|
||
val mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
|
||
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0) ?: 0
|
||
var foodWeight = 0.0
|
||
var eatWeight = 0.0
|
||
if (chargeMode == 0) {
|
||
//计费
|
||
if (mealPickupMode == 0) {
|
||
//即放即取
|
||
eatWeight = lastWeight * 1000
|
||
foodWeight = lastWeight * 1000
|
||
} else {
|
||
//余量计量
|
||
eatWeight = (recognizeWeight - lastWeight) * 1000
|
||
foodWeight = recognizeWeight * 1000
|
||
log("eatWeight=$eatWeight, recognizeWeight=$recognizeWeight, lastWeight=$lastWeight")
|
||
}
|
||
} else {
|
||
//不计费
|
||
foodWeight = recognizeWeight * 1000
|
||
eatWeight = if (mealPickupMode == 0) {
|
||
//即放即取
|
||
recognizeWeight * 1000
|
||
} else {
|
||
//余量计量
|
||
(recognizeWeight - lastWeight) * 1000
|
||
}
|
||
}
|
||
val eatNum = getEatNum(eatWeight, checkedItem!!.specWeight)
|
||
createOrder(
|
||
foodInfo = checkedItem!!,
|
||
foodWeight = foodWeight.roundToInt(),
|
||
eatWeight = eatWeight.roundToInt(),
|
||
eatNum = eatNum,
|
||
userId = null,
|
||
) {
|
||
isRefreshPage = false
|
||
//presentation?.dismiss()
|
||
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 = 30 * 1000L
|
||
private val dealyMillis = 3 * 60 * 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 {
|
||
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
|
||
//0-计费,1-不计费
|
||
if (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)
|
||
// }
|
||
|
||
} |