This commit is contained in:
2026-02-03 15:00:19 +08:00
parent 6e7eb80715
commit 7596924d75
24 changed files with 1216 additions and 342 deletions
+2 -2
View File
@@ -4,10 +4,10 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-01-09T06:05:40.833191600Z">
<DropdownSelection timestamp="2026-02-02T06:44:20.183314400Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="Default" identifier="serial=192.168.1.35:5555;connection=d6972001" />
<DeviceId pluginId="Default" identifier="serial=192.168.1.32:5555;connection=c1793c60" />
</handle>
</Target>
</DropdownSelection>
@@ -20,7 +20,7 @@ object GlobalData {
/**
* 具体业务baseurl
*/
// var appBaseUrl: String = "http://192.168.1.201:14801"
var appBaseUrl2: String = "http://192.168.1.201:14801"
var appBaseUrl: String = "https://dev.yixiong-tech.com:8081"
/**
@@ -24,6 +24,7 @@ class MyApp : App() {
Timber.d("deviceId = $deviceId")
// deviceId = "3ea47dc0-3cf0-3c2f-909c-265a9a65572e"
GlobalData.deviceId = deviceId
GlobalData.appBaseUrl = if ("2987f0c5-5754-33e9-b00a-251db5e2e55f" == deviceId) GlobalData.appBaseUrl2 else GlobalData.appBaseUrl
ObjectBox.init(this)
// 初始化崩溃处理器
CrashHandler.init(this)
@@ -18,12 +18,14 @@ import com.sw.dualscreen.R
import com.sw.dualscreen.databinding.DialogWaitingBinding
import com.sw.dualscreen.model.IActivityResult
import com.sw.dualscreen.model.response.PostEvent
import com.sw.dualscreen.utils.FileUtil
import com.sw.dualscreen.view.CustomDialog
import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.plate.utils.ToastUtils
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import timber.log.Timber
abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
@@ -131,6 +133,11 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
launchPermissionCallback?.callback(result)
}
fun log(msg: String) {
Timber.d(msg)
FileUtil.saveLog(msg)
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onPostEvent(event: PostEvent) {
@@ -3,6 +3,7 @@ package com.sw.dualscreen.activity
import android.annotation.SuppressLint
import androidx.activity.viewModels
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.R
@@ -13,16 +14,12 @@ import com.sw.dualscreen.ext.addOnActionSearchListener
import com.sw.dualscreen.ext.gone
import com.sw.dualscreen.ext.hideKeyboard
import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.model.response.ResetBoxEvent
import com.sw.dualscreen.objbox.CollectedFoodInfo
import com.sw.dualscreen.objbox.Food
import com.sw.dualscreen.objbox.ObjectBox
import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel
import com.sw.plate.utils.ToastUtils
import io.objectbox.Box
import io.objectbox.kotlin.boxFor
import org.greenrobot.eventbus.EventBus
import kotlinx.coroutines.launch
class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
@@ -51,8 +48,7 @@ class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
override fun initialize() {
super.initialize()
box = ObjectBox.boxStore.boxFor(Food::class)
//binding.root.setOnClickListener { it.hideKeyboard() }
binding.root.setOnClickListener { it.hideKeyboard() }
binding.rvFoodList.let {
it.layoutManager = LinearLayoutManager(this)
it.adapter = adapter
@@ -103,9 +99,6 @@ class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
getCollectGoods()
}
private lateinit var box: Box<Food>
@SuppressLint("NotifyDataSetChanged")
private fun getCollectGoods(searchName: String? = null) {
// if (box == null) {
@@ -199,16 +192,13 @@ class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
showWaitingDialog("加载中……")
viewModel.deleteCollectFood(food.foodId, GlobalData.foodModelVersion) { deleteSuccess ->
if (deleteSuccess) {
Thread {
val filterFoodList = box.all.filter { it.foodName == food.foodName }
// Thread {}.start()
lifecycleScope.launch {
val filterFoodList = ObjectBox.filter(food.foodName)
filterFoodList.forEach { it.isDel = true }
box.put(filterFoodList)
// val filterIdList = filterFoodList.map { it.id }
////// ObjectBox.boxStore.runInTx {
//// box.removeByIds(filterIdList)
////// EventBus.getDefault().post(ResetBoxEvent())
////// }
if (filterFoodList.isNotEmpty()) {
ObjectBox.putAll(filterFoodList)
}
runOnUiThread {
hideWaitingDialog()
@@ -218,7 +208,7 @@ class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
loadEmptyView()
}
}
}.start()
}
}
}
}
@@ -2,6 +2,7 @@ package com.sw.dualscreen.activity
import android.content.Intent
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.databinding.ActivityInitBinding
import com.sw.dualscreen.model.response.FoodVector
@@ -10,13 +11,13 @@ import com.sw.dualscreen.objbox.FoodModule
import com.sw.dualscreen.objbox.ObjectBox
import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel
import io.objectbox.Box
import io.objectbox.kotlin.boxFor
import kotlinx.coroutines.launch
class InitActivity : BaseActivity<ActivityInitBinding>() {
companion object {
private const val PAGE_SIZE = 100
}
private val userViewModel by viewModels<UserViewModel>()
private var pageNum = 1
@@ -29,11 +30,13 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
}
private fun getCollectedFoodVector(block: () -> Unit) {
userViewModel.getCollectedFoodVector(param = mutableMapOf(
userViewModel.getCollectedFoodVector(
param = mutableMapOf(
"pageNum" to "$pageNum",
"pageSize" to "$PAGE_SIZE",
"version" to GlobalData.foodModelVersion
)) { items ->
)
) { items ->
val list = items ?: emptyList()
if (pageNum == 1 && list.isEmpty()) {
block()
@@ -51,10 +54,13 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
}
}
}
// private val box by lazy { ObjectBox.boxStore.boxFor(Food::class) }
private fun saveFoodVector(list: List<FoodVector>) {
lifecycleScope.launch {
val vectorList = list.map { vt ->
val foodVector = vt.foodVector?.removeSurrounding("[", "]")?.split(",")?.map { it.toFloatOrNull()?:0.0f }?.toFloatArray()
val foodVector = vt.foodVector?.removeSurrounding("[", "]")?.split(",")
?.map { it.toFloatOrNull() ?: 0.0f }?.toFloatArray()
Food(
collectId = vt.id,
foodId = vt.foodId,
@@ -63,14 +69,15 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
foodVector = foodVector
)
}
val box = ObjectBox.boxStore.boxFor(Food::class)
box.put(vectorList)
ObjectBox.putAll(vectorList)
}
}
override fun initialize() {
super.initialize()
showWaitingDialog("加载中……")
FoodModule.init(this) {
lifecycleScope.launch {
FoodModule.init(this@InitActivity) {
getCollectedFoodVector {
runOnUiThread {
hideWaitingDialog()
@@ -80,6 +87,7 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
}
}
}
}
// val isSuccess = viewModel.checkEquipmentInfo()
@@ -3,6 +3,7 @@ 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
@@ -21,11 +22,11 @@ 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.DialogSearchFoodAdapter
import com.sw.dualscreen.adapter.MainFoodListAdapter
import com.sw.dualscreen.databinding.ActivityMainBinding
import com.sw.dualscreen.dialog.RemindDialog
@@ -63,22 +64,21 @@ 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 io.objectbox.Box
import io.objectbox.kotlin.boxFor
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 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.abs
import kotlin.math.roundToInt
/**
@@ -111,7 +111,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
searchFoodList[index].isChecked = index == position
}
val item = searchFoodList[position]
Timber.d("itemClick ${item.foodName}, position = $position")
log("itemClick ${item.foodName}, position = $position")
item.photoUri = null
checkedItem = item
notifyDataSetChanged()
@@ -149,7 +149,6 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
override fun initialize() {
super.initialize()
box = ObjectBox.boxStore.boxFor(Food::class)
isPageVisible = true
addBackEventListener()
initView()
@@ -177,6 +176,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
}
updateDateTime()
binding.ivSetting.setOnClickListener {
isRefreshPage = false
startActivity(Intent(this, SettingActivity::class.java))
}
binding.previewView.outlineProvider = object : ViewOutlineProvider() {
@@ -221,8 +221,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
}
pauseAnalysis()
recognizeWeight = lastWeight
Timber.tag(TAG)
.d("registerDataChange,副屏updateFood调用前耗时:${System.currentTimeMillis() - startTime}")
log("registerDataChange,副屏updateFood调用前耗时:${System.currentTimeMillis() - startTime}")
startTime = System.currentTimeMillis()
presentation?.updateFood(foodInfo)
binding.tvFoodName.text = foodInfo.foodName
@@ -239,7 +238,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
override fun registerDataChange() {
super.registerDataChange()
SensorScaleUtils.addWeightListener { weight ->
Timber.d("registerDataChange weight = $weight")
log("registerDataChange weight = $weight")
if (isPageVisible.not() || isStartRecognize.not()) {
if (ActivityManager.currentActivity() is MainActivity) {
isPageVisible = true
@@ -247,7 +246,8 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
}
return@addWeightListener
}
val isWeightChange = weight - lastWeight > 0.05
val isWeightChange = abs(weight - lastWeight) > 0.05
log("recognizeByWeight---00001,weight=$weight,lastWeight=$lastWeight,isWeightChange=$isWeightChange")
recognizeByWeight(weight, isWeightChange)
}
}
@@ -256,16 +256,19 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
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()
}
} else {
@@ -282,8 +285,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
LightManager.closeRedLight()
LightManager.openGreenLight()
showWaitingDialog("识别中,请稍后……")
takePhoto(
successCallback = { photoUri ->
takePhoto(successCallback = { photoUri ->
Thread {
onTakePhotoSuccess(photoUri)
}.start()
@@ -291,7 +293,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
}, failureCallback = {
runOnUiThread {
if (failCount >= 10) {
//ToastUtils.showToast("相机异常,请稍后重试")
ToastUtils.showToast("相机异常,请稍后重试")
shutdownCamera()
setupCamera()
return@runOnUiThread
@@ -318,7 +320,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
private fun updateFoodInfo(list: MutableList<FoodInfo>, scoreList: List<IdNameScore>) {
val queryData = GsonUtils.toJson(list)
val recData = GsonUtils.toJson(scoreList)
Timber.d("registerDataChange识别后查询接口数据:$queryData,识别数据:$recData")
log("registerDataChange识别后查询接口数据:$queryData,识别数据:$recData")
if (list.isEmpty()) {
binding.tvToSearch.let {
it.text = "未查询到,手动搜索"
@@ -359,15 +361,13 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
}
private fun setupCamera() {
Timber.d("setupCamera")
log("setupCamera")
cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture!!.addListener({
val cameraProvider = cameraProviderFuture!!.get()
// 1. 创建预览用例
val preview = Preview.Builder()
.build()
.also {
val preview = Preview.Builder().build().also {
it.setSurfaceProvider(binding.previewView.surfaceProvider)
}
@@ -375,8 +375,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
// .setTargetResolution(Size(640, 480)) // 降低分辨率减轻负担
.build()
.also { analysis ->
.build().also { analysis ->
analysis.setAnalyzer(executor) { imageProxy ->
// 仅在步骤1时传递
if (isAnalyzing) {
@@ -389,8 +388,8 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
}
// 3. 创建拍照用例
imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
imageCapture =
ImageCapture.Builder().setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
// .setTargetResolution(Size(1920, 1080)) // 设置拍照分辨率
.build()
@@ -405,20 +404,20 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageAnalysis, imageCapture
)
} catch (exc: Exception) {
Timber.e(exc.message)
} catch (e: Exception) {
log("setupCamera方法异常:${e.message}")
}
}, ContextCompat.getMainExecutor(this))
}
fun pauseAnalysis() {
Timber.d("pauseAnalysis")
log("pauseAnalysis")
isAnalyzing = false
imageAnalysis?.clearAnalyzer()
}
fun resumeAnalysis() {
Timber.d("resumeAnalysis isAnalyzing = $isAnalyzing")
log("resumeAnalysis isAnalyzing = $isAnalyzing")
if (isAnalyzing) return
// isAnalyzing = true
imageAnalysis?.setAnalyzer(executor) { imageProxy ->
@@ -434,7 +433,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
* 停止相机识别
*/
private fun shutdownCamera() {
Timber.d("shutdownCamera")
log("shutdownCamera")
try {
// 1. 首先停止图像分析
imageAnalysis?.clearAnalyzer()
@@ -449,13 +448,13 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
// 4. 停止副屏更新
isAnalyzing = false
} catch (e: Exception) {
Timber.e(e, "Error shutting down camera")
log("Error shutting down camera:${e.message}")
}
}
// 拍照功能
private fun takePhoto(successCallback: (Uri) -> Unit, failureCallback: () -> Unit) {
Timber.d("takePhoto")
log("takePhoto")
// 临时停止分析以避免干扰
isAnalyzing = false
@@ -463,21 +462,16 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
val executor = ContextCompat.getMainExecutor(this)
val cacheDir = cacheDir
val photoFile = File.createTempFile(
"IMG_${System.currentTimeMillis()}",
".jpg",
cacheDir
"IMG_${System.currentTimeMillis()}", ".jpg", cacheDir
)
val cacheOutputOptions =
ImageCapture.OutputFileOptions.Builder(photoFile).build()
val cacheOutputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
// 执行拍照
imageCapture?.takePicture(
cacheOutputOptions,
executor,
object : ImageCapture.OnImageSavedCallback {
cacheOutputOptions, executor, object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
val savedUri = outputFileResults.savedUri
Timber.d("takePhoto savedUri = $savedUri")
log("takePhoto savedUri = $savedUri")
if (savedUri != null) {
successCallback(savedUri)
}
@@ -486,14 +480,13 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
}
override fun onError(exception: ImageCaptureException) {
Timber.e("拍照失败: ${exception.message}")
log("takePhoto拍照失败: ${exception.message}")
failureCallback()
//ToastUtils.showToast("拍照失败: ${exception.message}")
// 恢复图像分析
isAnalyzing = true
}
}
)
})
}
private fun getSecondaryDisplay(): Display? {
@@ -522,7 +515,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
).apply {
setStepChangeCallback { step ->
if (step == 1) {
Timber.d("setStepChangeCallback${step}")
log("setStepChangeCallback${step}")
updateCurrentFood(null)
bottomSheetDialog?.dismiss()
}
@@ -560,12 +553,16 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
} else {
//副屏未显示重新加载
isRecognitionFood = true
log("onResume-00001")
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
@@ -576,7 +573,6 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
recognizeByWeight(weight, true)
}, 500)
}
isRefreshPage = !isRefreshPage
}
}
@@ -627,7 +623,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
notEnoughOneBlock: () -> Unit = {},
block: () -> Unit
) {
Timber.tag(TAG).d("foodWeight=$foodWeight,eatWeight=$eatWeight")
log("foodWeight=$foodWeight,eatWeight=$eatWeight")
val pickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
// if (pickupMode == 0) {
// if (foodWeight <= 5) {
@@ -735,10 +731,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
private var isRefreshPage = false
@Subscribe(threadMode = ThreadMode.MAIN)
fun onClickBackEvent(event: ClickBackEvent) {
isRefreshPage = false
}
// @Subscribe(threadMode = ThreadMode.MAIN)
// fun onClickBackEvent(event: ClickBackEvent) {
// isRefreshPage = false
// }
private fun clickPayButton() {
if (checkedItem == null) {
@@ -761,7 +757,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
//余量计量
eatWeight = (recognizeWeight - lastWeight) * 1000
foodWeight = recognizeWeight * 1000
Timber.d("eatWeight=$eatWeight, recognizeWeight=$recognizeWeight, lastWeight=$lastWeight")
log("eatWeight=$eatWeight, recognizeWeight=$recognizeWeight, lastWeight=$lastWeight")
}
} else {
//不计费
@@ -782,7 +778,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
eatNum = eatNum,
userId = null,
) {
// TODO: 暂不关闭首页副屏
isRefreshPage = false
//presentation?.dismiss()
startActivity(Intent(this, PayActivity::class.java).apply {
putExtra(PayActivity.FOOD_INFO, checkedItem!!)
@@ -814,13 +810,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
* 上传菜品信息
*/
private suspend fun uploadCollectFoodPics(
foodInfo: FoodInfo,
imageFile: File?,
imageVector: FloatArray?
foodInfo: FoodInfo, imageFile: File?, imageVector: FloatArray?
) {
if (imageFile == null || imageVector == null) {
Timber.tag(TAG)
.d("是否null判断,imageFile == null:${imageFile == null},imageVector == null:${imageVector == null}")
log("是否null判断,imageFile == null:${imageFile == null},imageVector == null:${imageVector == null}")
return
}
val foodId = foodInfo.foodId
@@ -834,30 +827,28 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
val vectors = listOf(imageVector)
val foodVectorList = vectors.map {
it.joinToString(
separator = ",",
prefix = "[",
postfix = "]"
separator = ",", prefix = "[", postfix = "]"
)
}
val foodVectorJson =
foodVectorList.joinToString(separator = ",", prefix = "[", postfix = "]")
Timber.tag(TAG).d("json=$foodVectorJson")
log("json=$foodVectorJson")
params["foodVector"] = foodVectorJson.toRequestBody()
val idList = viewModel.uploadCollectFoodPics(files, params)
if (idList.isNullOrEmpty()) {
loadRemindDialog("上传失败,未返回向量数据id")
Timber.tag(TAG).d("idList为空")
log("idList为空")
return
}
Timber.tag(TAG)
.d("uploadCollectFoodPics-已采集向量总数:${box.all.filter { it.isDel.not() }.size}条")
val filterList = box.all.filter { it.collectId == idList[0] }
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
}
Thread {
box.put(
ObjectBox.put(
Food(
collectId = idList[0],
foodId = foodId,
@@ -866,21 +857,15 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
version = foodModelVersion
)
)
}.start()
}
private fun loadRemindDialog(msg: String) {
runOnUiThread {
RemindDialog(
context = context,
content = msg,
confirmBlock = {}
).show()
context = context, content = msg, confirmBlock = {}).show()
}
}
private lateinit var box: Box<Food>
private fun uri2File(uri: Uri, block: (File?, FloatArray?) -> Unit) {
ImageUtil.uriToBitmap(this, uri)?.let { bitmap ->
val imageVector = try {
@@ -892,7 +877,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
val imageFile = BitmapSaver.saveToAppFilesDir(
bitmap, this, "IMG_CROP_${System.currentTimeMillis()}.jpg"
)
Timber.d("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${imageFile?.absolutePath}")
log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${imageFile?.absolutePath}")
block(imageFile, imageVector)
if (bitmap.isRecycled.not()) {
bitmap.recycle()
@@ -913,8 +898,8 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
// private val initialDelay = 5 * 60 * 1000L
// private val dealyMillis = 10 * 60 * 1000L
private val initialDelay = 60 * 1000L
private val dealyMillis = 30 * 1000L
private val initialDelay = 30 * 1000L
private val dealyMillis = 60 * 1000L
private var taskPageNo = 1
fun startFaceTask() {
faceTaskJob =
@@ -936,8 +921,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
}
updateFaceData(list)
}
}
)
})
}
}
@@ -952,9 +936,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
} else {
//保存数据
val faceEntity = FaceEntity(
model.userId,
null,
Base64.decode(model.faceFeatureStr)
model.userId, null, Base64.decode(model.faceFeatureStr)
).also {
it.userType = "1"
}
@@ -988,34 +970,42 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
private fun onTakePhotoSuccess(photoUri: Uri) {
lastPhotoUri = photoUri
Timber.tag(TAG).d("main,takePhoto耗时:${System.currentTimeMillis() - startTime}")
log("registerDataChange main,takePhoto耗时:${System.currentTimeMillis() - startTime}")
startTime = System.currentTimeMillis()
Timber.d("registerDataChange photoUri = ${photoUri.path}")
log("registerDataChange photoUri = ${photoUri.path}")
// viewModel.getIdentifiedFoodList()
ImageUtil.uriToBitmap(this, photoUri)?.let { bitmap ->
Timber.tag(TAG).d("main,uriToBitmap耗时:${System.currentTimeMillis() - startTime}")
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()
Timber.d("registerDataChange photoUri 拿到bitmap")
log("registerDataChange photoUri 拿到bitmap")
// val bmp = BitmapCropper.cropCenter(bitmap, 1300, 900)
Timber.d("registerDataChange photoUri bitmap裁剪完成")
log("registerDataChange registerDataChange photoUri bitmap裁剪完成")
val file = BitmapSaver.saveToAppFilesDir(
bitmap, this, "IMG_CROP_${System.currentTimeMillis()}.jpg"
)
Timber.tag(TAG)
.d("main,saveToAppFilesDir耗时:${System.currentTimeMillis() - startTime}")
log("registerDataChange main,saveToAppFilesDir耗时:${System.currentTimeMillis() - startTime}")
startTime = System.currentTimeMillis()
Timber.d("registerDataChange photoUri bitmap保存文件路径:${file?.absolutePath}")
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)
Timber.tag(TAG).d("main,getFoodScoreList耗时:${System.currentTimeMillis() - startTime}")
log("registerDataChange main,getFoodScoreList耗时:${System.currentTimeMillis() - startTime}")
startTime = System.currentTimeMillis()
Timber.d("registerDataChange photoUri 拿到识别数据:$recDataJson")
log("registerDataChange photoUri 拿到识别数据:$recDataJson")
val nameList = scoreList.map { it.name }
val foodName = nameList.joinToString(separator = ",")
nameList.joinToString(separator = ",")
} else ""
hideWaitingDialog()
Timber.d("registerDataChange photoUri 识别数据名称:$foodName")
log("registerDataChange photoUri 识别数据名称:$foodName")
runOnUiThread {
binding.flPay.run {
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
@@ -1040,15 +1030,12 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
viewModel.getFoodInfo(foodName) { list ->
runOnUiThread {
hideWaitingDialog()
Timber.tag(TAG)
.d("main,getFoodInfo耗时:${System.currentTimeMillis() - startTime}")
log("registerDataChange main,getFoodInfo耗时:${System.currentTimeMillis() - startTime}")
startTime = System.currentTimeMillis()
updateFoodInfo(list.toMutableList(), scoreList)
}
}
}
}
}
// @Subscribe(threadMode = ThreadMode.MAIN)
@@ -34,9 +34,9 @@ class SettingActivity : BaseActivity<ActivitySettingBinding>() {
initBackDispatcher()
binding.include.let {
it.ivPageBack.setOnClickListener {
// val intent = Intent(this, MainActivity::class.java)
// startActivity(intent)
EventBus.getDefault().post(ClickBackEvent())
//// val intent = Intent(this, MainActivity::class.java)
//// startActivity(intent)
// EventBus.getDefault().post(ClickBackEvent())
finish()
}
it.tvPageTitle.text = "设置"
@@ -73,13 +73,13 @@ class SettingActivity : BaseActivity<ActivitySettingBinding>() {
private lateinit var backPressedCallback: OnBackPressedCallback
private fun initBackDispatcher() {
// 创建回调,true表示初始启用状态
backPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
EventBus.getDefault().post(ClickBackEvent())
}
}
// 注册回调,使用lifecycleOwner确保生命周期安全
onBackPressedDispatcher.addCallback(this, backPressedCallback)
// backPressedCallback = object : OnBackPressedCallback(true) {
// override fun handleOnBackPressed() {
//// EventBus.getDefault().post(ClickBackEvent())
// }
// }
// // 注册回调,使用lifecycleOwner确保生命周期安全
// onBackPressedDispatcher.addCallback(this, backPressedCallback)
}
@@ -19,7 +19,6 @@ import com.sw.dualscreen.activity.CollectedFoodActivity
import com.sw.dualscreen.activity.SettingActivity
import com.sw.dualscreen.adapter.CollectFoodListAdapter
import com.sw.dualscreen.adapter.FoodCollectionAdapter
import com.sw.dualscreen.adapter.DialogSearchFoodAdapter
import com.sw.dualscreen.databinding.FragmentCollectBinding
import com.sw.dualscreen.databinding.LayoutCameraPreviewBinding
import com.sw.dualscreen.ext.clickWithDebounce
@@ -35,14 +34,11 @@ import com.sw.dualscreen.utils.Debouncer
import com.sw.dualscreen.utils.ImageUploader
import com.sw.dualscreen.utils.ImageUtil
import com.sw.plate.utils.ToastUtils
import io.objectbox.Box
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 kotlin.getValue
@SuppressLint("NotifyDataSetChanged")
class CollectFragment : BaseFragment<FragmentCollectBinding>() {
@@ -55,13 +51,6 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
private var selectedFoodId: String? = ""
private var selectedFoodName: String? = ""
private var box: Box<Food>? = null
fun initBox() {
if (box == null) {
box = ObjectBox.boxStore.boxFor(Food::class)
}
}
private val foodCollectionList = mutableListOf<FoodCollectionBean>().apply {
repeat(MAX_COUNT) {
add(FoodCollectionBean(isShowCamera = true))
@@ -138,6 +127,7 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
e.printStackTrace()
settingActivity?.hideWaitingDialog()
ToastUtils.showToast("程序异常${e.message}")
settingActivity?.log("程序异常${e.message}")
}
}
@@ -152,13 +142,14 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
} catch (e: Exception) {
e.printStackTrace()
ToastUtils.showToast("操作失败")
settingActivity?.log("操作失败:${e.message}")
settingActivity?.hideWaitingDialog()
return
}
val file = BitmapSaver.saveToAppFilesDir(
bitmap, requireActivity(), "IMG_CROP_${System.currentTimeMillis()}.jpg"
)
Timber.d("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}")
settingActivity?.log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}")
activity?.runOnUiThread {
foodCollectionList[index].let {
@@ -187,6 +178,7 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
settingActivity?.hideWaitingDialog()
if (cameraErrorCount >= 10) {
ToastUtils.showToast("拍照异常,请重新操作")
settingActivity?.log("拍照异常,请重新操作:$errMsg")
cameraErrorCount = 0
return@takePhoto
}
@@ -305,8 +297,7 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
version = GlobalData.foodModelVersion
)
}
initBox()
box?.put(foodList)
ObjectBox.putAll(foodList)
activity?.runOnUiThread {
batch.forEach { it.isFinish = true }
collectionAdapter.notifyDataSetChanged()
@@ -54,27 +54,29 @@ class CashPayFragment : BaseFragment<FragmentCashPayBinding>() {
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
if (hidden) {
delayDismiss()
dismissSubScreen()
return
}
//showSubScreen()
}
override fun onDestroy() {
delayDismiss()
dismissSubScreen()
super.onDestroy()
}
private fun delayDismiss() {
presentation?.dismiss()
//binding.root.postDelayed({
// presentation?.dismiss()
//}, 100)
private fun dismissSubScreen() {
presentation?.let {
if (it.isShowing) {
it.dismiss()
}
}
}
private fun showSubScreen() {
// 查找副屏(通常索引为1
if (displays.size > 1) {
dismissSubScreen()
presentation = CashPayPresentation(activity = payActivity, display = displays[1]) {
presentation?.dismiss()
}.also {
@@ -32,22 +32,23 @@ class FacePayFragment : BaseFragment<FragmentFacePayBinding>() {
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
if (hidden) {
delayDismiss()
dismissSubScreen()
return
}
//showSubScreen()
}
override fun onDestroy() {
delayDismiss()
dismissSubScreen()
super.onDestroy()
}
private fun delayDismiss() {
presentation?.dismiss()
//binding.root.postDelayed({
// presentation?.dismiss()
//}, 100)
private fun dismissSubScreen() {
presentation?.let {
if (it.isShowing) {
it.dismiss()
}
}
}
fun loadBitmap(frame: Bitmap) {
@@ -57,6 +58,7 @@ class FacePayFragment : BaseFragment<FragmentFacePayBinding>() {
private fun showSubScreen() {
// 查找副屏(通常索引为1
if (displays.size > 1) {
dismissSubScreen()
presentation = FacePayPresentation(
activity = payActivity,
display = displays[1],
@@ -1,14 +1,11 @@
package com.sw.dualscreen.activity.fragment.pay
import com.sw.dualscreen.GlobalKey
import com.sw.dualscreen.activity.PayActivity
import com.sw.dualscreen.activity.fragment.BaseFragment
import com.sw.dualscreen.databinding.FragmentNumberPayBinding
import com.sw.dualscreen.ext.format2String
import com.sw.dualscreen.ext.hideKeyboard
import com.sw.dualscreen.model.response.MemberInfo
import com.sw.dualscreen.presentation.pay.CashPayPresentation
import com.sw.dualscreen.utils.SPUtil
import com.sw.plate.utils.ToastUtils
class NumberPayFragment : BaseFragment<FragmentNumberPayBinding>() {
@@ -85,27 +82,29 @@ class NumberPayFragment : BaseFragment<FragmentNumberPayBinding>() {
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
if (hidden) {
delayDismiss()
dismissSubScreen()
return
}
//showSubScreen()
}
override fun onDestroy() {
delayDismiss()
dismissSubScreen()
super.onDestroy()
}
private fun delayDismiss() {
presentation?.dismiss()
//binding.root.postDelayed({
// presentation?.dismiss()
//}, 100)
private fun dismissSubScreen() {
presentation?.let {
if (it.isShowing) {
it.dismiss()
}
}
}
private fun showSubScreen() {
// 查找副屏(通常索引为1
if (displays.size > 1) {
dismissSubScreen()
presentation = CashPayPresentation(activity = payActivity, display = displays[1]) {
presentation?.dismiss()
}.also {
@@ -283,7 +283,7 @@ class PayResultFragment : BaseFragment<FragmentPayResultBinding>() {
private val intervalExecutor by lazy { IntervalExecutor() }
private var payTaskJob: Job? = null
fun paySuccessCallback(callback: () -> Unit) {
payTaskJob = intervalExecutor.startIntervalTaskWithInitialDelay(5000, 10000) {
payTaskJob = intervalExecutor.startIntervalTaskWithInitialDelay(200, 500) {
payActivity?.queryOrderState { paySuccess ->
if (paySuccess) {
callback()
@@ -5,7 +5,6 @@ import android.text.SpannedString
import android.text.style.AbsoluteSizeSpan
import androidx.core.text.buildSpannedString
import androidx.lifecycle.lifecycleScope
import com.sw.dualscreen.R
import com.sw.dualscreen.activity.PayActivity
import com.sw.dualscreen.activity.fragment.BaseFragment
import com.sw.dualscreen.ext.load
@@ -62,29 +61,31 @@ class ScanQrCodePayFragment : BaseFragment<FragmentScanQrcodePayBinding>() {
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
if (hidden) {
delayDismiss()
dismissSubScreen()
return
}
//showSubScreen()
}
override fun onDestroy() {
delayDismiss()
dismissSubScreen()
countDownJob?.cancel() // 自动取消订阅,防止内存泄漏
payTaskJob?.cancel()
super.onDestroy()
}
private fun delayDismiss() {
presentation?.dismiss()
//binding.root.postDelayed({
// presentation?.dismiss()
//}, 100)
private fun dismissSubScreen() {
presentation?.let {
if (it.isShowing) {
it.dismiss()
}
}
}
private fun showSubScreen() {
// 查找副屏(通常索引为1
if (displays.size > 1) {
dismissSubScreen()
presentation =
ScanQrCodePayPresentation(activity = payActivity, type = 0, display = displays[1]) {
presentation?.dismiss()
@@ -123,7 +124,7 @@ class ScanQrCodePayFragment : BaseFragment<FragmentScanQrcodePayBinding>() {
private val intervalExecutor by lazy { IntervalExecutor() }
private var payTaskJob: Job? = null
fun paySuccessCallback(isVip: Boolean) {
payTaskJob = intervalExecutor.startIntervalTaskWithInitialDelay(5000, 10000) {
payTaskJob = intervalExecutor.startIntervalTaskWithInitialDelay(200, 500) {
payActivity.queryOrderState { paySuccess ->
if (paySuccess) {
payActivity.showPaySuccess(isVip)
@@ -11,6 +11,8 @@ import com.sw.dualscreen.utils.ImageUtil
import com.sw.plate.utils.ToastUtils
import io.objectbox.Box
import io.objectbox.kotlin.boxFor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.pytorch.IValue
import org.pytorch.Module
import org.pytorch.torchvision.TensorImageUtils
@@ -26,7 +28,6 @@ object FoodModule {
// private const val THRESHOLD = 0.0
private var module: Module? = null
private var box: Box<Food>?=null
// private lateinit var embeddingsList: List<List<Float>>
// private lateinit var labelsList: IntArray
@@ -40,25 +41,21 @@ object FoodModule {
private const val MODEL_INPUT_HEIGHT = 224
@SuppressLint("SuspiciousIndentation")
fun init(context: Context, block: () -> Unit = {}) {
Thread {
suspend fun init(context: Context, block: () -> Unit = {}) {
// Thread {}.start()
withContext(Dispatchers.IO) {
val modelPath = copyAssetToCache(context, "best_embedding_model_mobile.pt")
// ToastUtils.showToast("modelPath==null:${modelPath == null}")
module = Module.load(modelPath)
// ToastUtils.showToast("module==null:${module == null}")
box = ObjectBox.boxStore.boxFor(Food::class)
//初始化默认重新拉取数据,先清空本地数据
// ObjectBox.boxStore.runInTx {
box?.run {
if (all.isNotEmpty()) {
removeAll()
}
val list = ObjectBox.getAll()
if (list.isNotEmpty()) {
ObjectBox.removeAll()
}
//if (box.all.isEmpty()) {
// initDefFoodData(context)
//}
block()
}.start()
}
}
// fun uri2FloatArray(uri: Uri): FloatArray? {
@@ -125,24 +122,21 @@ object FoodModule {
// return queryFoodNameScore(floatArray, queryCount)
// }
fun queryFoodNameScore(floatArray: FloatArray?, queryCount: Int = 15): List<IdNameScore> {
suspend fun queryFoodNameScore(
floatArray: FloatArray?,
queryCount: Int = 15
): List<IdNameScore> {
if (floatArray == null) return emptyList()
val startTime = System.currentTimeMillis()
Timber.tag(TAG)
.d("queryFoodNameScore-已采集向量总数:${box?.all?.filter { it.isDel.not() }?.size}")
//box.store.runInTx { }
val query = box?.query()
?.equal(Food_.isDel, false)
?.and()
?.nearestNeighbors(Food_.foodVector, floatArray, queryCount)
?.build()
val size = ObjectBox.getAll().filter { it.isDel.not() }.size
Timber.tag(TAG).d("queryFoodNameScore-已采集向量总数:${size}")
//查询比较分数
// val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" }
// val idScoreList = query.findIdsWithScores()
val objScoreList = query?.findWithScores()
val objScoreList = ObjectBox.query(floatArray, queryCount)
Timber.tag(TAG).d("idScoreList:${GsonUtils.toJson(objScoreList)}")
val nameScoreList = mutableListOf<IdNameScore>()
objScoreList?.forEach {
objScoreList.forEach {
val food = it.get()
nameScoreList.add(
IdNameScore(
@@ -153,13 +147,11 @@ object FoodModule {
)
}
val nameScoreData = GsonUtils.toJson(nameScoreList)
Timber.tag(TAG)
.d("queryFood,耗时:${System.currentTimeMillis() - startTime},数据:$nameScoreData")
query?.close()
Timber.tag(TAG).d("queryFood,耗时:${System.currentTimeMillis() - startTime},数据:$nameScoreData")
return nameScoreList
}
fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = 15): List<IdNameScore> {
suspend fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = 15): List<IdNameScore> {
val startTime = System.currentTimeMillis()
val floatArray = bitmap2FloatArray(bitmap, false)
Timber.tag(TAG).d("bitmap2FloatArray,耗时:${System.currentTimeMillis() - startTime}")
@@ -17,7 +17,9 @@
package com.sw.dualscreen.objbox
import android.content.Context
import android.os.Environment
import android.util.Log
import com.sw.plate.App
import io.objectbox.Box
import io.objectbox.BoxStore
import io.objectbox.BoxStoreBuilder
@@ -26,12 +28,22 @@ import io.objectbox.android.ObjectBoxLiveData
import io.objectbox.config.DebugFlags
import io.objectbox.exception.DbException
import io.objectbox.exception.FileCorruptException
import io.objectbox.kotlin.boxFor
import io.objectbox.query.Query
import io.objectbox.sync.Sync
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.util.Date
import java.util.zip.GZIPOutputStream
import kotlin.also
import kotlin.and
import kotlin.io.copyTo
import kotlin.io.inputStream
import kotlin.io.outputStream
@@ -52,8 +64,7 @@ object ObjectBox {
private const val TAG = "ObjectBox"
lateinit var boxStore: BoxStore
private set
var boxStore: BoxStore? = null
/**
* If building the [boxStore] failed, contains the thrown error message.
@@ -114,42 +125,166 @@ object ObjectBox {
return true
}
// /**
// * 安全获取 Food 实体 foodVector 字段的向量索引(推荐写法)
// */
// fun getFoodVectorIndex(box: Box<Food>): VectorIndex? {
// return try {
// // 核心修复:使用自动生成的 Food_.foodVector 而非字符串,避免拼写错误
// val foodVectorProperty = Food_.foodVector
// // 获取向量索引(适配 ObjectBox 3.5+ 所有版本)
// foodVectorProperty.vectorIndex
// } catch (e: Exception) {
// // 容错处理:打印错误信息,避免崩溃
// e.printStackTrace()
// null
// }
// }
//
// // 刷新索引的调用示例(结合你之前的更新场景)
// fun refreshFoodVectorIndex(box: Box<Food>) {
// val vectorIndex = getFoodVectorIndex(box)
// vectorIndex?.run {
// // 同步刷新索引(解决查询异常问题)
// refresh()
// // 等待索引构建完成(超时 5 秒,避免无限等待)
// waitUntilBuilt(5000)
// }
// }
//
// // 更新 isDel 字段后的完整调用流程
// fun markFoodAsDeleted(box: Box<Food>, foodId: Long) {
// val food = box[foodId]
// if (food != null) {
// food.isDel = true
// box.put(food)
// // 更新后刷新索引
// refreshFoodVectorIndex(box)
// }
// }
// 获取指定实体的Box,自动关联BoxStore
inline fun <reified T> getBox(): Box<T>? = boxStore?.boxFor()
// 协程安全执行数据库操作(推荐所有操作使用此方法)
suspend fun <T> safeDbOp(operation: suspend () -> T): T? {
return withContext(Dispatchers.IO) {
try {
dbMutex.withLock { operation() }
} catch (e: FileCorruptException) {
// 操作中触发损坏,尝试重建数据库
//val context = boxStore.context
//deleteDbFiles(context)
init(App.getContext()!!)
null
} catch (e: Exception) {
e.printStackTrace()
null
}
}
}
suspend fun query(floatArray: FloatArray, queryCount: Int) = safeDbOp {
val query: Query<Food>? = getBox<Food>()?.query()
?.equal(Food_.isDel, false)
?.and()
?.nearestNeighbors(Food_.foodVector, floatArray, queryCount)
?.build()
try {
query?.findWithScores()
} finally {
// 先关闭Query,释放Cursor
query?.close()
}
} ?: emptyList()
suspend fun get(id: Long) = safeDbOp {
getBox<Food>()?.get(id)
}
suspend fun put(entity: Food) = safeDbOp {
getBox<Food>()?.put(entity)
}
suspend fun putAll(entities: List<Food>) = safeDbOp {
getBox<Food>()?.put(entities)
}
suspend fun getAll() = safeDbOp {
getBox<Food>()?.all
} ?: emptyList()
suspend fun filter(name: String?, isDistinct: Boolean = false) = safeDbOp {
val list = if (isDistinct) {
getBox<Food>()?.all?.distinctBy { it.foodName }
} else {
getBox<Food>()?.all
}
if (name.isNullOrBlank().not()) {
list?.filter { it.foodName?.contains(name) == true }
}
list
} ?: emptyList()
suspend fun removeAll() = safeDbOp {
getBox<Food>()?.removeAll()
}
suspend fun remove(name: String) = safeDbOp {
getBox<Food>()?.run {
val filterIdList = all.filter { it.foodName == name }.map { it.id }
removeByIds(filterIdList)
}
}
private val dbMutex = Mutex() // 协程并发锁,保证写入操作原子性
private const val DB_DIR_NAME = "objectbox" // ObjectBox 默认数据库目录
private const val BACKUP_DIR_NAME = "objectbox_backup" // 备份目录
// 检查存储是否可读写(操作数据库前调用)
fun isStorageAvailable(context: Context): Boolean {
return try {
val state = Environment.getExternalStorageState()
val innerDir = context.filesDir
Environment.MEDIA_MOUNTED == state && innerDir.canRead() && innerDir.canWrite()
} catch (e: Exception) {
false
}
}
// 备份数据库到应用私有目录(无权限要求,推荐)
suspend fun backupDb(context: Context): Boolean = withContext(Dispatchers.IO) {
if (!isStorageAvailable(context)) return@withContext false
val dbDir = File(context.filesDir, DB_DIR_NAME)
val backupDir = File(context.filesDir, BACKUP_DIR_NAME)
return@withContext copyDir(dbDir, backupDir)
}
// 从备份恢复数据库(恢复后会重建BoxStore)
suspend fun restoreDb(context: Context): Boolean = withContext(Dispatchers.IO) {
if (!isStorageAvailable(context)) return@withContext false
val dbDir = File(context.filesDir, DB_DIR_NAME)
val backupDir = File(context.filesDir, BACKUP_DIR_NAME)
if (!backupDir.exists()) return@withContext false
// 先关闭旧的BoxStore,删除损坏文件,再恢复备份
boxStore?.close()
deleteDbFiles(context)
val isSuccess = copyDir(backupDir, dbDir)
// 重新初始化
init(context)
return@withContext isSuccess
}
// 递归删除数据库文件
private fun deleteDbFiles(context: Context) {
val dbDir = File(context.filesDir, DB_DIR_NAME)
if (dbDir.exists()) deleteDirRecursively(dbDir)
}
// 递归删除目录
private fun deleteDirRecursively(file: File) {
if (file.isDirectory) {
file.listFiles()?.forEach { deleteDirRecursively(it) }
}
file.delete()
}
// 递归复制目录(核心备份/恢复逻辑)
private fun copyDir(srcDir: File, destDir: File): Boolean {
return try {
if (!srcDir.exists()) return false
if (!destDir.exists()) destDir.mkdirs()
srcDir.listFiles()?.forEach { srcFile ->
val destFile = File(destDir, srcFile.name)
if (srcFile.isDirectory) {
copyDir(srcFile, destFile)
} else {
copyFile(srcFile, destFile)
}
}
true
} catch (e: IOException) {
e.printStackTrace()
false
}
}
// 复制单个文件(使用NIO,高效稳定)
private fun copyFile(srcFile: File, destFile: File) {
FileInputStream(srcFile).channel.use { srcChannel ->
FileOutputStream(destFile).channel.use { destChannel ->
destChannel.transferFrom(srcChannel, 0, srcChannel.size())
}
}
}
// 关闭BoxStore(应用退出时调用,可选)
fun close() {
boxStore?.close()
boxStore = null
}
}
@@ -133,7 +133,7 @@ class RemoteRepository constructor(
suspend fun getRestInfoFoodsByType(
foodName: String,
pageNum: Int = 1,
pageSize:Int = 100
pageSize:Int = 50
): ApiResponse<List<FoodInfo>> {
return safeApiCall {
apiService.getRestInfoFoodsByType(param = hashMapOf(
@@ -0,0 +1,697 @@
package com.sw.dualscreen.utils;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import com.sw.plate.App;
import com.sw.plate.utils.AppUtil;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringReader;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
public class FileUtil {
private static boolean isSaveLog = true;
public static final String FILE_STR_PATH = App.getContext().getExternalCacheDir().getAbsolutePath();
public static final int SIZETYPE_B = 1;//获取文件大小单位为B的double值
public static final int SIZETYPE_KB = 2;//获取文件大小单位为KB的double值
public static final int SIZETYPE_MB = 3;//获取文件大小单位为MB的double值
public static final int SIZETYPE_GB = 4;//获取文件大小单位为GB的double值
private static String urlNull = "原文件路径不存在";
private static String isFile = "原文件不是文件";
private static String canRead = "原文件不能读";
private static String copyFalse = "备份失败!";
private static String cFromFile = "创建原文件出错:";
private static String ctoFile = "创建备份文件出错:";
/**
* 写入文件
* FileUtil.byteWriteFile(getExternalFilesDir("123"),"test.jpg",nv21);
*
* @param filePath
* @param fileName
* @param bytes
*/
public static void byteWriteFile(File filePath, String fileName, byte[] bytes) {
BufferedOutputStream bout = null;
try {
File file = new File(filePath, fileName);
if (file.exists() == false) {
if (file.getParentFile().exists() == false) {
file.getParentFile().mkdirs();
}
file.createNewFile();
}
bout = new BufferedOutputStream(new FileOutputStream(file, false));
bout.write(bytes);
bout.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (bout != null) {
try {
bout.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 获取文件名
*
* @param path 路径
* @return 文件名
*/
public static String getFileName(String path) {
int index = path.lastIndexOf("/");
return path.substring(index + 1);
}
/**
* 根据文件路径获取文件
*
* @param path 路径
* @return
*/
public static File getFileByPath(String path) {
File file = new File(path);
if (file.exists()) {
return new File(path);
} else {
return null;
}
}
/**
* 重命名文件
*
* @param filePath 文件路径
* @param newName 新名称
* @return {@code true}: 重命名成功<br>{@code false}: 重命名失败
*/
public static boolean rename(String filePath, String newName) {
return rename(getFileByPath(filePath), newName);
}
/**
* 重命名文件
*
* @param file 文件
* @param newName 新名称
* @return {@code true}: 重命名成功<br>{@code false}: 重命名失败
*/
public static boolean rename(File file, String newName) {
// 文件为空返回false
if (file == null) return false;
// 文件不存在返回false
if (!file.exists()) return false;
// 新的文件名为空返回false
if (AppUtil.isEmpty(newName)) return false;
// 如果文件名没有改变返回true
if (newName.equals(file.getName())) return true;
File newFile = new File(file.getParent() + File.separator + newName);
// 如果重命名的文件已存在返回false
return !newFile.exists()
&& file.renameTo(newFile);
}
/**
* 判断文件是否存在
*
* @param path 文件的路径,含文件后缀和文件名
* @return 是否存在
*/
public static boolean isFileExists(String path) {
File file = new File(path);
if (file.exists()) {
return true;
} else {
return false;
}
}
/**
* 判断文件是否存在
*
* @return 是否存在
*/
public static boolean isFileExists(File file) {
if (file.exists()) {
return true;
} else {
return false;
}
}
/**
* 删除文件夹及文件夹下所有内容
*
* @param path 文件夹路径
* @return 返回是否删除成功
*/
public static boolean deleteFiles(String path) {
if (getFileByPath(path) != null) {
return deleteFiles(getFileByPath(path));
} else {
return false;
}
}
/**
* 删除文件夹及文件夹下所有内容
*
* @param file 文件
* @return 返回是否删除成功
*/
public static boolean deleteFiles(File file) {
try {
if (file.exists()) { // 判断文件是否存在
if (file.isFile()) { // 判断是否是文件
file.delete(); // delete()方法
} else if (file.isDirectory()) { // 否则如果它是一个目录
File files[] = file.listFiles(); // 声明目录下所有的文件 files[];
for (int i = 0; i < files.length; i++) { // 遍历目录下所有的文件
deleteFiles(files[i].getPath()); // 把每个文件 用这个方法进行迭代
}
file.delete();//删除目录
}
//file.delete();
return true;
} else {
return false;
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 获取文件的Uri
*
* @param path 文件的路径
* @return 文件的Uri
*/
public static Uri getUriFromFile(String path) {
File file = new File(path);
return Uri.fromFile(file);
}
/**
* 获取文件指定文件的指定单位的大小
*
* @param filePath 文件路径
* @param sizeType 获取大小的类型1为B、2为KB、3为MB、4为GB
* @return double值的大小
*/
public static double getFileOrFilesSize(String filePath, int sizeType) {
File file = new File(filePath);
long blockSize = 0;
try {
if (file.isDirectory()) {
blockSize = getFileSizes(file);
} else {
blockSize = getFileSize(file);
}
} catch (Exception e) {
e.printStackTrace();
L.i("获取文件大小", "获取失败!");
}
return FormetFileSize(blockSize, sizeType);
}
/**
* 调用此方法自动计算指定文件或指定文件夹的大小
*
* @param filePath 文件路径
* @return 计算好的带B、KB、MB、GB的字符串
*/
public static String getAutoFileOrFilesSize(String filePath) {
File file = new File(filePath);
long blockSize = 0;
try {
if (file.isDirectory()) {
blockSize = getFileSizes(file);
} else {
blockSize = getFileSize(file);
}
} catch (Exception e) {
e.printStackTrace();
L.i("获取文件大小", "获取失败!");
}
return FormetFileSize(blockSize);
}
/**
* 获取指定文件大小
*
* @param file
* @return
* @throws Exception
*/
private static long getFileSize(File file) throws Exception {
long size = 0;
if (file.exists()) {
FileInputStream fis = null;
fis = new FileInputStream(file);
size = fis.available();
} else {
// file.createNewFile();
L.i("获取文件大小", "文件不存在!");
}
return size;
}
/**
* 获取目录下指定文件名的文件包括子目录
* <p>大小写忽略</p>
*
* @param dirPath 目录路径
* @param fileName 文件名
* @return 文件链表
*/
public static List<File> searchFileInDir(String dirPath, String fileName) {
return searchFileInDir(getFileByPath(dirPath), fileName);
}
/**
* 获取目录下指定文件名的文件包括子目录
* <p>大小写忽略</p>
*
* @param dir 目录
* @param fileName 文件名
* @return 文件链表
*/
public static List<File> searchFileInDir(File dir, String fileName) {
if (dir == null || !dir.isDirectory()) return null;
List<File> list = new ArrayList<>();
File[] files = dir.listFiles();
if (files != null && files.length != 0) {
for (File file : files) {
if (file.getName().toUpperCase().equals(fileName.toUpperCase())) {
list.add(file);
}
if (file.isDirectory()) {
list.addAll(searchFileInDir(file, fileName));
}
}
}
return list;
}
/**
* 获取指定文件夹
*
* @param f
* @return
* @throws Exception
*/
private static long getFileSizes(File f) throws Exception {
long size = 0;
File flist[] = f.listFiles();
for (int i = 0; i < flist.length; i++) {
if (flist[i].isDirectory()) {
size = size + getFileSizes(flist[i]);
} else {
size = size + getFileSize(flist[i]);
}
}
return size;
}
/**
* 转换文件大小
*
* @param fileS
* @return
*/
private static String FormetFileSize(long fileS) {
DecimalFormat df = new DecimalFormat("#.00");
String fileSizeString = "";
String wrongSize = "0B";
if (fileS == 0) {
return wrongSize;
}
if (fileS < 1024) {
fileSizeString = df.format((double) fileS) + "B";
} else if (fileS < 1048576) {
fileSizeString = df.format((double) fileS / 1024) + "KB";
} else if (fileS < 1073741824) {
fileSizeString = df.format((double) fileS / 1048576) + "MB";
} else {
fileSizeString = df.format((double) fileS / 1073741824) + "GB";
}
return fileSizeString;
}
/**
* 转换文件大小,指定转换的类型
*
* @param fileS
* @param sizeType
* @return
*/
private static double FormetFileSize(long fileS, int sizeType) {
DecimalFormat df = new DecimalFormat("#.00");
double fileSizeLong = 0;
switch (sizeType) {
case SIZETYPE_B:
fileSizeLong = Double.valueOf(df.format((double) fileS));
break;
case SIZETYPE_KB:
fileSizeLong = Double.valueOf(df.format((double) fileS / 1024));
break;
case SIZETYPE_MB:
fileSizeLong = Double.valueOf(df.format((double) fileS / 1048576));
break;
case SIZETYPE_GB:
fileSizeLong = Double.valueOf(df.format((double) fileS / 1073741824));
break;
default:
break;
}
return fileSizeLong;
}
/**
* 复制文件
*
* @param fromFilePath 旧文件地址和名称
* @param toFilePath 新文件地址和名称
* @return 返回备份文件的信息,ok是成功,其它就是错误
*/
public static File copyFile(String fromFilePath, String toFilePath) {
File fromFile = null;
File toFile = null;
try {
fromFile = new File(fromFilePath);
} catch (Exception e) {
L.i(cFromFile + e.getMessage());
return null;
}
try {
toFile = new File(toFilePath);
if (toFile.isDirectory()) {
toFile = new File(toFilePath.endsWith("/") ? toFilePath + fromFile.getName() : toFilePath + "/" + fromFile.getName());
}
} catch (Exception e) {
L.i(ctoFile + e.getMessage());
return null;
}
if (!fromFile.exists()) {
L.i(urlNull);
return null;
}
if (!fromFile.isFile()) {
L.i(isFile);
return null;
}
if (!fromFile.canRead()) {
L.i(canRead);
return null;
}
// 复制到的路径如果不存在就创建
if (!toFile.getParentFile().exists()) {
toFile.getParentFile().mkdirs();
}
if (toFile.exists()) {
toFile.delete();
}
if (!toFile.canWrite()) {
//return notWrite;
}
try {
FileInputStream fosfrom = new FileInputStream(
fromFile);
FileOutputStream fosto = new FileOutputStream(toFile);
byte bt[] = new byte[1024];
int c;
while ((c = fosfrom.read(bt)) > 0) {
fosto.write(bt, 0, c); // 将内容写到新文件当中
}
//关闭数据流
fosfrom.close();
fosto.close();
} catch (Exception e) {
e.printStackTrace();
L.i(copyFalse + e.getMessage());
return null;
}
return toFile;
}
/**
* 创建文件
*
* @param path 文件路径
* @return 创建的文件
*/
public static synchronized File createNewFile(String path) {
File file = new File(path);
String parentPath = path.substring(0, path.lastIndexOf("/") + 1);
File parentFile = new File(parentPath);
if (!parentFile.exists()) {
parentFile.mkdirs();
}
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
return file;
}
/***
* 根据文件后缀回去MIME类型
****/
private static String getMIMEType(File file) {
String type = "*/*";
String fName = file.getName();
//获取后缀名前的分隔符"."在fName中的位置。
int dotIndex = fName.lastIndexOf(".");
if (dotIndex < 0) {
return type;
}
/* 获取文件的后缀名*/
String end = fName.substring(dotIndex, fName.length()).toLowerCase();
if (end == "") return type;
//在MIME和文件类型的匹配表中找到对应的MIME类型。
for (int i = 0; i < MIME_MapTable.length; i++) { //MIME_MapTable??在这里你一定有疑问,这个MIME_MapTable是什么?
if (end.equals(MIME_MapTable[i][0]))
type = MIME_MapTable[i][1];
}
return type;
}
/**
* 获取全路径中的文件拓展名
*
* @param file 文件
* @return 文件拓展名
*/
public static String getFileExtension(File file) {
if (file == null) return null;
return getFileExtension(file.getPath());
}
/**
* 获取全路径中的文件拓展名
*
* @param filePath 文件路径
* @return 文件拓展名
*/
public static String getFileExtension(String filePath) {
if (AppUtil.isEmpty(filePath)) return filePath;
int lastPoi = filePath.lastIndexOf('.');
int lastSep = filePath.lastIndexOf(File.separator);
if (lastPoi == -1 || lastSep >= lastPoi) return "";
return filePath.substring(lastPoi + 1);
}
/**
* 调用系统应用打开文件
*
* @param context 上下文
* @param file 文件
*/
public static void openFile(Context context, File file) {
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//设置intent的Action属性
intent.setAction(Intent.ACTION_VIEW);
//获取文件file的MIME类型
String type = getMIMEType(file);
//设置intent的data和Type属性。
intent.setDataAndType(Uri.fromFile(file), type);
//跳转
try {
context.startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
L.i("找不到打开此文件的应用!");
}
}
/**
* 保存文本文件
* FileUtil.saveStrFile("测试", "log.txt", FILE_STR_PATH, true);
*
* @param res 写入内容
* @param fileName 文件名
* @param filePath 路径
* @param append true新增 false替换
* @return
*/
public static boolean saveStrFile(String res, String fileName, String filePath, boolean append) {
boolean flag = true;
BufferedReader bufferedReader = null;
BufferedWriter bufferedWriter = null;
try {
File file = new File(filePath, fileName);
if (!file.exists()) {
file.getParentFile().mkdirs();
file.createNewFile();
}
bufferedReader = new BufferedReader(new StringReader(res));
bufferedWriter = new BufferedWriter(new FileWriter(file, append));
char buf[] = new char[1024]; //字符缓冲区
int len;
while ((len = bufferedReader.read(buf)) != -1) {
bufferedWriter.write(buf, 0, len);
}
bufferedWriter.flush();
bufferedReader.close();
bufferedWriter.close();
} catch (IOException e) {
e.printStackTrace();
flag = false;
return flag;
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return flag;
}
public static void saveLog(String... logs) {
if (!isSaveLog) {
return;
}
new Thread() {
@Override
public void run() {
super.run();
StringBuilder sb = new StringBuilder();
for (String log : logs) {
sb.append("===")
.append((AppUtil.formatDateGetCurrentTime()))
.append("===")
.append("\n")
.append(log)
.append("\n");
}
String fileName = "log" + AppUtil.formatDateGetDay(System.currentTimeMillis()) + ".txt";
FileUtil.saveStrFile(sb.toString(), fileName, FILE_STR_PATH, true);
}
}.start();
}
private static final String[][] MIME_MapTable = {
// {后缀名,MIME类型}
{".3gp", "video/3gpp"},
{".apk", "application/vnd.android.package-archive"},
{".asf", "video/x-ms-asf"},
{".avi", "video/x-msvideo"},
{".bin", "application/octet-stream"},
{".bmp", "image/bmp"},
{".c", "text/plain"},
{".class", "application/octet-stream"},
{".conf", "text/plain"},
{".cpp", "text/plain"},
{".doc", "application/msword"},
{".docx",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"},
{".xls", "application/vnd.ms-excel"},
{".xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
{".exe", "application/octet-stream"},
{".gif", "image/gif"},
{".gtar", "application/x-gtar"},
{".gz", "application/x-gzip"},
{".h", "text/plain"},
{".htm", "text/html"},
{".html", "text/html"},
{".jar", "application/java-archive"},
{".java", "text/plain"},
{".jpeg", "image/jpeg"},
{".jpg", "image/jpeg"},
{".js", "application/x-javascript"},
{".log", "text/plain"},
{".m3u", "audio/x-mpegurl"},
{".m4a", "audio/mp4a-latm"},
{".m4b", "audio/mp4a-latm"},
{".m4p", "audio/mp4a-latm"},
{".m4u", "video/vnd.mpegurl"},
{".m4v", "video/x-m4v"},
{".mov", "video/quicktime"},
{".mp2", "audio/x-mpeg"},
{".mp3", "audio/x-mpeg"},
{".mp4", "video/mp4"},
{".mpc", "application/vnd.mpohun.certificate"},
{".mpe", "video/mpeg"},
{".mpeg", "video/mpeg"},
{".mpg", "video/mpeg"},
{".mpg4", "video/mp4"},
{".mpga", "audio/mpeg"},
{".msg", "application/vnd.ms-outlook"},
{".ogg", "audio/ogg"},
{".pdf", "application/pdf"},
{".png", "image/png"},
{".pps", "application/vnd.ms-powerpoint"},
{".ppt", "application/vnd.ms-powerpoint"},
{".pptx",
"application/vnd.openxmlformats-officedocument.presentationml.presentation"},
{".prop", "text/plain"}, {".rc", "text/plain"},
{".rmvb", "audio/x-pn-realaudio"}, {".rtf", "application/rtf"},
{".sh", "text/plain"}, {".tar", "application/x-tar"},
{".tgz", "application/x-compressed"}, {".txt", "text/plain"},
{".wav", "audio/x-wav"}, {".wma", "audio/x-ms-wma"},
{".wmv", "audio/x-ms-wmv"},
{".wps", "application/vnd.ms-works"}, {".xml", "text/plain"},
{".z", "application/x-compress"},
{".zip", "application/x-zip-compressed"}, {"", "*/*"}};
}
@@ -0,0 +1,57 @@
package com.sw.dualscreen.utils;
/**
* Log统一管理类
*/
public class L {
private L() {
/* cannot be instantiated */
throw new UnsupportedOperationException("cannot be instantiated");
}
public static boolean isDebug = true;// 是否需要打印bug,可以在application的onCreate函数里面初始化
private static final String TAG = "mzf";
// 下面四个是默认tag的函数
public static void i(String msg) {
if (isDebug)
android.util.Log.i(TAG, msg);
}
public static void d(String msg) {
if (isDebug)
android.util.Log.d(TAG, msg);
}
public static void e(String msg) {
if (isDebug)
android.util.Log.e(TAG, msg);
}
public static void v(String msg) {
if (isDebug)
android.util.Log.v(TAG, msg);
}
// 下面是传入自定义tag的函数
public static void i(String tag, String msg) {
if (isDebug)
android.util.Log.i(tag, msg);
}
public static void d(String tag, String msg) {
if (isDebug)
android.util.Log.d(tag, msg);
}
public static void e(String tag, String msg) {
if (isDebug)
android.util.Log.e(tag, msg);
}
public static void v(String tag, String msg) {
if (isDebug)
android.util.Log.v(tag, msg);
}
}
@@ -69,6 +69,7 @@
android:contentDescription="食物图预览"
android:scaleType="centerCrop"
android:visibility="gone"
app:strokeColor="@android:color/transparent"
app:shapeAppearance="@style/round10dpCornerStyle" />
<View
@@ -37,7 +37,7 @@
android:layout_height="100dp"
android:layout_marginHorizontal="32dp"
android:layout_marginBottom="20dp"
android:background="@drawable/bg_btn_save2"
android:background="@drawable/bg_btn_confirm"
android:text="已支付确认"
android:enabled="true"
android:textColor="@color/white"
@@ -28,6 +28,7 @@
android:layout_width="120dp"
android:layout_height="120dp"
android:src="@drawable/ic_avatar_default"
app:strokeColor="@android:color/transparent"
app:shapeAppearance="@style/CircleStyle"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@@ -93,6 +93,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
app:strokeColor="@android:color/transparent"
app:shapeAppearance="@style/round10dpCornerStyle"
tools:src="@mipmap/ic_launcher"/>
@@ -102,6 +103,7 @@
android:layout_height="match_parent"
android:scaleType="centerCrop"
tools:src="@mipmap/ic_launcher_round"
app:strokeColor="@android:color/transparent"
app:shapeAppearance="@style/round10dpCornerStyle"
android:visibility="gone"/>
@@ -124,6 +124,7 @@
android:src="@drawable/ic_avatar_default"
android:layout_marginTop="28dp"
app:shapeAppearance="@style/CircleStyle"
app:strokeColor="@android:color/transparent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />