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> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <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"> <Target type="DEFAULT_BOOT">
<handle> <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> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>
@@ -20,7 +20,7 @@ object GlobalData {
/** /**
* 具体业务baseurl * 具体业务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" var appBaseUrl: String = "https://dev.yixiong-tech.com:8081"
/** /**
@@ -24,6 +24,7 @@ class MyApp : App() {
Timber.d("deviceId = $deviceId") Timber.d("deviceId = $deviceId")
// deviceId = "3ea47dc0-3cf0-3c2f-909c-265a9a65572e" // deviceId = "3ea47dc0-3cf0-3c2f-909c-265a9a65572e"
GlobalData.deviceId = deviceId GlobalData.deviceId = deviceId
GlobalData.appBaseUrl = if ("2987f0c5-5754-33e9-b00a-251db5e2e55f" == deviceId) GlobalData.appBaseUrl2 else GlobalData.appBaseUrl
ObjectBox.init(this) ObjectBox.init(this)
// 初始化崩溃处理器 // 初始化崩溃处理器
CrashHandler.init(this) CrashHandler.init(this)
@@ -18,12 +18,14 @@ import com.sw.dualscreen.R
import com.sw.dualscreen.databinding.DialogWaitingBinding import com.sw.dualscreen.databinding.DialogWaitingBinding
import com.sw.dualscreen.model.IActivityResult import com.sw.dualscreen.model.IActivityResult
import com.sw.dualscreen.model.response.PostEvent import com.sw.dualscreen.model.response.PostEvent
import com.sw.dualscreen.utils.FileUtil
import com.sw.dualscreen.view.CustomDialog import com.sw.dualscreen.view.CustomDialog
import com.sw.dualscreen.viewmodel.BaseViewModel import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import timber.log.Timber
abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() { abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
@@ -131,6 +133,11 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
launchPermissionCallback?.callback(result) launchPermissionCallback?.callback(result)
} }
fun log(msg: String) {
Timber.d(msg)
FileUtil.saveLog(msg)
}
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
fun onPostEvent(event: PostEvent) { fun onPostEvent(event: PostEvent) {
@@ -3,6 +3,7 @@ package com.sw.dualscreen.activity
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.sw.dualscreen.GlobalData import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.R 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.gone
import com.sw.dualscreen.ext.hideKeyboard import com.sw.dualscreen.ext.hideKeyboard
import com.sw.dualscreen.ext.visible import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.model.response.ResetBoxEvent
import com.sw.dualscreen.objbox.CollectedFoodInfo import com.sw.dualscreen.objbox.CollectedFoodInfo
import com.sw.dualscreen.objbox.Food
import com.sw.dualscreen.objbox.ObjectBox import com.sw.dualscreen.objbox.ObjectBox
import com.sw.dualscreen.viewmodel.BaseViewModel import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel import com.sw.dualscreen.viewmodel.UserViewModel
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import io.objectbox.Box import kotlinx.coroutines.launch
import io.objectbox.kotlin.boxFor
import org.greenrobot.eventbus.EventBus
class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() { class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
@@ -51,8 +48,7 @@ class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
override fun initialize() { override fun initialize() {
super.initialize() super.initialize()
box = ObjectBox.boxStore.boxFor(Food::class) binding.root.setOnClickListener { it.hideKeyboard() }
//binding.root.setOnClickListener { it.hideKeyboard() }
binding.rvFoodList.let { binding.rvFoodList.let {
it.layoutManager = LinearLayoutManager(this) it.layoutManager = LinearLayoutManager(this)
it.adapter = adapter it.adapter = adapter
@@ -103,9 +99,6 @@ class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
getCollectGoods() getCollectGoods()
} }
private lateinit var box: Box<Food>
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
private fun getCollectGoods(searchName: String? = null) { private fun getCollectGoods(searchName: String? = null) {
// if (box == null) { // if (box == null) {
@@ -199,16 +192,13 @@ class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
showWaitingDialog("加载中……") showWaitingDialog("加载中……")
viewModel.deleteCollectFood(food.foodId, GlobalData.foodModelVersion) { deleteSuccess -> viewModel.deleteCollectFood(food.foodId, GlobalData.foodModelVersion) { deleteSuccess ->
if (deleteSuccess) { if (deleteSuccess) {
Thread { // Thread {}.start()
val filterFoodList = box.all.filter { it.foodName == food.foodName } lifecycleScope.launch {
val filterFoodList = ObjectBox.filter(food.foodName)
filterFoodList.forEach { it.isDel = true } filterFoodList.forEach { it.isDel = true }
box.put(filterFoodList) if (filterFoodList.isNotEmpty()) {
// val filterIdList = filterFoodList.map { it.id } ObjectBox.putAll(filterFoodList)
////// ObjectBox.boxStore.runInTx { }
//// box.removeByIds(filterIdList)
////// EventBus.getDefault().post(ResetBoxEvent())
////// }
runOnUiThread { runOnUiThread {
hideWaitingDialog() hideWaitingDialog()
@@ -218,7 +208,7 @@ class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
loadEmptyView() loadEmptyView()
} }
} }
}.start() }
} }
} }
} }
@@ -2,6 +2,7 @@ package com.sw.dualscreen.activity
import android.content.Intent import android.content.Intent
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import com.sw.dualscreen.GlobalData import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.databinding.ActivityInitBinding import com.sw.dualscreen.databinding.ActivityInitBinding
import com.sw.dualscreen.model.response.FoodVector 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.objbox.ObjectBox
import com.sw.dualscreen.viewmodel.BaseViewModel import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel import com.sw.dualscreen.viewmodel.UserViewModel
import io.objectbox.Box import kotlinx.coroutines.launch
import io.objectbox.kotlin.boxFor
class InitActivity : BaseActivity<ActivityInitBinding>() { class InitActivity : BaseActivity<ActivityInitBinding>() {
companion object { companion object {
private const val PAGE_SIZE = 100 private const val PAGE_SIZE = 100
} }
private val userViewModel by viewModels<UserViewModel>() private val userViewModel by viewModels<UserViewModel>()
private var pageNum = 1 private var pageNum = 1
@@ -28,12 +29,14 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
return ActivityInitBinding.inflate(layoutInflater) return ActivityInitBinding.inflate(layoutInflater)
} }
private fun getCollectedFoodVector(block:()-> Unit) { private fun getCollectedFoodVector(block: () -> Unit) {
userViewModel.getCollectedFoodVector(param = mutableMapOf( userViewModel.getCollectedFoodVector(
"pageNum" to "$pageNum", param = mutableMapOf(
"pageSize" to "$PAGE_SIZE", "pageNum" to "$pageNum",
"version" to GlobalData.foodModelVersion "pageSize" to "$PAGE_SIZE",
)) { items -> "version" to GlobalData.foodModelVersion
)
) { items ->
val list = items ?: emptyList() val list = items ?: emptyList()
if (pageNum == 1 && list.isEmpty()) { if (pageNum == 1 && list.isEmpty()) {
block() block()
@@ -51,32 +54,37 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
} }
} }
} }
// private val box by lazy { ObjectBox.boxStore.boxFor(Food::class) }
// private val box by lazy { ObjectBox.boxStore.boxFor(Food::class) }
private fun saveFoodVector(list: List<FoodVector>) { private fun saveFoodVector(list: List<FoodVector>) {
val vectorList = list.map { vt -> lifecycleScope.launch {
val foodVector = vt.foodVector?.removeSurrounding("[", "]")?.split(",")?.map { it.toFloatOrNull()?:0.0f }?.toFloatArray() val vectorList = list.map { vt ->
Food( val foodVector = vt.foodVector?.removeSurrounding("[", "]")?.split(",")
collectId = vt.id, ?.map { it.toFloatOrNull() ?: 0.0f }?.toFloatArray()
foodId = vt.foodId, Food(
foodName = vt.foodName, collectId = vt.id,
version = vt.version, foodId = vt.foodId,
foodVector = foodVector foodName = vt.foodName,
) version = vt.version,
foodVector = foodVector
)
}
ObjectBox.putAll(vectorList)
} }
val box = ObjectBox.boxStore.boxFor(Food::class)
box.put(vectorList)
} }
override fun initialize() { override fun initialize() {
super.initialize() super.initialize()
showWaitingDialog("加载中……") showWaitingDialog("加载中……")
FoodModule.init(this) { lifecycleScope.launch {
getCollectedFoodVector { FoodModule.init(this@InitActivity) {
runOnUiThread { getCollectedFoodVector {
hideWaitingDialog() runOnUiThread {
binding.root.postDelayed({ hideWaitingDialog()
goMainActivity() binding.root.postDelayed({
}, 1000) goMainActivity()
}, 1000)
}
} }
} }
} }
@@ -3,6 +3,7 @@ package com.sw.dualscreen.activity
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Outline import android.graphics.Outline
import android.hardware.display.DisplayManager import android.hardware.display.DisplayManager
import android.net.Uri import android.net.Uri
@@ -21,11 +22,11 @@ import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import com.sw.dualscreen.GlobalData import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.GlobalKey import com.sw.dualscreen.GlobalKey
import com.sw.dualscreen.adapter.DialogSearchFoodAdapter
import com.sw.dualscreen.adapter.MainFoodListAdapter import com.sw.dualscreen.adapter.MainFoodListAdapter
import com.sw.dualscreen.databinding.ActivityMainBinding import com.sw.dualscreen.databinding.ActivityMainBinding
import com.sw.dualscreen.dialog.RemindDialog 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.FaceDatabase
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import io.objectbox.Box
import io.objectbox.kotlin.boxFor
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.RequestBody import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import org.json.JSONObject import org.json.JSONObject
import timber.log.Timber
import java.io.File import java.io.File
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.Locale import java.util.Locale
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.math.abs
import kotlin.math.roundToInt import kotlin.math.roundToInt
/** /**
@@ -111,7 +111,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
searchFoodList[index].isChecked = index == position searchFoodList[index].isChecked = index == position
} }
val item = searchFoodList[position] val item = searchFoodList[position]
Timber.d("itemClick ${item.foodName}, position = $position") log("itemClick ${item.foodName}, position = $position")
item.photoUri = null item.photoUri = null
checkedItem = item checkedItem = item
notifyDataSetChanged() notifyDataSetChanged()
@@ -149,7 +149,6 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
override fun initialize() { override fun initialize() {
super.initialize() super.initialize()
box = ObjectBox.boxStore.boxFor(Food::class)
isPageVisible = true isPageVisible = true
addBackEventListener() addBackEventListener()
initView() initView()
@@ -177,6 +176,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
} }
updateDateTime() updateDateTime()
binding.ivSetting.setOnClickListener { binding.ivSetting.setOnClickListener {
isRefreshPage = false
startActivity(Intent(this, SettingActivity::class.java)) startActivity(Intent(this, SettingActivity::class.java))
} }
binding.previewView.outlineProvider = object : ViewOutlineProvider() { binding.previewView.outlineProvider = object : ViewOutlineProvider() {
@@ -221,8 +221,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
} }
pauseAnalysis() pauseAnalysis()
recognizeWeight = lastWeight recognizeWeight = lastWeight
Timber.tag(TAG) log("registerDataChange,副屏updateFood调用前耗时:${System.currentTimeMillis() - startTime}")
.d("registerDataChange,副屏updateFood调用前耗时:${System.currentTimeMillis() - startTime}")
startTime = System.currentTimeMillis() startTime = System.currentTimeMillis()
presentation?.updateFood(foodInfo) presentation?.updateFood(foodInfo)
binding.tvFoodName.text = foodInfo.foodName binding.tvFoodName.text = foodInfo.foodName
@@ -239,7 +238,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
override fun registerDataChange() { override fun registerDataChange() {
super.registerDataChange() super.registerDataChange()
SensorScaleUtils.addWeightListener { weight -> SensorScaleUtils.addWeightListener { weight ->
Timber.d("registerDataChange weight = $weight") log("registerDataChange weight = $weight")
if (isPageVisible.not() || isStartRecognize.not()) { if (isPageVisible.not() || isStartRecognize.not()) {
if (ActivityManager.currentActivity() is MainActivity) { if (ActivityManager.currentActivity() is MainActivity) {
isPageVisible = true isPageVisible = true
@@ -247,7 +246,8 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
} }
return@addWeightListener 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) recognizeByWeight(weight, isWeightChange)
} }
} }
@@ -256,16 +256,19 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
presentation?.updateWeight(weight) presentation?.updateWeight(weight)
if (weight <= 0.005) { if (weight <= 0.005) {
log("recognizeByWeight---00002,未超过5克")
////余量取餐,检测到秤上没有东西,重新启动识别菜品 && presentation?.mealPickupMode == 1 ////余量取餐,检测到秤上没有东西,重新启动识别菜品 && presentation?.mealPickupMode == 1
isRecognitionFood = true isRecognitionFood = true
return return
} }
if (isWeightChange && isRecognitionFood) { // 大于50g if (isWeightChange && isRecognitionFood) { // 大于50g
log("recognizeByWeight---00003,mealPickupMode=${presentation?.mealPickupMode}")
if (presentation?.mealPickupMode == 1) { if (presentation?.mealPickupMode == 1) {
isRecognitionFood = false isRecognitionFood = false
} }
debouncer.debounce { debouncer.debounce {
log("recognizeByWeight---00004")
recognizeFood() recognizeFood()
} }
} else { } else {
@@ -282,31 +285,30 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
LightManager.closeRedLight() LightManager.closeRedLight()
LightManager.openGreenLight() LightManager.openGreenLight()
showWaitingDialog("识别中,请稍后……") showWaitingDialog("识别中,请稍后……")
takePhoto( takePhoto(successCallback = { photoUri ->
successCallback = { photoUri -> Thread {
Thread { onTakePhotoSuccess(photoUri)
onTakePhotoSuccess(photoUri) }.start()
}.start() failCount = 0
failCount = 0 }, failureCallback = {
}, failureCallback = { runOnUiThread {
runOnUiThread { if (failCount >= 10) {
if (failCount >= 10) { ToastUtils.showToast("相机异常,请稍后重试")
//ToastUtils.showToast("相机异常,请稍后重试") shutdownCamera()
shutdownCamera() setupCamera()
setupCamera() return@runOnUiThread
return@runOnUiThread
}
} }
failCount++ }
hideWaitingDialog() failCount++
hideWaitingDialog()
shutdownCamera() shutdownCamera()
setupCamera() setupCamera()
binding.root.postDelayed({ binding.root.postDelayed({
recognizeFood() recognizeFood()
}, 1000) }, 1000)
}) })
} }
/** /**
@@ -318,7 +320,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
private fun updateFoodInfo(list: MutableList<FoodInfo>, scoreList: List<IdNameScore>) { private fun updateFoodInfo(list: MutableList<FoodInfo>, scoreList: List<IdNameScore>) {
val queryData = GsonUtils.toJson(list) val queryData = GsonUtils.toJson(list)
val recData = GsonUtils.toJson(scoreList) val recData = GsonUtils.toJson(scoreList)
Timber.d("registerDataChange识别后查询接口数据:$queryData,识别数据:$recData") log("registerDataChange识别后查询接口数据:$queryData,识别数据:$recData")
if (list.isEmpty()) { if (list.isEmpty()) {
binding.tvToSearch.let { binding.tvToSearch.let {
it.text = "未查询到,手动搜索" it.text = "未查询到,手动搜索"
@@ -359,24 +361,21 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
} }
private fun setupCamera() { private fun setupCamera() {
Timber.d("setupCamera") log("setupCamera")
cameraProviderFuture = ProcessCameraProvider.getInstance(this) cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture!!.addListener({ cameraProviderFuture!!.addListener({
val cameraProvider = cameraProviderFuture!!.get() val cameraProvider = cameraProviderFuture!!.get()
// 1. 创建预览用例 // 1. 创建预览用例
val preview = Preview.Builder() val preview = Preview.Builder().build().also {
.build() it.setSurfaceProvider(binding.previewView.surfaceProvider)
.also { }
it.setSurfaceProvider(binding.previewView.surfaceProvider)
}
// 2. 创建图像分析用例(用于副屏显示) // 2. 创建图像分析用例(用于副屏显示)
imageAnalysis = ImageAnalysis.Builder() imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
// .setTargetResolution(Size(640, 480)) // 降低分辨率减轻负担 // .setTargetResolution(Size(640, 480)) // 降低分辨率减轻负担
.build() .build().also { analysis ->
.also { analysis ->
analysis.setAnalyzer(executor) { imageProxy -> analysis.setAnalyzer(executor) { imageProxy ->
// 仅在步骤1时传递 // 仅在步骤1时传递
if (isAnalyzing) { if (isAnalyzing) {
@@ -389,10 +388,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
} }
// 3. 创建拍照用例 // 3. 创建拍照用例
imageCapture = ImageCapture.Builder() imageCapture =
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) ImageCapture.Builder().setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
// .setTargetResolution(Size(1920, 1080)) // 设置拍照分辨率 // .setTargetResolution(Size(1920, 1080)) // 设置拍照分辨率
.build() .build()
// 4. 选择摄像头 // 4. 选择摄像头
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
@@ -405,20 +404,20 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
cameraProvider.bindToLifecycle( cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageAnalysis, imageCapture this, cameraSelector, preview, imageAnalysis, imageCapture
) )
} catch (exc: Exception) { } catch (e: Exception) {
Timber.e(exc.message) log("setupCamera方法异常:${e.message}")
} }
}, ContextCompat.getMainExecutor(this)) }, ContextCompat.getMainExecutor(this))
} }
fun pauseAnalysis() { fun pauseAnalysis() {
Timber.d("pauseAnalysis") log("pauseAnalysis")
isAnalyzing = false isAnalyzing = false
imageAnalysis?.clearAnalyzer() imageAnalysis?.clearAnalyzer()
} }
fun resumeAnalysis() { fun resumeAnalysis() {
Timber.d("resumeAnalysis isAnalyzing = $isAnalyzing") log("resumeAnalysis isAnalyzing = $isAnalyzing")
if (isAnalyzing) return if (isAnalyzing) return
// isAnalyzing = true // isAnalyzing = true
imageAnalysis?.setAnalyzer(executor) { imageProxy -> imageAnalysis?.setAnalyzer(executor) { imageProxy ->
@@ -434,7 +433,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
* 停止相机识别 * 停止相机识别
*/ */
private fun shutdownCamera() { private fun shutdownCamera() {
Timber.d("shutdownCamera") log("shutdownCamera")
try { try {
// 1. 首先停止图像分析 // 1. 首先停止图像分析
imageAnalysis?.clearAnalyzer() imageAnalysis?.clearAnalyzer()
@@ -449,13 +448,13 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
// 4. 停止副屏更新 // 4. 停止副屏更新
isAnalyzing = false isAnalyzing = false
} catch (e: Exception) { } 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) { private fun takePhoto(successCallback: (Uri) -> Unit, failureCallback: () -> Unit) {
Timber.d("takePhoto") log("takePhoto")
// 临时停止分析以避免干扰 // 临时停止分析以避免干扰
isAnalyzing = false isAnalyzing = false
@@ -463,21 +462,16 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
val executor = ContextCompat.getMainExecutor(this) val executor = ContextCompat.getMainExecutor(this)
val cacheDir = cacheDir val cacheDir = cacheDir
val photoFile = File.createTempFile( val photoFile = File.createTempFile(
"IMG_${System.currentTimeMillis()}", "IMG_${System.currentTimeMillis()}", ".jpg", cacheDir
".jpg",
cacheDir
) )
val cacheOutputOptions = val cacheOutputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
ImageCapture.OutputFileOptions.Builder(photoFile).build()
// 执行拍照 // 执行拍照
imageCapture?.takePicture( imageCapture?.takePicture(
cacheOutputOptions, cacheOutputOptions, executor, object : ImageCapture.OnImageSavedCallback {
executor,
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
val savedUri = outputFileResults.savedUri val savedUri = outputFileResults.savedUri
Timber.d("takePhoto savedUri = $savedUri") log("takePhoto savedUri = $savedUri")
if (savedUri != null) { if (savedUri != null) {
successCallback(savedUri) successCallback(savedUri)
} }
@@ -486,14 +480,13 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
} }
override fun onError(exception: ImageCaptureException) { override fun onError(exception: ImageCaptureException) {
Timber.e("拍照失败: ${exception.message}") log("takePhoto拍照失败: ${exception.message}")
failureCallback() failureCallback()
//ToastUtils.showToast("拍照失败: ${exception.message}") //ToastUtils.showToast("拍照失败: ${exception.message}")
// 恢复图像分析 // 恢复图像分析
isAnalyzing = true isAnalyzing = true
} }
} })
)
} }
private fun getSecondaryDisplay(): Display? { private fun getSecondaryDisplay(): Display? {
@@ -522,7 +515,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
).apply { ).apply {
setStepChangeCallback { step -> setStepChangeCallback { step ->
if (step == 1) { if (step == 1) {
Timber.d("setStepChangeCallback${step}") log("setStepChangeCallback${step}")
updateCurrentFood(null) updateCurrentFood(null)
bottomSheetDialog?.dismiss() bottomSheetDialog?.dismiss()
} }
@@ -560,12 +553,16 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
} else { } else {
//副屏未显示重新加载 //副屏未显示重新加载
isRecognitionFood = true isRecognitionFood = true
log("onResume-00001")
if (presentation == null || !presentation!!.isShowing) { if (presentation == null || !presentation!!.isShowing) {
log("onResume-00002,副屏未显示,加载副屏")
lastWeight = 0.0 lastWeight = 0.0
setupSecondaryDisplay() setupSecondaryDisplay()
presentation?.step1FoodRecognizing() presentation?.step1FoodRecognizing()
} else { } else {
log("onResume-00003,副屏显示,更新副屏, isRefreshPage=$isRefreshPage")
if (isRefreshPage) { if (isRefreshPage) {
isRefreshPage = false
//已显示副屏,则进行更新 //已显示副屏,则进行更新
val weight = lastWeight val weight = lastWeight
lastWeight = 0.0 lastWeight = 0.0
@@ -576,7 +573,6 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
recognizeByWeight(weight, true) recognizeByWeight(weight, true)
}, 500) }, 500)
} }
isRefreshPage = !isRefreshPage
} }
} }
@@ -627,7 +623,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
notEnoughOneBlock: () -> Unit = {}, notEnoughOneBlock: () -> Unit = {},
block: () -> 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 val pickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
// if (pickupMode == 0) { // if (pickupMode == 0) {
// if (foodWeight <= 5) { // if (foodWeight <= 5) {
@@ -735,10 +731,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
private var isRefreshPage = false private var isRefreshPage = false
@Subscribe(threadMode = ThreadMode.MAIN) // @Subscribe(threadMode = ThreadMode.MAIN)
fun onClickBackEvent(event: ClickBackEvent) { // fun onClickBackEvent(event: ClickBackEvent) {
isRefreshPage = false // isRefreshPage = false
} // }
private fun clickPayButton() { private fun clickPayButton() {
if (checkedItem == null) { if (checkedItem == null) {
@@ -761,7 +757,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
//余量计量 //余量计量
eatWeight = (recognizeWeight - lastWeight) * 1000 eatWeight = (recognizeWeight - lastWeight) * 1000
foodWeight = recognizeWeight * 1000 foodWeight = recognizeWeight * 1000
Timber.d("eatWeight=$eatWeight, recognizeWeight=$recognizeWeight, lastWeight=$lastWeight") log("eatWeight=$eatWeight, recognizeWeight=$recognizeWeight, lastWeight=$lastWeight")
} }
} else { } else {
//不计费 //不计费
@@ -782,7 +778,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
eatNum = eatNum, eatNum = eatNum,
userId = null, userId = null,
) { ) {
// TODO: 暂不关闭首页副屏 isRefreshPage = false
//presentation?.dismiss() //presentation?.dismiss()
startActivity(Intent(this, PayActivity::class.java).apply { startActivity(Intent(this, PayActivity::class.java).apply {
putExtra(PayActivity.FOOD_INFO, checkedItem!!) putExtra(PayActivity.FOOD_INFO, checkedItem!!)
@@ -814,13 +810,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
* 上传菜品信息 * 上传菜品信息
*/ */
private suspend fun uploadCollectFoodPics( private suspend fun uploadCollectFoodPics(
foodInfo: FoodInfo, foodInfo: FoodInfo, imageFile: File?, imageVector: FloatArray?
imageFile: File?,
imageVector: FloatArray?
) { ) {
if (imageFile == null || imageVector == null) { if (imageFile == null || imageVector == null) {
Timber.tag(TAG) log("是否null判断,imageFile == null:${imageFile == null},imageVector == null:${imageVector == null}")
.d("是否null判断,imageFile == null:${imageFile == null},imageVector == null:${imageVector == null}")
return return
} }
val foodId = foodInfo.foodId val foodId = foodInfo.foodId
@@ -834,53 +827,45 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
val vectors = listOf(imageVector) val vectors = listOf(imageVector)
val foodVectorList = vectors.map { val foodVectorList = vectors.map {
it.joinToString( it.joinToString(
separator = ",", separator = ",", prefix = "[", postfix = "]"
prefix = "[",
postfix = "]"
) )
} }
val foodVectorJson = val foodVectorJson =
foodVectorList.joinToString(separator = ",", prefix = "[", postfix = "]") foodVectorList.joinToString(separator = ",", prefix = "[", postfix = "]")
Timber.tag(TAG).d("json=$foodVectorJson") log("json=$foodVectorJson")
params["foodVector"] = foodVectorJson.toRequestBody() params["foodVector"] = foodVectorJson.toRequestBody()
val idList = viewModel.uploadCollectFoodPics(files, params) val idList = viewModel.uploadCollectFoodPics(files, params)
if (idList.isNullOrEmpty()) { if (idList.isNullOrEmpty()) {
loadRemindDialog("上传失败,未返回向量数据id") loadRemindDialog("上传失败,未返回向量数据id")
Timber.tag(TAG).d("idList为空") log("idList为空")
return return
} }
Timber.tag(TAG) val all = ObjectBox.getAll()
.d("uploadCollectFoodPics-已采集向量总数:${box.all.filter { it.isDel.not() }.size}条") val size = all.filter { it.isDel.not() }.size
val filterList = box.all.filter { it.collectId == idList[0] } log("uploadCollectFoodPics-已采集向量总数:${size}")
val filterList = all.filter { it.collectId == idList[0] }
if (filterList.isNotEmpty()) { if (filterList.isNotEmpty()) {
loadRemindDialog("返回的id${idList[0]}已存在") loadRemindDialog("返回的id${idList[0]}已存在")
return return
} }
Thread { ObjectBox.put(
box.put( Food(
Food( collectId = idList[0],
collectId = idList[0], foodId = foodId,
foodId = foodId, foodName = foodName,
foodName = foodName, foodVector = imageVector,
foodVector = imageVector, version = foodModelVersion
version = foodModelVersion
)
) )
}.start() )
} }
private fun loadRemindDialog(msg: String) { private fun loadRemindDialog(msg: String) {
runOnUiThread { runOnUiThread {
RemindDialog( RemindDialog(
context = context, context = context, content = msg, confirmBlock = {}).show()
content = msg,
confirmBlock = {}
).show()
} }
} }
private lateinit var box: Box<Food>
private fun uri2File(uri: Uri, block: (File?, FloatArray?) -> Unit) { private fun uri2File(uri: Uri, block: (File?, FloatArray?) -> Unit) {
ImageUtil.uriToBitmap(this, uri)?.let { bitmap -> ImageUtil.uriToBitmap(this, uri)?.let { bitmap ->
val imageVector = try { val imageVector = try {
@@ -892,7 +877,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
val imageFile = BitmapSaver.saveToAppFilesDir( val imageFile = BitmapSaver.saveToAppFilesDir(
bitmap, this, "IMG_CROP_${System.currentTimeMillis()}.jpg" 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) block(imageFile, imageVector)
if (bitmap.isRecycled.not()) { if (bitmap.isRecycled.not()) {
bitmap.recycle() bitmap.recycle()
@@ -913,8 +898,8 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
// private val initialDelay = 5 * 60 * 1000L // private val initialDelay = 5 * 60 * 1000L
// private val dealyMillis = 10 * 60 * 1000L // private val dealyMillis = 10 * 60 * 1000L
private val initialDelay = 60 * 1000L private val initialDelay = 30 * 1000L
private val dealyMillis = 30 * 1000L private val dealyMillis = 60 * 1000L
private var taskPageNo = 1 private var taskPageNo = 1
fun startFaceTask() { fun startFaceTask() {
faceTaskJob = faceTaskJob =
@@ -936,8 +921,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
} }
updateFaceData(list) updateFaceData(list)
} }
} })
)
} }
} }
@@ -952,9 +936,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
} else { } else {
//保存数据 //保存数据
val faceEntity = FaceEntity( val faceEntity = FaceEntity(
model.userId, model.userId, null, Base64.decode(model.faceFeatureStr)
null,
Base64.decode(model.faceFeatureStr)
).also { ).also {
it.userType = "1" it.userType = "1"
} }
@@ -988,66 +970,71 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
private fun onTakePhotoSuccess(photoUri: Uri) { private fun onTakePhotoSuccess(photoUri: Uri) {
lastPhotoUri = photoUri lastPhotoUri = photoUri
Timber.tag(TAG).d("main,takePhoto耗时:${System.currentTimeMillis() - startTime}") log("registerDataChange main,takePhoto耗时:${System.currentTimeMillis() - startTime}")
startTime = System.currentTimeMillis() startTime = System.currentTimeMillis()
Timber.d("registerDataChange photoUri = ${photoUri.path}") log("registerDataChange photoUri = ${photoUri.path}")
// viewModel.getIdentifiedFoodList() // viewModel.getIdentifiedFoodList()
ImageUtil.uriToBitmap(this, photoUri)?.let { bitmap -> lifecycleScope.launch {
Timber.tag(TAG).d("main,uriToBitmap耗时:${System.currentTimeMillis() - startTime}") ImageUtil.uriToBitmap(this@MainActivity, photoUri)?.let { bitmap ->
startTime = System.currentTimeMillis() queryFoodData(bitmap)
Timber.d("registerDataChange photoUri 拿到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) // val bmp = BitmapCropper.cropCenter(bitmap, 1300, 900)
Timber.d("registerDataChange photoUri bitmap裁剪完成") log("registerDataChange registerDataChange photoUri bitmap裁剪完成")
val file = BitmapSaver.saveToAppFilesDir( val file = BitmapSaver.saveToAppFilesDir(
bitmap, this, "IMG_CROP_${System.currentTimeMillis()}.jpg" bitmap, this, "IMG_CROP_${System.currentTimeMillis()}.jpg"
) )
Timber.tag(TAG) log("registerDataChange main,saveToAppFilesDir耗时:${System.currentTimeMillis() - startTime}")
.d("main,saveToAppFilesDir耗时:${System.currentTimeMillis() - startTime}") startTime = System.currentTimeMillis()
startTime = System.currentTimeMillis() log("registerDataChange photoUri bitmap保存文件路径:${file?.absolutePath}")
Timber.d("registerDataChange photoUri bitmap保存文件路径:${file?.absolutePath}") //val nameList = FoodModule.queryFood(bitmap)
//val nameList = FoodModule.queryFood(bitmap) val scoreList = FoodModule.getFoodScoreList(bitmap)
val scoreList = FoodModule.getFoodScoreList(bitmap) val foodName = if (scoreList.isNotEmpty()) {
val recDataJson = GsonUtils.toJson(scoreList) val recDataJson = GsonUtils.toJson(scoreList)
Timber.tag(TAG).d("main,getFoodScoreList耗时:${System.currentTimeMillis() - startTime}") log("registerDataChange main,getFoodScoreList耗时:${System.currentTimeMillis() - startTime}")
startTime = System.currentTimeMillis() startTime = System.currentTimeMillis()
Timber.d("registerDataChange photoUri 拿到识别数据:$recDataJson") log("registerDataChange photoUri 拿到识别数据:$recDataJson")
val nameList = scoreList.map { it.name } val nameList = scoreList.map { it.name }
val foodName = nameList.joinToString(separator = ",") 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
hideWaitingDialog()
Timber.d("registerDataChange photoUri 识别数据名称:$foodName")
runOnUiThread { runOnUiThread {
binding.flPay.run { binding.tvToSearch.let {
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0) it.text = "未识别到,手动搜索"
//0-计费,1-不计费 it.visible()
if (chargeMode == 0) visible() else gone()
} }
} }
if (TextUtils.isEmpty(foodName)) { } else {
LightManager.closeGreenLight() //binding.layoutRescan.visibility = View.GONE
LightManager.closeRedLight() showWaitingDialog("正在查询菜品信息,请稍后……")
//binding.layoutRescan.visibility = View.VISIBLE viewModel.getFoodInfo(foodName) { list ->
runOnUiThread { runOnUiThread {
binding.tvToSearch.let { hideWaitingDialog()
it.text = "未识别到,手动搜索" log("registerDataChange main,getFoodInfo耗时:${System.currentTimeMillis() - startTime}")
it.visible() startTime = System.currentTimeMillis()
} updateFoodInfo(list.toMutableList(), scoreList)
}
} else {
//binding.layoutRescan.visibility = View.GONE
showWaitingDialog("正在查询菜品信息,请稍后……")
viewModel.getFoodInfo(foodName) { list ->
runOnUiThread {
hideWaitingDialog()
Timber.tag(TAG)
.d("main,getFoodInfo耗时:${System.currentTimeMillis() - startTime}")
startTime = System.currentTimeMillis()
updateFoodInfo(list.toMutableList(), scoreList)
}
} }
} }
} }
} }
@@ -34,9 +34,9 @@ class SettingActivity : BaseActivity<ActivitySettingBinding>() {
initBackDispatcher() initBackDispatcher()
binding.include.let { binding.include.let {
it.ivPageBack.setOnClickListener { it.ivPageBack.setOnClickListener {
// val intent = Intent(this, MainActivity::class.java) //// val intent = Intent(this, MainActivity::class.java)
// startActivity(intent) //// startActivity(intent)
EventBus.getDefault().post(ClickBackEvent()) // EventBus.getDefault().post(ClickBackEvent())
finish() finish()
} }
it.tvPageTitle.text = "设置" it.tvPageTitle.text = "设置"
@@ -73,13 +73,13 @@ class SettingActivity : BaseActivity<ActivitySettingBinding>() {
private lateinit var backPressedCallback: OnBackPressedCallback private lateinit var backPressedCallback: OnBackPressedCallback
private fun initBackDispatcher() { private fun initBackDispatcher() {
// 创建回调,true表示初始启用状态 // 创建回调,true表示初始启用状态
backPressedCallback = object : OnBackPressedCallback(true) { // backPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { // override fun handleOnBackPressed() {
EventBus.getDefault().post(ClickBackEvent()) //// EventBus.getDefault().post(ClickBackEvent())
} // }
} // }
// 注册回调,使用lifecycleOwner确保生命周期安全 // // 注册回调,使用lifecycleOwner确保生命周期安全
onBackPressedDispatcher.addCallback(this, backPressedCallback) // onBackPressedDispatcher.addCallback(this, backPressedCallback)
} }
@@ -19,7 +19,6 @@ import com.sw.dualscreen.activity.CollectedFoodActivity
import com.sw.dualscreen.activity.SettingActivity import com.sw.dualscreen.activity.SettingActivity
import com.sw.dualscreen.adapter.CollectFoodListAdapter import com.sw.dualscreen.adapter.CollectFoodListAdapter
import com.sw.dualscreen.adapter.FoodCollectionAdapter import com.sw.dualscreen.adapter.FoodCollectionAdapter
import com.sw.dualscreen.adapter.DialogSearchFoodAdapter
import com.sw.dualscreen.databinding.FragmentCollectBinding import com.sw.dualscreen.databinding.FragmentCollectBinding
import com.sw.dualscreen.databinding.LayoutCameraPreviewBinding import com.sw.dualscreen.databinding.LayoutCameraPreviewBinding
import com.sw.dualscreen.ext.clickWithDebounce 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.ImageUploader
import com.sw.dualscreen.utils.ImageUtil import com.sw.dualscreen.utils.ImageUtil
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import io.objectbox.Box
import io.objectbox.kotlin.boxFor
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.RequestBody import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import timber.log.Timber import timber.log.Timber
import kotlin.getValue
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
class CollectFragment : BaseFragment<FragmentCollectBinding>() { class CollectFragment : BaseFragment<FragmentCollectBinding>() {
@@ -55,13 +51,6 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
private var selectedFoodId: String? = "" private var selectedFoodId: String? = ""
private var selectedFoodName: 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 { private val foodCollectionList = mutableListOf<FoodCollectionBean>().apply {
repeat(MAX_COUNT) { repeat(MAX_COUNT) {
add(FoodCollectionBean(isShowCamera = true)) add(FoodCollectionBean(isShowCamera = true))
@@ -138,6 +127,7 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
e.printStackTrace() e.printStackTrace()
settingActivity?.hideWaitingDialog() settingActivity?.hideWaitingDialog()
ToastUtils.showToast("程序异常${e.message}") ToastUtils.showToast("程序异常${e.message}")
settingActivity?.log("程序异常${e.message}")
} }
} }
@@ -152,13 +142,14 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
ToastUtils.showToast("操作失败") ToastUtils.showToast("操作失败")
settingActivity?.log("操作失败:${e.message}")
settingActivity?.hideWaitingDialog() settingActivity?.hideWaitingDialog()
return return
} }
val file = BitmapSaver.saveToAppFilesDir( val file = BitmapSaver.saveToAppFilesDir(
bitmap, requireActivity(), "IMG_CROP_${System.currentTimeMillis()}.jpg" 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 { activity?.runOnUiThread {
foodCollectionList[index].let { foodCollectionList[index].let {
@@ -187,6 +178,7 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
settingActivity?.hideWaitingDialog() settingActivity?.hideWaitingDialog()
if (cameraErrorCount >= 10) { if (cameraErrorCount >= 10) {
ToastUtils.showToast("拍照异常,请重新操作") ToastUtils.showToast("拍照异常,请重新操作")
settingActivity?.log("拍照异常,请重新操作:$errMsg")
cameraErrorCount = 0 cameraErrorCount = 0
return@takePhoto return@takePhoto
} }
@@ -305,8 +297,7 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
version = GlobalData.foodModelVersion version = GlobalData.foodModelVersion
) )
} }
initBox() ObjectBox.putAll(foodList)
box?.put(foodList)
activity?.runOnUiThread { activity?.runOnUiThread {
batch.forEach { it.isFinish = true } batch.forEach { it.isFinish = true }
collectionAdapter.notifyDataSetChanged() collectionAdapter.notifyDataSetChanged()
@@ -54,27 +54,29 @@ class CashPayFragment : BaseFragment<FragmentCashPayBinding>() {
override fun onHiddenChanged(hidden: Boolean) { override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden) super.onHiddenChanged(hidden)
if (hidden) { if (hidden) {
delayDismiss() dismissSubScreen()
return return
} }
//showSubScreen() //showSubScreen()
} }
override fun onDestroy() { override fun onDestroy() {
delayDismiss() dismissSubScreen()
super.onDestroy() super.onDestroy()
} }
private fun delayDismiss() { private fun dismissSubScreen() {
presentation?.dismiss() presentation?.let {
//binding.root.postDelayed({ if (it.isShowing) {
// presentation?.dismiss() it.dismiss()
//}, 100) }
}
} }
private fun showSubScreen() { private fun showSubScreen() {
// 查找副屏(通常索引为1 // 查找副屏(通常索引为1
if (displays.size > 1) { if (displays.size > 1) {
dismissSubScreen()
presentation = CashPayPresentation(activity = payActivity, display = displays[1]) { presentation = CashPayPresentation(activity = payActivity, display = displays[1]) {
presentation?.dismiss() presentation?.dismiss()
}.also { }.also {
@@ -24,7 +24,7 @@ class FacePayFragment : BaseFragment<FragmentFacePayBinding>() {
// } // }
payActivity.foodInfo?.let { payActivity.foodInfo?.let {
foodName = it.foodName foodName = it.foodName
payAmount = (it.vipPrice?:0.0) * payActivity.eatNum payAmount = (it.vipPrice ?: 0.0) * payActivity.eatNum
} }
showSubScreen() showSubScreen()
} }
@@ -32,22 +32,23 @@ class FacePayFragment : BaseFragment<FragmentFacePayBinding>() {
override fun onHiddenChanged(hidden: Boolean) { override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden) super.onHiddenChanged(hidden)
if (hidden) { if (hidden) {
delayDismiss() dismissSubScreen()
return return
} }
//showSubScreen() //showSubScreen()
} }
override fun onDestroy() { override fun onDestroy() {
delayDismiss() dismissSubScreen()
super.onDestroy() super.onDestroy()
} }
private fun delayDismiss() { private fun dismissSubScreen() {
presentation?.dismiss() presentation?.let {
//binding.root.postDelayed({ if (it.isShowing) {
// presentation?.dismiss() it.dismiss()
//}, 100) }
}
} }
fun loadBitmap(frame: Bitmap) { fun loadBitmap(frame: Bitmap) {
@@ -57,6 +58,7 @@ class FacePayFragment : BaseFragment<FragmentFacePayBinding>() {
private fun showSubScreen() { private fun showSubScreen() {
// 查找副屏(通常索引为1 // 查找副屏(通常索引为1
if (displays.size > 1) { if (displays.size > 1) {
dismissSubScreen()
presentation = FacePayPresentation( presentation = FacePayPresentation(
activity = payActivity, activity = payActivity,
display = displays[1], display = displays[1],
@@ -1,14 +1,11 @@
package com.sw.dualscreen.activity.fragment.pay package com.sw.dualscreen.activity.fragment.pay
import com.sw.dualscreen.GlobalKey
import com.sw.dualscreen.activity.PayActivity import com.sw.dualscreen.activity.PayActivity
import com.sw.dualscreen.activity.fragment.BaseFragment import com.sw.dualscreen.activity.fragment.BaseFragment
import com.sw.dualscreen.databinding.FragmentNumberPayBinding import com.sw.dualscreen.databinding.FragmentNumberPayBinding
import com.sw.dualscreen.ext.format2String import com.sw.dualscreen.ext.format2String
import com.sw.dualscreen.ext.hideKeyboard import com.sw.dualscreen.ext.hideKeyboard
import com.sw.dualscreen.model.response.MemberInfo
import com.sw.dualscreen.presentation.pay.CashPayPresentation import com.sw.dualscreen.presentation.pay.CashPayPresentation
import com.sw.dualscreen.utils.SPUtil
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
class NumberPayFragment : BaseFragment<FragmentNumberPayBinding>() { class NumberPayFragment : BaseFragment<FragmentNumberPayBinding>() {
@@ -85,27 +82,29 @@ class NumberPayFragment : BaseFragment<FragmentNumberPayBinding>() {
override fun onHiddenChanged(hidden: Boolean) { override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden) super.onHiddenChanged(hidden)
if (hidden) { if (hidden) {
delayDismiss() dismissSubScreen()
return return
} }
//showSubScreen() //showSubScreen()
} }
override fun onDestroy() { override fun onDestroy() {
delayDismiss() dismissSubScreen()
super.onDestroy() super.onDestroy()
} }
private fun delayDismiss() { private fun dismissSubScreen() {
presentation?.dismiss() presentation?.let {
//binding.root.postDelayed({ if (it.isShowing) {
// presentation?.dismiss() it.dismiss()
//}, 100) }
}
} }
private fun showSubScreen() { private fun showSubScreen() {
// 查找副屏(通常索引为1 // 查找副屏(通常索引为1
if (displays.size > 1) { if (displays.size > 1) {
dismissSubScreen()
presentation = CashPayPresentation(activity = payActivity, display = displays[1]) { presentation = CashPayPresentation(activity = payActivity, display = displays[1]) {
presentation?.dismiss() presentation?.dismiss()
}.also { }.also {
@@ -283,7 +283,7 @@ class PayResultFragment : BaseFragment<FragmentPayResultBinding>() {
private val intervalExecutor by lazy { IntervalExecutor() } private val intervalExecutor by lazy { IntervalExecutor() }
private var payTaskJob: Job? = null private var payTaskJob: Job? = null
fun paySuccessCallback(callback: () -> Unit) { fun paySuccessCallback(callback: () -> Unit) {
payTaskJob = intervalExecutor.startIntervalTaskWithInitialDelay(5000, 10000) { payTaskJob = intervalExecutor.startIntervalTaskWithInitialDelay(200, 500) {
payActivity?.queryOrderState { paySuccess -> payActivity?.queryOrderState { paySuccess ->
if (paySuccess) { if (paySuccess) {
callback() callback()
@@ -5,7 +5,6 @@ import android.text.SpannedString
import android.text.style.AbsoluteSizeSpan import android.text.style.AbsoluteSizeSpan
import androidx.core.text.buildSpannedString import androidx.core.text.buildSpannedString
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.sw.dualscreen.R
import com.sw.dualscreen.activity.PayActivity import com.sw.dualscreen.activity.PayActivity
import com.sw.dualscreen.activity.fragment.BaseFragment import com.sw.dualscreen.activity.fragment.BaseFragment
import com.sw.dualscreen.ext.load import com.sw.dualscreen.ext.load
@@ -62,29 +61,31 @@ class ScanQrCodePayFragment : BaseFragment<FragmentScanQrcodePayBinding>() {
override fun onHiddenChanged(hidden: Boolean) { override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden) super.onHiddenChanged(hidden)
if (hidden) { if (hidden) {
delayDismiss() dismissSubScreen()
return return
} }
//showSubScreen() //showSubScreen()
} }
override fun onDestroy() { override fun onDestroy() {
delayDismiss() dismissSubScreen()
countDownJob?.cancel() // 自动取消订阅,防止内存泄漏 countDownJob?.cancel() // 自动取消订阅,防止内存泄漏
payTaskJob?.cancel() payTaskJob?.cancel()
super.onDestroy() super.onDestroy()
} }
private fun delayDismiss() { private fun dismissSubScreen() {
presentation?.dismiss() presentation?.let {
//binding.root.postDelayed({ if (it.isShowing) {
// presentation?.dismiss() it.dismiss()
//}, 100) }
}
} }
private fun showSubScreen() { private fun showSubScreen() {
// 查找副屏(通常索引为1 // 查找副屏(通常索引为1
if (displays.size > 1) { if (displays.size > 1) {
dismissSubScreen()
presentation = presentation =
ScanQrCodePayPresentation(activity = payActivity, type = 0, display = displays[1]) { ScanQrCodePayPresentation(activity = payActivity, type = 0, display = displays[1]) {
presentation?.dismiss() presentation?.dismiss()
@@ -123,7 +124,7 @@ class ScanQrCodePayFragment : BaseFragment<FragmentScanQrcodePayBinding>() {
private val intervalExecutor by lazy { IntervalExecutor() } private val intervalExecutor by lazy { IntervalExecutor() }
private var payTaskJob: Job? = null private var payTaskJob: Job? = null
fun paySuccessCallback(isVip: Boolean) { fun paySuccessCallback(isVip: Boolean) {
payTaskJob = intervalExecutor.startIntervalTaskWithInitialDelay(5000, 10000) { payTaskJob = intervalExecutor.startIntervalTaskWithInitialDelay(200, 500) {
payActivity.queryOrderState { paySuccess -> payActivity.queryOrderState { paySuccess ->
if (paySuccess) { if (paySuccess) {
payActivity.showPaySuccess(isVip) payActivity.showPaySuccess(isVip)
@@ -11,6 +11,8 @@ import com.sw.dualscreen.utils.ImageUtil
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import io.objectbox.Box import io.objectbox.Box
import io.objectbox.kotlin.boxFor import io.objectbox.kotlin.boxFor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.pytorch.IValue import org.pytorch.IValue
import org.pytorch.Module import org.pytorch.Module
import org.pytorch.torchvision.TensorImageUtils import org.pytorch.torchvision.TensorImageUtils
@@ -25,8 +27,7 @@ object FoodModule {
private const val THRESHOLD = 0.8 private const val THRESHOLD = 0.8
// private const val THRESHOLD = 0.0 // private const val THRESHOLD = 0.0
private var module: Module?=null private var module: Module? = null
private var box: Box<Food>?=null
// private lateinit var embeddingsList: List<List<Float>> // private lateinit var embeddingsList: List<List<Float>>
// private lateinit var labelsList: IntArray // private lateinit var labelsList: IntArray
@@ -40,25 +41,21 @@ object FoodModule {
private const val MODEL_INPUT_HEIGHT = 224 private const val MODEL_INPUT_HEIGHT = 224
@SuppressLint("SuspiciousIndentation") @SuppressLint("SuspiciousIndentation")
fun init(context: Context, block: () -> Unit = {}) { suspend fun init(context: Context, block: () -> Unit = {}) {
Thread { // Thread {}.start()
withContext(Dispatchers.IO) {
val modelPath = copyAssetToCache(context, "best_embedding_model_mobile.pt") val modelPath = copyAssetToCache(context, "best_embedding_model_mobile.pt")
// ToastUtils.showToast("modelPath==null:${modelPath == null}")
module = Module.load(modelPath) module = Module.load(modelPath)
// ToastUtils.showToast("module==null:${module == null}")
box = ObjectBox.boxStore.boxFor(Food::class)
//初始化默认重新拉取数据,先清空本地数据 //初始化默认重新拉取数据,先清空本地数据
// ObjectBox.boxStore.runInTx { val list = ObjectBox.getAll()
box?.run { if (list.isNotEmpty()) {
if (all.isNotEmpty()) { ObjectBox.removeAll()
removeAll()
}
} }
//if (box.all.isEmpty()) { //if (box.all.isEmpty()) {
// initDefFoodData(context) // initDefFoodData(context)
//} //}
block() block()
}.start() }
} }
// fun uri2FloatArray(uri: Uri): FloatArray? { // fun uri2FloatArray(uri: Uri): FloatArray? {
@@ -125,24 +122,21 @@ object FoodModule {
// return queryFoodNameScore(floatArray, queryCount) // 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() if (floatArray == null) return emptyList()
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
Timber.tag(TAG) val size = ObjectBox.getAll().filter { it.isDel.not() }.size
.d("queryFoodNameScore-已采集向量总数:${box?.all?.filter { it.isDel.not() }?.size}") Timber.tag(TAG).d("queryFoodNameScore-已采集向量总数:${size}")
//box.store.runInTx { }
val query = box?.query()
?.equal(Food_.isDel, false)
?.and()
?.nearestNeighbors(Food_.foodVector, floatArray, queryCount)
?.build()
//查询比较分数 //查询比较分数
// val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" } // val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" }
// val idScoreList = query.findIdsWithScores() // val idScoreList = query.findIdsWithScores()
val objScoreList = query?.findWithScores() val objScoreList = ObjectBox.query(floatArray, queryCount)
Timber.tag(TAG).d("idScoreList:${GsonUtils.toJson(objScoreList)}") Timber.tag(TAG).d("idScoreList:${GsonUtils.toJson(objScoreList)}")
val nameScoreList = mutableListOf<IdNameScore>() val nameScoreList = mutableListOf<IdNameScore>()
objScoreList?.forEach { objScoreList.forEach {
val food = it.get() val food = it.get()
nameScoreList.add( nameScoreList.add(
IdNameScore( IdNameScore(
@@ -153,13 +147,11 @@ object FoodModule {
) )
} }
val nameScoreData = GsonUtils.toJson(nameScoreList) val nameScoreData = GsonUtils.toJson(nameScoreList)
Timber.tag(TAG) Timber.tag(TAG).d("queryFood,耗时:${System.currentTimeMillis() - startTime},数据:$nameScoreData")
.d("queryFood,耗时:${System.currentTimeMillis() - startTime},数据:$nameScoreData")
query?.close()
return nameScoreList 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 startTime = System.currentTimeMillis()
val floatArray = bitmap2FloatArray(bitmap, false) val floatArray = bitmap2FloatArray(bitmap, false)
Timber.tag(TAG).d("bitmap2FloatArray,耗时:${System.currentTimeMillis() - startTime}") Timber.tag(TAG).d("bitmap2FloatArray,耗时:${System.currentTimeMillis() - startTime}")
@@ -17,7 +17,9 @@
package com.sw.dualscreen.objbox package com.sw.dualscreen.objbox
import android.content.Context import android.content.Context
import android.os.Environment
import android.util.Log import android.util.Log
import com.sw.plate.App
import io.objectbox.Box import io.objectbox.Box
import io.objectbox.BoxStore import io.objectbox.BoxStore
import io.objectbox.BoxStoreBuilder import io.objectbox.BoxStoreBuilder
@@ -26,12 +28,22 @@ import io.objectbox.android.ObjectBoxLiveData
import io.objectbox.config.DebugFlags import io.objectbox.config.DebugFlags
import io.objectbox.exception.DbException import io.objectbox.exception.DbException
import io.objectbox.exception.FileCorruptException import io.objectbox.exception.FileCorruptException
import io.objectbox.kotlin.boxFor
import io.objectbox.query.Query
import io.objectbox.sync.Sync 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 timber.log.Timber
import java.io.File import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.util.Date import java.util.Date
import java.util.zip.GZIPOutputStream import java.util.zip.GZIPOutputStream
import kotlin.also import kotlin.also
import kotlin.and
import kotlin.io.copyTo import kotlin.io.copyTo
import kotlin.io.inputStream import kotlin.io.inputStream
import kotlin.io.outputStream import kotlin.io.outputStream
@@ -52,8 +64,7 @@ object ObjectBox {
private const val TAG = "ObjectBox" private const val TAG = "ObjectBox"
lateinit var boxStore: BoxStore var boxStore: BoxStore? = null
private set
/** /**
* If building the [boxStore] failed, contains the thrown error message. * If building the [boxStore] failed, contains the thrown error message.
@@ -65,9 +76,9 @@ object ObjectBox {
// On Android make sure to pass a Context when building the Store. // On Android make sure to pass a Context when building the Store.
boxStore = try { boxStore = try {
MyObjectBox.builder() MyObjectBox.builder()
.androidContext(context.applicationContext) .androidContext(context.applicationContext)
.debugFlags(DebugFlags.LOG_QUERY_PARAMETERS) .debugFlags(DebugFlags.LOG_QUERY_PARAMETERS)
.build() .build()
} catch (e: DbException) { } catch (e: DbException) {
if (e.javaClass == DbException::class.java || e is FileCorruptException) { if (e.javaClass == DbException::class.java || e is FileCorruptException) {
// Failed to build BoxStore due to database file issue, store message; // Failed to build BoxStore due to database file issue, store message;
@@ -81,12 +92,12 @@ object ObjectBox {
} }
// if (BuildConfig.DEBUG) { // if (BuildConfig.DEBUG) {
val syncAvailable = if (Sync.isAvailable()) "available" else "unavailable" val syncAvailable = if (Sync.isAvailable()) "available" else "unavailable"
Timber.tag(TAG) Timber.tag(TAG)
.d("Using ObjectBox ${BoxStore.getVersion()} (${BoxStore.getVersionNative()}, sync $syncAvailable)") .d("Using ObjectBox ${BoxStore.getVersion()} (${BoxStore.getVersionNative()}, sync $syncAvailable)")
// Enable ObjectBox Admin on debug builds. // Enable ObjectBox Admin on debug builds.
// https://docs.objectbox.io/data-browser // https://docs.objectbox.io/data-browser
Admin(boxStore).start(context.applicationContext) Admin(boxStore).start(context.applicationContext)
// } // }
} }
@@ -114,42 +125,166 @@ object ObjectBox {
return true return true
} }
// /**
// * 安全获取 Food 实体 foodVector 字段的向量索引(推荐写法) // 获取指定实体的Box,自动关联BoxStore
// */ inline fun <reified T> getBox(): Box<T>? = boxStore?.boxFor()
// fun getFoodVectorIndex(box: Box<Food>): VectorIndex? {
// return try { // 协程安全执行数据库操作(推荐所有操作使用此方法)
// // 核心修复:使用自动生成的 Food_.foodVector 而非字符串,避免拼写错误 suspend fun <T> safeDbOp(operation: suspend () -> T): T? {
// val foodVectorProperty = Food_.foodVector return withContext(Dispatchers.IO) {
// // 获取向量索引(适配 ObjectBox 3.5+ 所有版本) try {
// foodVectorProperty.vectorIndex dbMutex.withLock { operation() }
// } catch (e: Exception) { } catch (e: FileCorruptException) {
// // 容错处理:打印错误信息,避免崩溃 // 操作中触发损坏,尝试重建数据库
// e.printStackTrace() //val context = boxStore.context
// null //deleteDbFiles(context)
// } init(App.getContext()!!)
// } null
// } catch (e: Exception) {
// // 刷新索引的调用示例(结合你之前的更新场景) e.printStackTrace()
// fun refreshFoodVectorIndex(box: Box<Food>) { null
// val vectorIndex = getFoodVectorIndex(box) }
// vectorIndex?.run { }
// // 同步刷新索引(解决查询异常问题) }
// refresh()
// // 等待索引构建完成(超时 5 秒,避免无限等待) suspend fun query(floatArray: FloatArray, queryCount: Int) = safeDbOp {
// waitUntilBuilt(5000) val query: Query<Food>? = getBox<Food>()?.query()
// } ?.equal(Food_.isDel, false)
// } ?.and()
// ?.nearestNeighbors(Food_.foodVector, floatArray, queryCount)
// // 更新 isDel 字段后的完整调用流程 ?.build()
// fun markFoodAsDeleted(box: Box<Food>, foodId: Long) { try {
// val food = box[foodId] query?.findWithScores()
// if (food != null) { } finally {
// food.isDel = true // 先关闭Query,释放Cursor
// box.put(food) query?.close()
// // 更新后刷新索引 }
// refreshFoodVectorIndex(box) } ?: 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( suspend fun getRestInfoFoodsByType(
foodName: String, foodName: String,
pageNum: Int = 1, pageNum: Int = 1,
pageSize:Int = 100 pageSize:Int = 50
): ApiResponse<List<FoodInfo>> { ): ApiResponse<List<FoodInfo>> {
return safeApiCall { return safeApiCall {
apiService.getRestInfoFoodsByType(param = hashMapOf( 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:contentDescription="食物图预览"
android:scaleType="centerCrop" android:scaleType="centerCrop"
android:visibility="gone" android:visibility="gone"
app:strokeColor="@android:color/transparent"
app:shapeAppearance="@style/round10dpCornerStyle" /> app:shapeAppearance="@style/round10dpCornerStyle" />
<View <View
@@ -37,7 +37,7 @@
android:layout_height="100dp" android:layout_height="100dp"
android:layout_marginHorizontal="32dp" android:layout_marginHorizontal="32dp"
android:layout_marginBottom="20dp" android:layout_marginBottom="20dp"
android:background="@drawable/bg_btn_save2" android:background="@drawable/bg_btn_confirm"
android:text="已支付确认" android:text="已支付确认"
android:enabled="true" android:enabled="true"
android:textColor="@color/white" android:textColor="@color/white"
@@ -28,6 +28,7 @@
android:layout_width="120dp" android:layout_width="120dp"
android:layout_height="120dp" android:layout_height="120dp"
android:src="@drawable/ic_avatar_default" android:src="@drawable/ic_avatar_default"
app:strokeColor="@android:color/transparent"
app:shapeAppearance="@style/CircleStyle" app:shapeAppearance="@style/CircleStyle"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
@@ -93,6 +93,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:scaleType="centerCrop" android:scaleType="centerCrop"
app:strokeColor="@android:color/transparent"
app:shapeAppearance="@style/round10dpCornerStyle" app:shapeAppearance="@style/round10dpCornerStyle"
tools:src="@mipmap/ic_launcher"/> tools:src="@mipmap/ic_launcher"/>
@@ -102,6 +103,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:scaleType="centerCrop" android:scaleType="centerCrop"
tools:src="@mipmap/ic_launcher_round" tools:src="@mipmap/ic_launcher_round"
app:strokeColor="@android:color/transparent"
app:shapeAppearance="@style/round10dpCornerStyle" app:shapeAppearance="@style/round10dpCornerStyle"
android:visibility="gone"/> android:visibility="gone"/>
@@ -124,6 +124,7 @@
android:src="@drawable/ic_avatar_default" android:src="@drawable/ic_avatar_default"
android:layout_marginTop="28dp" android:layout_marginTop="28dp"
app:shapeAppearance="@style/CircleStyle" app:shapeAppearance="@style/CircleStyle"
app:strokeColor="@android:color/transparent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />