diff --git a/.gitignore b/.gitignore index 0ffac96..c089de2 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ local.properties .claude/ app/debug/ app/release/ +# 临时忽略 assets 下的 .pt 模型文件,正式文件就绪后取消此行 +app/src/main/assets/*.pt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 666bb40..ea2679f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -137,8 +137,8 @@ dependencies { implementation(libs.androidx.camera.extensions) //pytorch -// implementation (libs.pytorch.android) -// implementation (libs.pytorch.android.torchvision) + implementation (libs.pytorch.android) + implementation (libs.pytorch.android.torchvision) // objectbox if (isDebug) { diff --git a/app/src/main/java/com/shuwei/dish/match/base/BaseApp.kt b/app/src/main/java/com/shuwei/dish/match/base/BaseApp.kt index 9325e75..c47be71 100644 --- a/app/src/main/java/com/shuwei/dish/match/base/BaseApp.kt +++ b/app/src/main/java/com/shuwei/dish/match/base/BaseApp.kt @@ -8,6 +8,7 @@ import android.content.SharedPreferences import android.util.Log import com.shuwei.dish.match.R import com.shuwei.dish.match.db.DatabaseProvider +import com.shuwei.dish.match.objbox.ObjectBox import com.shuwei.dish.match.scale.ScaleServiceManager import com.shuwei.dish.match.ui.InitActivity.Companion.TAG import com.shuwei.dish.match.utils.AppUtil @@ -44,7 +45,7 @@ class BaseApp : Application() { // GlobalData.deviceId = "39a7abdd06b3c7ab" val filter = IntentFilter(Intent.ACTION_BOOT_COMPLETED) registerReceiver(BootReceiver(), filter) -// ObjectBox.init(this) + ObjectBox.init(this) } override fun onTerminate() { diff --git a/app/src/main/java/com/shuwei/dish/match/objbox/FoodModule.kt b/app/src/main/java/com/shuwei/dish/match/objbox/FoodModule.kt index 9a18459..9e4fb11 100644 --- a/app/src/main/java/com/shuwei/dish/match/objbox/FoodModule.kt +++ b/app/src/main/java/com/shuwei/dish/match/objbox/FoodModule.kt @@ -1,308 +1,308 @@ -//package com.shuwei.dish.match.objbox -// -//import android.content.Context -//import android.graphics.Bitmap -//import android.util.Log -//import androidx.core.graphics.scale -//import com.google.gson.Gson -//import com.google.gson.reflect.TypeToken -//import com.shuwei.dish.match.utils.AssetsTool -//import com.shuwei.dish.match.utils.LogSaveUtil -//import com.shuwei.dish.match.utils.ext.toJsonString -//import kotlinx.coroutines.Dispatchers -//import kotlinx.coroutines.withContext -//import org.pytorch.IValue -//import org.pytorch.Module -//import org.pytorch.torchvision.TensorImageUtils -//import java.io.File -//import java.io.FileOutputStream -//import java.io.IOException -//import java.io.InputStream -// -// -//object FoodModule { -// -// private lateinit var module_mobile: Module -// -// private val NO_MEAN_RGB = floatArrayOf(0.0f, 0.0f, 0.0f) -// private val NO_STD_RGB = floatArrayOf(1.0f, 1.0f, 1.0f) -// -// // 1. 定义你的模型固定输入尺寸 (根据你的tflite模型修改,比如224x224) -// private const val MODEL_INPUT_WIDTH = 300 -// private const val MODEL_INPUT_HEIGHT = 300 -// const val DEFAULT_FOOD_INDEX = -1 -// const val DEFAULT_QUERY_COUNT = 50 -// -// const val BAG_RATE = 0.05 -// -// suspend fun init(context: Context) { -// withContext(Dispatchers.IO) { -// module_mobile = Module.load(copyAssetToCache(context, "best_embedding_model_mobile.pt")) -// initDefFoodData(context) -// } -// } -// -//// fun uri2FloatArray(uri: Uri): FloatArray? { -//// return MyApp.instance?.let { context -> -//// ImageUtil.uriToBitmap(context, uri)?.let { -//// bitmap2FloatArray(it) -//// } -//// } -//// } -// -// fun bitmap2FloatArray(originBitmap: Bitmap, isRecycle: Boolean): FloatArray? { -// var rgb565Bitmap: Bitmap? = null -// try { -// val scaledBitmap = originBitmap.scale(MODEL_INPUT_WIDTH, MODEL_INPUT_HEIGHT) -// if (isRecycle) { -// originBitmap.recycle() +package com.shuwei.dish.match.objbox + +import android.content.Context +import android.graphics.Bitmap +import android.util.Log +import androidx.core.graphics.scale +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.shuwei.dish.match.utils.AssetsTool +import com.shuwei.dish.match.utils.LogSaveUtil +import com.shuwei.dish.match.utils.ext.toJsonString +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.pytorch.IValue +import org.pytorch.Module +import org.pytorch.torchvision.TensorImageUtils +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream + + +object FoodModule { + + private lateinit var module_mobile: Module + + private val NO_MEAN_RGB = floatArrayOf(0.0f, 0.0f, 0.0f) + private val NO_STD_RGB = floatArrayOf(1.0f, 1.0f, 1.0f) + + // 1. 定义你的模型固定输入尺寸 (根据你的tflite模型修改,比如224x224) + private const val MODEL_INPUT_WIDTH = 300 + private const val MODEL_INPUT_HEIGHT = 300 + const val DEFAULT_FOOD_INDEX = -1 + const val DEFAULT_QUERY_COUNT = 50 + + const val BAG_RATE = 0.05 + + suspend fun init(context: Context) { + withContext(Dispatchers.IO) { + module_mobile = Module.load(copyAssetToCache(context, "best_embedding_model_mobile.pt")) + initDefFoodData(context) + } + } + +// fun uri2FloatArray(uri: Uri): FloatArray? { +// return MyApp.instance?.let { context -> +// ImageUtil.uriToBitmap(context, uri)?.let { +// bitmap2FloatArray(it) // } -// rgb565Bitmap = scaledBitmap.copy(Bitmap.Config.RGB_565, false) -// scaledBitmap.recycle() -// val inputTensor = TensorImageUtils.bitmapToFloat32Tensor( -// rgb565Bitmap, -// NO_MEAN_RGB, // [0.485, 0.456, 0.406] TORCHVISION_NORM_MEAN_RGB -// NO_STD_RGB // [0.229, 0.224, 0.225] TORCHVISION_NORM_STD_RGB -// ) -// val outputTensor = module_mobile.forward(IValue.from(inputTensor)).toTensor() -// return outputTensor.dataAsFloatArray -// } catch (e: OutOfMemoryError) { -// e.printStackTrace() -// logInfo("bitmap2FloatArray异常:${e.message}") -// } finally { -// if (rgb565Bitmap != null && rgb565Bitmap.isRecycled.not()) { -// rgb565Bitmap.recycle() -// } -// //System.gc() -// //System.runFinalization() -// } -// return null -// } -// -//// fun queryFood(uri: Uri, queryCount: Int = DEFAULT_QUERY_COUNT): List? { -//// return uri2FloatArray(uri)?.let { -//// queryFood(it, queryCount) -//// } -//// } -// -//// /** -//// * 返回识别物品名称列表 -//// */ -//// fun queryFood(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List { -//// val floatArray = bitmap2FloatArray(bitmap) -//// return queryFood(floatArray, queryCount) -//// } -// -//// /** -//// * 返回识别物品IdNameScore对象列表 -//// */ -//// fun queryFoodNameScore(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List { -//// val floatArray = bitmap2FloatArray(bitmap) -//// return queryFoodNameScore(floatArray, queryCount) -//// } -// -// suspend fun queryFoodNameScore( -// floatArray: FloatArray?, -// queryCount: Int = DEFAULT_QUERY_COUNT -// ): List { -// if (floatArray == null) return emptyList() -//// val query: Query = box.query(Food_.foodVector.nearestNeighbors(floatArray, queryCount)).build() -//// //查询比较分数 -////// val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" } -//// var idScoreList: List -//// try { -//// idScoreList = query.findIdsWithScores(); -//// } finally { -//// // 先关闭Query,释放Cursor -//// query.close() -//// } -// -// logInfo("queryFood向量:${floatArray.slice(0 until 50).toJsonString()}") -// val idScoreList = ObjectBox.query(floatArray, queryCount) -// // 批量加载所有实体,一次DB操作替代循环单条查询 -// val foodMap = ObjectBox.getByIds(idScoreList.map { it.id }).associateBy { it.id } -// val nameScoreList = mutableListOf() -// idScoreList.forEach { -// val name = foodMap[it.id]?.name ?: "" -// nameScoreList.add(IdNameScore(id = it.id, name = name, score = it.score)) -// } -// logInfo("queryFood数据:${nameScoreList.toJsonString()}") -// return nameScoreList -// } -// -// suspend fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List { -// val floatArray = bitmap2FloatArray(bitmap, false) ?: return emptyList() -// logInfo("getFoodScoreList向量:${floatArray.toJsonString()}") -// val nameScoreList = queryFoodNameScore(floatArray, queryCount) -// if (nameScoreList.isEmpty()) { -// return emptyList() -// } -// //暂时不过滤 -// val maxScoreList = nameScoreList -// //.filter { it.score < 0.15 } -// .groupBy { it.name } -// .map { (_, value) -> value.minByOrNull { it.score }!! } -// .toMutableList() -//// val map = mutableMapOf() -//// nameScoreList.forEach { -//// val key = it.name -//// val count = map[key] ?: 0 -//// map[key] = count + 1 -//// } -//// val orderList = map.entries.sortedByDescending { it.value }.map { it.key }.toMutableList() -//// val firstFood = nameScoreList[0].name -//// orderList.remove(firstFood) -//// orderList.add(0, firstFood) -//// -//// val sortedScoreList = maxScoreList.sortedWith(compareBy { -//// orderList.indexOf(it.name) -//// }) -// val sortedScoreList = maxScoreList.sortedBy { it.score } -// logInfo("getFoodScoreList数据:${sortedScoreList.toJsonString()}") -// return sortedScoreList -// } -// -//// suspend fun queryFood(floatArray: FloatArray, queryCount: Int = DEFAULT_QUERY_COUNT): List { -//// val map = mutableMapOf() -//// val nameScoreList = queryFoodNameScore(floatArray, queryCount) -//// nameScoreList.filter { it.score < 0.05 }.forEach { -//// val count = map[it.name] ?: 0 -//// map[it.name] = count + 1 -//// } -//// val list = map.entries.sortedByDescending { it.value }.map { it.key } -//// return list -//// } -// -// data class IdNameScore( -// val id: Long, -// var name: String, -// val score: Double -// ) -// -// suspend fun initDefFoodData(context: Context, action: () -> Unit = {}) { -// try { -// val count = ObjectBox.countIndexField(DEFAULT_FOOD_INDEX) -// if (count > 0) { -// return -// } -// val embeddingsJson = AssetsTool.readAssetsFile(context, "data/embeddings.json") -// val labelsJson = AssetsTool.readAssetsFile(context, "data/labels.json") -// val classInfoJson = AssetsTool.readAssetsFile(context, "data/class_info.json") -// -// val embeddingsList: List> = -// Gson().fromJson(embeddingsJson, object : TypeToken>>() {}.type) -// val labelsList: IntArray = Gson().fromJson(labelsJson, IntArray::class.java) -// val classInfo: FoodClassInfo = -// Gson().fromJson(classInfoJson, FoodClassInfo::class.java) -// -// val foodMap = classInfo.idx_to_class -// val list = mutableListOf() -// val size = embeddingsList.size -// val size2 = labelsList.size -// val size3 = foodMap.size -// logInfo("initDefFoodData: $size,$size2,$size3") -// embeddingsList.forEachIndexed { index, floatList -> -// val classIdx = labelsList[index] -// val foodName = foodMap["$classIdx"] -// val array = floatList.toFloatArray() -// // ObjectBox.boxStore.runInTx { -// // box.put(Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX)) -// // } -// val food = Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX) -// list.add(food) -// } -//// val tempList = list.chunked(10) -// ObjectBox.putAll(list) -// action() -// } catch (e: Exception) { -// e.printStackTrace() -// logInfo("initDefFoodData: ---${e.message}--") // } // } -// -// + + fun bitmap2FloatArray(originBitmap: Bitmap, isRecycle: Boolean): FloatArray? { + var rgb565Bitmap: Bitmap? = null + try { + val scaledBitmap = originBitmap.scale(MODEL_INPUT_WIDTH, MODEL_INPUT_HEIGHT) + if (isRecycle) { + originBitmap.recycle() + } + rgb565Bitmap = scaledBitmap.copy(Bitmap.Config.RGB_565, false) + scaledBitmap.recycle() + val inputTensor = TensorImageUtils.bitmapToFloat32Tensor( + rgb565Bitmap, + NO_MEAN_RGB, // [0.485, 0.456, 0.406] TORCHVISION_NORM_MEAN_RGB + NO_STD_RGB // [0.229, 0.224, 0.225] TORCHVISION_NORM_STD_RGB + ) + val outputTensor = module_mobile.forward(IValue.from(inputTensor)).toTensor() + return outputTensor.dataAsFloatArray + } catch (e: OutOfMemoryError) { + e.printStackTrace() + logInfo("bitmap2FloatArray异常:${e.message}") + } finally { + if (rgb565Bitmap != null && rgb565Bitmap.isRecycled.not()) { + rgb565Bitmap.recycle() + } + //System.gc() + //System.runFinalization() + } + return null + } + +// fun queryFood(uri: Uri, queryCount: Int = DEFAULT_QUERY_COUNT): List? { +// return uri2FloatArray(uri)?.let { +// queryFood(it, queryCount) +// } +// } + // /** -// * ,此方法的主要目的是:从assets 拷贝到 app的cache目录 -// * @param context -// * @param fileName -// * @return 例如是这样:/data/user/0/com.frizzle.pluginhookandroid9/cache/plugin-debug.apk -// * -// * 不可能反正SD +// * 返回识别物品名称列表 // */ -//// fun copyAssetToCache(context: Context, fileName: String): String? { -//// // 此app的缓存目录 --> 会默认在 cache目录...,可以自己去看看哦 -//// val cacheDir = context.getCacheDir() -//// if (!cacheDir.exists()) { -//// cacheDir.mkdirs() // TODO 如果没有缓存目录,就创建 -//// } -//// val outPath = File(cacheDir, fileName) // TODO 创建输出的文件位置 -//// if (outPath.exists()) { -//// outPath.delete() // TODO 如果该文件已经存在,就删掉 -//// } -//// var `is`: InputStream? = null // 读取 -//// var fos: FileOutputStream? = null // 写入 -//// try { -//// // 创建文件,如果创建成功,就返回true -//// val res = outPath.createNewFile() -//// if (res) { -//// `is` = context.getAssets().open(fileName) // 拿到main/assets目录的输入流,用于读取字节 -//// fos = FileOutputStream(outPath) // 读取出来的字节最终写到outPath -//// val buf = ByteArray(`is`.available()) // 缓存区 -//// var byteCount: Int -//// -//// // 开始循环读取 -//// while ((`is`.read(buf).also { byteCount = it }) != -1) { -//// fos.write(buf, 0, byteCount) -//// } -//// return outPath.getAbsolutePath() -//// } -//// } catch (e: IOException) { -//// e.printStackTrace() -//// } finally { -//// try { -//// // TODO 一定要记得关闭资源,为了不去性能的磨损 -//// fos?.flush() -//// `is`?.close() -//// fos?.close() -//// } catch (e: IOException) { -//// e.printStackTrace() -//// } -//// } -//// return null -//// } -// -// fun copyAssetToCache(context: Context, fileName: String): String? { -// val cacheFile = File(context.cacheDir, fileName) -// val buffer = ByteArray(8 * 1024) -// var inputStream: InputStream? = null -// var outputStream: FileOutputStream? = null -// +// fun queryFood(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List { +// val floatArray = bitmap2FloatArray(bitmap) +// return queryFood(floatArray, queryCount) +// } + +// /** +// * 返回识别物品IdNameScore对象列表 +// */ +// fun queryFoodNameScore(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List { +// val floatArray = bitmap2FloatArray(bitmap) +// return queryFoodNameScore(floatArray, queryCount) +// } + + suspend fun queryFoodNameScore( + floatArray: FloatArray?, + queryCount: Int = DEFAULT_QUERY_COUNT + ): List { + if (floatArray == null) return emptyList() +// val query: Query = box.query(Food_.foodVector.nearestNeighbors(floatArray, queryCount)).build() +// //查询比较分数 +//// val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" } +// var idScoreList: List // try { -// inputStream = context.assets.open(fileName) -// outputStream = FileOutputStream(cacheFile) -// var byteCount: Int -// while (inputStream.read(buffer).also { byteCount = it } != -1) { -// outputStream.write(buffer, 0, byteCount) +// idScoreList = query.findIdsWithScores(); +// } finally { +// // 先关闭Query,释放Cursor +// query.close() +// } + + logInfo("queryFood向量:${floatArray.slice(0 until 50).toJsonString()}") + val idScoreList = ObjectBox.query(floatArray, queryCount) + // 批量加载所有实体,一次DB操作替代循环单条查询 + val foodMap = ObjectBox.getByIds(idScoreList.map { it.id }).associateBy { it.id } + val nameScoreList = mutableListOf() + idScoreList.forEach { + val name = foodMap[it.id]?.name ?: "" + nameScoreList.add(IdNameScore(id = it.id, name = name, score = it.score)) + } + logInfo("queryFood数据:${nameScoreList.toJsonString()}") + return nameScoreList + } + + suspend fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List { + val floatArray = bitmap2FloatArray(bitmap, false) ?: return emptyList() + logInfo("getFoodScoreList向量:${floatArray.toJsonString()}") + val nameScoreList = queryFoodNameScore(floatArray, queryCount) + if (nameScoreList.isEmpty()) { + return emptyList() + } + //暂时不过滤 + val maxScoreList = nameScoreList + //.filter { it.score < 0.15 } + .groupBy { it.name } + .map { (_, value) -> value.minByOrNull { it.score }!! } + .toMutableList() +// val map = mutableMapOf() +// nameScoreList.forEach { +// val key = it.name +// val count = map[key] ?: 0 +// map[key] = count + 1 +// } +// val orderList = map.entries.sortedByDescending { it.value }.map { it.key }.toMutableList() +// val firstFood = nameScoreList[0].name +// orderList.remove(firstFood) +// orderList.add(0, firstFood) +// +// val sortedScoreList = maxScoreList.sortedWith(compareBy { +// orderList.indexOf(it.name) +// }) + val sortedScoreList = maxScoreList.sortedBy { it.score } + logInfo("getFoodScoreList数据:${sortedScoreList.toJsonString()}") + return sortedScoreList + } + +// suspend fun queryFood(floatArray: FloatArray, queryCount: Int = DEFAULT_QUERY_COUNT): List { +// val map = mutableMapOf() +// val nameScoreList = queryFoodNameScore(floatArray, queryCount) +// nameScoreList.filter { it.score < 0.05 }.forEach { +// val count = map[it.name] ?: 0 +// map[it.name] = count + 1 +// } +// val list = map.entries.sortedByDescending { it.value }.map { it.key } +// return list +// } + + data class IdNameScore( + val id: Long, + var name: String, + val score: Double + ) + + suspend fun initDefFoodData(context: Context, action: () -> Unit = {}) { + try { + val count = ObjectBox.countIndexField(DEFAULT_FOOD_INDEX) + if (count > 0) { + return + } + val embeddingsJson = AssetsTool.readAssetsFile(context, "data/embeddings.json") + val labelsJson = AssetsTool.readAssetsFile(context, "data/labels.json") + val classInfoJson = AssetsTool.readAssetsFile(context, "data/class_info.json") + + val embeddingsList: List> = + Gson().fromJson(embeddingsJson, object : TypeToken>>() {}.type) + val labelsList: IntArray = Gson().fromJson(labelsJson, IntArray::class.java) + val classInfo: FoodClassInfo = + Gson().fromJson(classInfoJson, FoodClassInfo::class.java) + + val foodMap = classInfo.idx_to_class + val list = mutableListOf() + val size = embeddingsList.size + val size2 = labelsList.size + val size3 = foodMap.size + logInfo("initDefFoodData: $size,$size2,$size3") + embeddingsList.forEachIndexed { index, floatList -> + val classIdx = labelsList[index] + val foodName = foodMap["$classIdx"] + val array = floatList.toFloatArray() + // ObjectBox.boxStore.runInTx { + // box.put(Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX)) + // } + val food = Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX) + list.add(food) + } +// val tempList = list.chunked(10) + ObjectBox.putAll(list) + action() + } catch (e: Exception) { + e.printStackTrace() + logInfo("initDefFoodData: ---${e.message}--") + } + } + + + /** + * ,此方法的主要目的是:从assets 拷贝到 app的cache目录 + * @param context + * @param fileName + * @return 例如是这样:/data/user/0/com.frizzle.pluginhookandroid9/cache/plugin-debug.apk + * + * 不可能反正SD + */ +// fun copyAssetToCache(context: Context, fileName: String): String? { +// // 此app的缓存目录 --> 会默认在 cache目录...,可以自己去看看哦 +// val cacheDir = context.getCacheDir() +// if (!cacheDir.exists()) { +// cacheDir.mkdirs() // TODO 如果没有缓存目录,就创建 +// } +// val outPath = File(cacheDir, fileName) // TODO 创建输出的文件位置 +// if (outPath.exists()) { +// outPath.delete() // TODO 如果该文件已经存在,就删掉 +// } +// var `is`: InputStream? = null // 读取 +// var fos: FileOutputStream? = null // 写入 +// try { +// // 创建文件,如果创建成功,就返回true +// val res = outPath.createNewFile() +// if (res) { +// `is` = context.getAssets().open(fileName) // 拿到main/assets目录的输入流,用于读取字节 +// fos = FileOutputStream(outPath) // 读取出来的字节最终写到outPath +// val buf = ByteArray(`is`.available()) // 缓存区 +// var byteCount: Int +// +// // 开始循环读取 +// while ((`is`.read(buf).also { byteCount = it }) != -1) { +// fos.write(buf, 0, byteCount) +// } +// return outPath.getAbsolutePath() // } -// outputStream.channel.force(true) // 强制物理落盘,比flush更彻底 -// return cacheFile.absolutePath // } catch (e: IOException) { // e.printStackTrace() -// logInfo("文件拷贝失败 fileName=$fileName, error=${e.message}") -// // 拷贝失败时删除残缺文件,避免下次读取到损坏文件 -// if (cacheFile.exists()) { -// cacheFile.delete() -// } // } finally { -// outputStream?.close() -// inputStream?.close() +// try { +// // TODO 一定要记得关闭资源,为了不去性能的磨损 +// fos?.flush() +// `is`?.close() +// fos?.close() +// } catch (e: IOException) { +// e.printStackTrace() +// } // } // return null // } -// -// private fun logInfo(msg:String) { -// Log.d("FoodModule", msg) -// LogSaveUtil.saveLogFile(msg) -// } -// -//} \ No newline at end of file + + fun copyAssetToCache(context: Context, fileName: String): String? { + val cacheFile = File(context.cacheDir, fileName) + val buffer = ByteArray(8 * 1024) + var inputStream: InputStream? = null + var outputStream: FileOutputStream? = null + + try { + inputStream = context.assets.open(fileName) + outputStream = FileOutputStream(cacheFile) + var byteCount: Int + while (inputStream.read(buffer).also { byteCount = it } != -1) { + outputStream.write(buffer, 0, byteCount) + } + outputStream.channel.force(true) // 强制物理落盘,比flush更彻底 + return cacheFile.absolutePath + } catch (e: IOException) { + e.printStackTrace() + logInfo("文件拷贝失败 fileName=$fileName, error=${e.message}") + // 拷贝失败时删除残缺文件,避免下次读取到损坏文件 + if (cacheFile.exists()) { + cacheFile.delete() + } + } finally { + outputStream?.close() + inputStream?.close() + } + return null + } + + private fun logInfo(msg:String) { + Log.d("FoodModule", msg) + LogSaveUtil.saveLogFile(msg) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/shuwei/dish/match/ui/InitActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/InitActivity.kt index 1dcecc9..28a1b8e 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/InitActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/InitActivity.kt @@ -18,6 +18,7 @@ import com.shuwei.dish.match.base.DeviceRole import com.shuwei.dish.match.base.GlobalData import com.shuwei.dish.match.databinding.ActivityInitBinding import com.shuwei.dish.match.db.AppRepository +import com.shuwei.dish.match.objbox.FoodModule import com.shuwei.dish.match.utils.AppUtil import com.shuwei.dish.match.utils.NetworkUtils import com.shuwei.dish.match.utils.SpTool @@ -47,9 +48,9 @@ class InitActivity : BaseActivity() { // useBackground(false) binding = ActivityInitBinding.inflate(layoutInflater) setContentView(binding.root) -// lifecycleScope.launch { -// FoodModule.init(this@InitActivity) -// } + lifecycleScope.launch { + FoodModule.init(this@InitActivity) + } // setHeaderBackground(isHomePage = true) setHeaderBgVisible(false) BaseApp.appVersion = AppUtil.getAppVersionCode(this).toString() diff --git a/app/src/main/java/com/shuwei/dish/match/ui/MasterScaleActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/MasterScaleActivity.kt index d708ecf..c18d502 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/MasterScaleActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/MasterScaleActivity.kt @@ -52,6 +52,7 @@ class MasterScaleActivity : BaseActivity() { setContentView(binding.root) setHeaderBackground() + binding.ivMasterBack.setOnClickListener { finish() } binding.rvScaleList.layoutManager = LinearLayoutManager(this) binding.rvScaleList.itemAnimator = null // 关闭默认动画,避免数据频繁更新时出现闪烁错乱 binding.rvScaleList.adapter = adapter diff --git a/app/src/main/java/com/shuwei/dish/match/ui/fragment/CollectFragment.kt b/app/src/main/java/com/shuwei/dish/match/ui/fragment/CollectFragment.kt index ffcf1c4..25177cc 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/fragment/CollectFragment.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/fragment/CollectFragment.kt @@ -17,6 +17,7 @@ import com.shuwei.dish.match.base.BaseFragment import com.shuwei.dish.match.databinding.FragmentCollectBinding import com.shuwei.dish.match.dialog.Loading import com.shuwei.dish.match.objbox.FoodCollectionBean +import com.shuwei.dish.match.objbox.FoodModule import com.shuwei.dish.match.ui.SettingActivity import com.shuwei.dish.match.utils.BitmapSaver import com.shuwei.dish.match.utils.Debouncer @@ -119,38 +120,38 @@ class CollectFragment : BaseFragment() { } private fun getImageVector(index: Int, bitmap: Bitmap) { -//// val bitmap = BitmapCropper.cropCenter( -//// original = srcBmp, -//// targetWidth = 900, targetHeight = 900, -////// offsetX = 30, offsetY = 100 -//// ) -// val imageVector = try { -// FoodModule.bitmap2FloatArray(bitmap, false) -// } catch (e: Exception) { -// e.printStackTrace() -// toast("操作失败") -// log("操作失败:${e.message}") -// hideWaitingDialog() -// return -// } -// val file = BitmapSaver.saveToAppFilesDir( -// bitmap, requireActivity(), "IMG_CROP_${System.currentTimeMillis()}.jpg" +// val bitmap = BitmapCropper.cropCenter( +// original = srcBmp, +// targetWidth = 900, targetHeight = 900, +//// offsetX = 30, offsetY = 100 // ) -// log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}") -// -// activity?.runOnUiThread { -// foodCollectionList[index].let { -// it.imageVector = imageVector -// it.bitmap = null -// it.isShowCamera = false -// it.imageFile = file -// } -// collectionAdapter.notifyItemChanged(index) -// } -// if (bitmap.isRecycled.not()) { -// bitmap.recycle() -// } -// hideWaitingDialog() + val imageVector = try { + FoodModule.bitmap2FloatArray(bitmap, false) + } catch (e: Exception) { + e.printStackTrace() + toast("操作失败") + log("操作失败:${e.message}") + hideWaitingDialog() + return + } + val file = BitmapSaver.saveToAppFilesDir( + bitmap, requireActivity(), "IMG_CROP_${System.currentTimeMillis()}.jpg" + ) + log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}") + + activity?.runOnUiThread { + foodCollectionList[index].let { + it.imageVector = imageVector + it.bitmap = null + it.isShowCamera = false + it.imageFile = file + } + collectionAdapter.notifyItemChanged(index) + } + if (bitmap.isRecycled.not()) { + bitmap.recycle() + } + hideWaitingDialog() } @SuppressLint("NotifyDataSetChanged") diff --git a/app/src/main/res/layout/activity_master_scale.xml b/app/src/main/res/layout/activity_master_scale.xml index 86a2c62..f35b3e7 100644 --- a/app/src/main/res/layout/activity_master_scale.xml +++ b/app/src/main/res/layout/activity_master_scale.xml @@ -1,19 +1,23 @@ - @@ -23,20 +27,36 @@ android:id="@+id/tvConnectionStatus" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="center_horizontal" + app:layout_constraintTop_toBottomOf="@id/tvMasterTitle" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" android:layout_marginBottom="16dp" android:textColor="@color/home_sub_title" android:textSize="24sp" tools:text="已发现设备:3 台" /> + + - +