package com.sw.inbound.activity import android.annotation.SuppressLint import android.app.Dialog import android.content.Context import android.graphics.Bitmap import android.net.Uri import android.os.Bundle import android.text.TextUtils import android.view.View import android.view.ViewGroup import android.widget.TextView import com.sw.inbound.base.BaseActivity import androidx.activity.viewModels import androidx.camera.view.PreviewView import androidx.core.view.updateLayoutParams import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import coil.ImageLoader import coil.annotation.ExperimentalCoilApi import com.sw.inbound.R import com.sw.inbound.adapter.FoodCollectionAdapter import com.sw.inbound.adapter.GoodsSearchAdapter import com.sw.inbound.databinding.ActivityFoodCollectionBinding import com.sw.inbound.databinding.LayoutCameraPreviewBinding import com.sw.inbound.databinding.LayoutEmptySearchBinding import com.sw.inbound.dialog.CollectSearchDialog import com.sw.inbound.dialog.CustomDialog import com.sw.inbound.dialog.Loading import com.sw.inbound.dialog.WarnDialog import com.sw.inbound.model.response.SearchGoodsInfo import com.sw.inbound.objbox.Food import com.sw.inbound.objbox.FoodCollectionBean import com.sw.inbound.objbox.FoodModule import com.sw.inbound.objbox.ObjectBox import com.sw.inbound.utils.BitmapCropper import com.sw.inbound.utils.BitmapSaver import com.sw.inbound.utils.CameraUtils import com.sw.inbound.utils.ImageUploader import com.sw.inbound.utils.ImageUtil import com.sw.inbound.utils.ext.addOnActionSearchListener import com.sw.inbound.utils.ext.clickWithDebounce import com.sw.inbound.utils.ext.dp import com.sw.inbound.utils.ext.gone import com.sw.inbound.utils.ext.hideKeyboard import com.sw.inbound.utils.ext.invisible import com.sw.inbound.utils.ext.startActivity import com.sw.inbound.utils.ext.toJsonString import com.sw.inbound.utils.ext.toObject import com.sw.inbound.utils.ext.toast import com.sw.inbound.utils.ext.visible import com.sw.inbound.viewmodel.ReceiptViewModel import dagger.hilt.android.AndroidEntryPoint import io.objectbox.Box import io.objectbox.exception.DbException import io.objectbox.exception.FileCorruptException import io.objectbox.kotlin.boxFor import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody import timber.log.Timber import java.io.File /** * 采集菜品 */ @SuppressLint("NotifyDataSetChanged") @AndroidEntryPoint class FoodCollectionActivity : BaseActivity() { companion object { const val TAG = "FoodCollectionActivity" const val MAX_COUNT = 16 const val PAGE_SIZE = 30 } // private var box: Box? = null private val collectList: MutableList = mutableListOf() private lateinit var binding: ActivityFoodCollectionBinding private lateinit var previewView: PreviewView val viewModel: ReceiptViewModel by viewModels() private val cameraUtils: CameraUtils by lazy { CameraUtils(this) } private val collectionAdapter: FoodCollectionAdapter by lazy { FoodCollectionAdapter(collectList).apply { setOnItemClickListener { adapter, view, position -> val file = collectList[position].imageFile ?: return@setOnItemClickListener ImageUtil.decodeFromFile(file)?.let { bitmap -> val imageVector = FoodModule.bitmap2FloatArray(bitmap, false) Timber.tag(TAG).d("向量:${imageVector.toJsonString()}") } } addOnItemChildClickListener(R.id.ivDelete) { _, _, position -> collectList[position].let { it.imageFile = null it.imageVector = null it.bitmap = null it.isFinish = false it.isShowCamera = true it.uploadSuccess = false } collectionAdapter.notifyItemChanged(position) // val count = collectList.count { it.imageFile != null && it.uploadSuccess.not() } // binding.btnUploadImage.text = "待上传图片${count}张" } } } override fun onResume() { super.onResume() cameraUtils.bind() binding.llCameraFlag.run { visible() postDelayed({ gone() }, 3000) } } override fun onPause() { super.onPause() cameraUtils.unbind() binding.llCameraFlag.visible() } @SuppressLint("NotifyDataSetChanged") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityFoodCollectionBinding.inflate(layoutInflater) setContentView(binding.root) cameraUtils.initCamera() binding.root.setOnClickListener { it.hideKeyboard() } val previewBinding = LayoutCameraPreviewBinding.inflate(layoutInflater, binding.flCameraPreview) previewView = previewBinding.previewView.also { it.updateLayoutParams { width = 420.dp height = 420.dp } } cameraUtils.setPreviewController(previewView) binding.btnBack.setOnClickListener { finish() } binding.rvFoodCollection.let { it.layoutManager = GridLayoutManager(this, 4, GridLayoutManager.VERTICAL, false) it.adapter = collectionAdapter } repeat(MAX_COUNT) { collectList.add(FoodCollectionBean(isShowCamera = true)) } collectionAdapter.notifyDataSetChanged() binding.ivGoodsSearch.setOnClickListener { searchGoods() } binding.etGoodsInput.let { v -> v.addOnActionSearchListener { searchGoods() } } // binding.btnUploadImage.setOnClickListener { // val count = collectList.count { it.imageFile!=null && it.uploadSuccess.not() } // if (count == 0) { // toast("已全部上传成功") // return@setOnClickListener // } // uploadAllImage() // } binding.btnSave.setOnClickListener { _ -> if (clickIndex == -1) { toast("请选择物品名称") return@setOnClickListener } val count = collectList.count { it.imageVector != null } if (count == 0) { toast("请拍摄物品照片") return@setOnClickListener } // vectorThread() // uploadAllImage() showUploadDialog() } binding.btnTakePhoto.clickWithDebounce(800) { takePhoto() } binding.btnCollectedGoods.setOnClickListener { CollectSearchDialog(this@FoodCollectionActivity).show() } binding.btnClearData.setOnClickListener { clearData() } binding.rvSearch.let { it.layoutManager = GridLayoutManager(this, 2, LinearLayoutManager.VERTICAL, false) it.adapter = searchAdapter } binding.refreshLayout.let { it.setEnableRefresh(true) it.setEnableLoadMore(false) it.setOnRefreshListener { pageNo = 1 searchGoods() } it.setOnLoadMoreListener { searchGoods() } } loadEmptyView() // 点击"图片"按钮,跳转至本地图片预览页 binding.tvImagePreview.setOnClickListener { startActivity { } } binding.tvTitle.setOnClickListener { // startActivity { } // val goodsImage = GoodsImage(goodsId = "1993493094888140802", goodsName = "多宝鱼", startTime = 1764120531856L, endTime = 1764121842825L) // uploadImage(goodsImage) // val goodsImage = GoodsImage(goodsId = "1986366073649287170", goodsName = "黑鱼(整条鱼)", startTime = 1764122703093L, endTime = 1764123156022L) // uploadImage(goodsImage) // val goodsImage = GoodsImage(goodsId = "1986699063868801025", goodsName = "鸡腿肉", startTime = 1764123375841L, endTime = 1764124039162L) // uploadImage(goodsImage) } } private fun showUploadDialog() { val goodsName = searchGoodsList[clickIndex].goodsName WarnDialog( context = this, content = "选择错误会影响识别结果,请确认当前采集物品是:${goodsName}?", cancelBlock = { }, confirmBlock = { upload() } ) .show() } private var pageNo = 1 private var clickIndex = -1 private val searchGoodsList: MutableList = mutableListOf() private val searchAdapter by lazy { GoodsSearchAdapter(searchGoodsList).apply { isStateViewEnable = true setOnItemClickListener { adapter, view, position -> clickIndex = position if (list[position].isSelected) { return@setOnItemClickListener } list.forEach { item -> item.isSelected = false } list[position].isSelected = true notifyDataSetChanged() } } } private fun finishRefresh() { binding.refreshLayout.let { if (pageNo == 1) { it.finishRefresh(500) } else { it.finishLoadMore(500) } } } @SuppressLint("NotifyDataSetChanged") private fun takePhoto() { val count = collectList.count { it.imageFile != null } if (count == MAX_COUNT) { toast("每次只允许保存${MAX_COUNT}条数据") return } showWaitingDialog2("加载中") cameraUtils.takePhoto(cameraCallback) } @SuppressLint("NotifyDataSetChanged") private fun clearData() { val count = collectList.count { it.imageFile != null } if (count == 0) return clearCoilCache(this) collectList.forEach { try { if (it.bitmap?.isRecycled?.not() == true) { it.bitmap?.recycle() } it.bitmap = null it.imageVector = null it.imageFile = null it.isShowCamera = true it.isFinish = false it.uploadSuccess = false } catch (e: Exception) { e.printStackTrace() } } collectionAdapter.notifyDataSetChanged() clickIndex = -1 binding.etGoodsInput.setText("") searchGoodsList.clear() searchAdapter.notifyDataSetChanged() loadEmptyView() binding.ivFinish.invisible() // binding.btnUploadImage.text = "待上传图片${0}张" } private val cameraCallback: (Uri) -> Unit = { uri -> lifecycleScope.launch { handlePhoto(uri) } } private suspend fun handlePhoto(uri: Uri) { val index = collectList.indexOfFirst { it.imageFile == null } if (index == -1) { toast("每次只允许保存${MAX_COUNT}条数据") showWaitingDialog2("加载中") return } // toast("index=$index") try { ImageUtil.uriToBitmap(this, uri)?.let { bitmap -> val cropBitmap = BitmapCropper.cropCenter( original = bitmap, targetWidth = 1000, targetHeight = 1300, offsetX = 30, offsetY = 100 ) initBox() val imageVector = try { FoodModule.bitmap2FloatArray(cropBitmap, false) } catch (e: Exception) { e.printStackTrace() hideWaitingDialog() toast("操作失败") return@let } Timber.tag(TAG).d("向量:${imageVector.toJsonString()}") val foodList = try { FoodModule.queryFoodNameScore(imageVector) } catch (e: DbException) { try { ObjectBox.init(this) } catch (e: Exception) { e.printStackTrace() } toast("数据库异常,请稍后重试") return@let } val filterList = foodList.filter { it.score < FoodModule.BAG_RATE && it.name.contains("黑袋子") } if (filterList.isNotEmpty()) { hideWaitingDialog() toast("当前物品可能被袋子遮挡,请检查后重试") return@let } val file = BitmapSaver.saveToAppFilesDir( cropBitmap, this, "IMG_CROP_${System.currentTimeMillis()}.jpg" ) Timber.d("${this.javaClass.simpleName}-searchFood-裁剪bitmap保存文件路径:${file?.absolutePath}") collectList[index].let { it.imageVector = imageVector it.bitmap = null it.imageFile = file it.isShowCamera = false it.isFinish = false it.uploadSuccess = false } collectionAdapter.notifyItemChanged(index) // val count = collectList.count { it.imageFile != null && it.uploadSuccess.not() } // binding.btnUploadImage.text = "待上传图片${count}张" if (cropBitmap.isRecycled.not()) { cropBitmap.recycle() } if (bitmap.isRecycled.not()) { bitmap.recycle() } hideWaitingDialog() } } catch (e: Exception) { e.printStackTrace() hideWaitingDialog() val errMsg = e.message ?: "" if (errMsg.contains("Could not get from cursor") || e is FileCorruptException || e is DbException ) { try { ObjectBox.init(this) } catch (e: Exception) { e.printStackTrace() } toast("请重试") } else { toast("程序异常$errMsg") } } } private fun vectorThread() { Loading.show(this) lifecycleScope.launch { collectList //.filter { it.bitmap != null } // .filter { it.imageVector != null } .forEachIndexed { index, it -> //image2VectorTask(it.bitmap!!, index) it.imageVector?.let { imageVector -> image2VectorTask(imageVector = imageVector, position = index) } } runOnUiThread { window.decorView.postDelayed({ Loading.dismiss() toast("保存成功") }, 1000) } } } private suspend fun image2VectorTask( bitmap: Bitmap? = null, imageVector: FloatArray? = null, position: Int ) { try { initBox() // val imageVector = try { // FoodModule.bitmap2FloatArray(bitmap) // } catch (e: Exception) { // e.printStackTrace() // //toast("保存失败") // return // } val goods = searchGoodsList[clickIndex] val saveName = goods.goodsName + goods.goodsCode // ObjectBox.boxStore.runInTx { // box?.put(Food(name = saveName, foodIdx = 0, foodVector = imageVector)) // } ObjectBox.put(Food(name = saveName, foodIdx = 0, foodVector = imageVector)) collectList[position].let { // it.imageVector = imageVector it.isFinish = true } runOnUiThread { collectionAdapter.notifyItemChanged(position) } } catch (e: Exception) { e.printStackTrace() } } @SuppressLint("NotifyDataSetChanged") private fun searchGoods() { val searchName = binding.etGoodsInput.text.toString().trim() if (searchName.isBlank()) { toast("请输入物品名称") finishRefresh() return } searchGoods( searchName = searchName, pageNo = pageNo ) { records -> finishRefresh() binding.etGoodsInput.hideKeyboard() if (pageNo == 1 && records.isEmpty()) { searchGoodsList.clear() searchAdapter.notifyDataSetChanged() loadEmptyView() return@searchGoods } if (pageNo == 1) { searchGoodsList.clear() } searchGoodsList.addAll(records) val enableLoadMore = records.size >= PAGE_SIZE binding.refreshLayout.setEnableLoadMore(enableLoadMore) if (enableLoadMore) { pageNo++ } binding.rvSearch.layoutManager = GridLayoutManager(this, 2, GridLayoutManager.VERTICAL, false) searchAdapter.notifyDataSetChanged() } } private var emptyBinding: LayoutEmptySearchBinding? = null private fun loadEmptyView() { binding.rvSearch.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) if (emptyBinding == null) { emptyBinding = LayoutEmptySearchBinding.inflate(layoutInflater, binding.rvSearch, false) } emptyBinding?.tvContent?.text = "请检查物品名称,或稍后重新搜索" emptyBinding?.root?.let { layout -> layout.setOnClickListener { layout.hideKeyboard() } searchAdapter.stateView = layout } } fun searchGoods( searchName: String, pageNo: Int, pageSize: Int = PAGE_SIZE, callback: (List) -> Unit ) { runBlocking { val records = viewModel.searchGoodsInfoList2( goodsName = searchName, pageNo = pageNo, pageSize = pageSize ) runOnUiThread { callback(records) } } } private fun initBox() { // if (box == null) { // box = ObjectBox.boxStore.boxFor(Food::class) // } } // private fun uploadAllImage() { // Loading.show(this) // val files = collectList.filter { it.imageFile!=null }.map { it.imageFile!! } // if (files.isEmpty()) { // return // } // val goods = searchGoodsList[clickIndex] // for (index in collectList.indices step 5) { // val end = if(index + 5 < collectList.size - 1) index + 5 else collectList.size - 1 // val subList = collectList.subList(index, end) // val subFiles = subList.map { it.imageFile } // viewModel.uploadMultipleImages(goods.id!!, goods.goodsName!!, subFiles) {isSuccess-> // Timber.tag(TAG).d("uploadMultipleImages: ${isSuccess}") // subList.filter { it.imageFile!=null }.forEach { it.uploadSuccess = isSuccess } // // val count = collectList.count { it.imageFile!=null && it.uploadSuccess.not() } // runOnUiThread { // binding.btnUploadImage.text = "待上传图片${count}张" // if (count == 0) { // Loading.dismiss() // } // } // } // } // } private fun upload() { lifecycleScope.launch { val totalFileCount = collectList.count { it.imageFile != null } showWaitingDialog2("图片上传中0/$totalFileCount") val goods = searchGoodsList[clickIndex] val params = HashMap() params["goodsId"] = goods.id!!.toRequestBody() params["goodsName"] = goods.goodsName!!.toRequestBody() //params["foodVector"] = foodVector.toRequestBody() ImageUploader( totalList = collectList, uploadImage = { batch -> val files = batch.map { it.imageFile } viewModel.uploadMultipleImages(files = files, params) }, onProgress = { count, batch -> runOnUiThread { showWaitingDialog2("图片上传中$count/$totalFileCount") batch.forEach { it.uploadSuccess = true } } }, onError = { runOnUiThread { hideWaitingDialog() toast("上传失败,请稍后重试") } }, onComplete = { hideWaitingDialog() vectorThread() } ).processUploads() } } private val receiptViewModel: ReceiptViewModel by viewModels() fun searchGoodsInfoList(goodsName: String, block: (List) -> Unit) { runBlocking { val list = searchGoodsInfoList2(goodsName) block(list) } } suspend fun searchGoodsInfoList2( goodsName: String, pageNo: Int = 1, pageSize: Int = 10 ): List { return receiptViewModel.searchGoodsInfoList2(goodsName, pageNo, pageSize) } private var mDialogWaiting: CustomDialog? = null /** * 显示等待提示框 */ fun showWaitingDialog(tip: String?) { runOnUiThread { hideWaitingDialog() val view = View.inflate(this, R.layout.dialog_waiting, null) if (!TextUtils.isEmpty(tip)) (view.findViewById(R.id.tvTip) as TextView).text = tip mDialogWaiting = CustomDialog(this, view, R.style.MyDialog) mDialogWaiting!!.show() mDialogWaiting!!.setCanceledOnTouchOutside(true) mDialogWaiting!!.setCancelable(true) } } /** * 隐藏等待提示框 */ fun hideWaitingDialog() { runOnUiThread { mDialogWaiting?.dismiss() mDialogWaiting = null } } fun showWaitingDialog2(tip: String?) { runOnUiThread { if (mDialogWaiting == null) { hideWaitingDialog() val view = View.inflate(this, R.layout.dialog_waiting, null) mDialogWaiting = CustomDialog(this, view, R.style.MyDialog) mDialogWaiting!!.show() mDialogWaiting!!.setCanceledOnTouchOutside(true) mDialogWaiting!!.setCancelable(true) } val contentView = mDialogWaiting?.findViewById(android.R.id.content) val tvTip = contentView?.findViewById(R.id.tvTip) tvTip?.text = tip } } /** * 清除Coil缓存 */ @OptIn(ExperimentalCoilApi::class) fun clearCoilCache(context: Context) { val imageLoader = ImageLoader.Builder(context).build() // 清除内存缓存 imageLoader.memoryCache?.clear() // 清除磁盘缓存 imageLoader.diskCache?.clear() } data class RespData( var code: String? = null, var data: String? = null, var msg: String? = null, ) data class GoodsImage( var goodsId: String, var goodsName: String, var startTime: Long, var endTime: Long ) // // val goodsImageList = mutableListOf().apply { // add( // GoodsImage( // goodsId = "1986353555971297282", // goodsName = "马铃薯[土豆、洋芋]", // startTime = 1763692477786L, // endTime = 1763693153825L // ) // ) // add(GoodsImage( // goodsId = "1986607476769869825", // goodsName = "辣椒(青辣椒,螺丝椒)", // startTime = 1763693182929L, // endTime = 1763693621664L // )) // add(GoodsImage( // goodsId = "1987880591453859842", // goodsName = "辣椒(红,小)", // startTime = 1763693683837L, // endTime = 1763694154521L // )) // add(GoodsImage( // goodsId = "1986699063868801025", // goodsName = "鸡腿肉", // startTime = 1763694221267L, // endTime = 1763695052924L // )) // // add(GoodsImage( // goodsId = "1986689570300809217", // goodsName = "猪腿肉", // startTime = 1763695212428L, // endTime = 1763695810578L // )) // add(GoodsImage( // goodsId = "1986673470079029249", // goodsName = "西兰花[绿菜花]", // startTime = 1763695924575L, // endTime = 1763696252669L // )) // add(GoodsImage( // goodsId = "1991473021965070338", // goodsName = "姜[黄姜]", // startTime = 1763696258001L, // endTime = 1763696294831L // )) // add(GoodsImage( // goodsId = "00000", // goodsName = "其它", // startTime = 1763696442577L, // endTime = 1763696990541L // )) // } // private fun uploadImage(goodsImage: GoodsImage) { // Loading.show(this) // val cropFile = File(this.cacheDir, "crop") // val list = mutableListOf() // val startTime = goodsImage.startTime // val endTime = goodsImage.endTime // cropFile.listFiles()?.forEach { // if (it.isDirectory) { // return@forEach // } // val start = "IMG_CROP_".length // val end = it.name.indexOf(".jpg") // val time = it.name.substring(start, end).toLong() // if (time in startTime..endTime) { // list.add(it) // } // if (list.size < 5) { // return@forEach // } // viewModel.uploadMultipleImages( // goodsId = goodsImage.goodsId, // goodsName = goodsImage.goodsName, // files = list // ) { isSuccess -> //// val resp = data?.toObject() //// val isSuccess = resp?.code == "00000" // binding.ivFinish.run { // if (isSuccess) visible() else invisible() // } // } // list.clear() // } // if (list.isNotEmpty()) { // viewModel.uploadMultipleImages( // goodsId = goodsImage.goodsId, // goodsName = goodsImage.goodsName, // files = list // ) { isSuccess -> //// val resp = data?.toObject() //// val isSuccess = resp?.code == "00000" // binding.ivFinish.run { // if (isSuccess) visible() else invisible() // } // } // } // Loading.dismiss() // } }