diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index ea2679f..666bb40 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/release/菜品配比终端v1.0-release.apk b/app/release/菜品配比终端v1.0-release.apk
deleted file mode 100644
index ea09e3c..0000000
Binary files a/app/release/菜品配比终端v1.0-release.apk and /dev/null differ
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 23ff004..13947e9 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -21,8 +21,6 @@
-
-
+ android:exported="true">
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/assets/best_embedding_model_mobile.pt b/app/src/main/assets/best_embedding_model_mobile.pt
deleted file mode 100644
index f1464da..0000000
Binary files a/app/src/main/assets/best_embedding_model_mobile.pt and /dev/null differ
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 e403b33..fc602e8 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,8 +8,6 @@ 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.FoodModule
-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
@@ -42,7 +40,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 9e4fb11..9a18459 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()
- }
- 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)
+//package com.shuwei.dish.match.objbox
//
-// 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
+//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
//
-// // 开始循环读取
-// while ((`is`.read(buf).also { byteCount = it }) != -1) {
-// fos.write(buf, 0, byteCount)
-// }
-// return outPath.getAbsolutePath()
+//
+//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()
// }
-// } catch (e: IOException) {
+// 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 {
-// try {
-// // TODO 一定要记得关闭资源,为了不去性能的磨损
-// fos?.flush()
-// `is`?.close()
-// fos?.close()
-// } catch (e: IOException) {
-// e.printStackTrace()
+// if (rgb565Bitmap != null && rgb565Bitmap.isRecycled.not()) {
+// rgb565Bitmap.recycle()
// }
+// //System.gc()
+// //System.runFinalization()
// }
// 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
-
- 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
+//
+//// 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}--")
+// }
+// }
+//
+//
+// /**
+// * ,此方法的主要目的是:从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
+//
+// 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/scale/ScaleServiceManager.kt b/app/src/main/java/com/shuwei/dish/match/scale/ScaleServiceManager.kt
index 4f097a5..4e25194 100644
--- a/app/src/main/java/com/shuwei/dish/match/scale/ScaleServiceManager.kt
+++ b/app/src/main/java/com/shuwei/dish/match/scale/ScaleServiceManager.kt
@@ -31,9 +31,11 @@ object ScaleServiceManager {
private var mdnsRegister: MdnsRegisterManager? = null
private var wsServer: ScaleWebSocketServer? = null
+ private var udpSender: UdpBroadcastSender? = null
// 以下仅主设备使用
private var mdnsDiscovery: MdnsDiscoveryManager? = null
+ private var udpReceiver: UdpBroadcastReceiver? = null
private var wsClient: ScaleWebSocketClient? = null
private var aggregator: ScaleDataAggregator? = null
@@ -66,6 +68,10 @@ object ScaleServiceManager {
if (role == DeviceRole.MASTER) {
startMasterServices(context, deviceId)
+ } else {
+ // 子设备:启动 UDP 广播,作为 mDNS 的兜底发现机制
+ val localIp = getLocalIp(context)
+ udpSender = UdpBroadcastSender(deviceId, localIp).also { it.start() }
}
}
@@ -93,24 +99,35 @@ object ScaleServiceManager {
}
wsClient = client
+ // 统一的设备发现处理:mDNS 和 UDP 共用同一逻辑
+ fun onDeviceFound(remoteId: String, host: String, port: Int) {
+ if (remoteId == deviceId) return
+ Log.d(TAG, "发现子设备 $remoteId ($host:$port), 建立连接")
+ aggregator?.setDeviceIp(remoteId, host)
+ client.connect(remoteId, host, port)
+ }
+
// mDNS 发现:发现子设备 → 建立 WebSocket 连接;设备离线 → 断开连接并清除数据
mdnsDiscovery = MdnsDiscoveryManager(context).also { discovery ->
discovery.onDeviceFound = { remoteId, host, port ->
- // 不连接自身
- if (remoteId != deviceId) {
- Log.d(TAG, "发现子设备 $remoteId, 建立连接")
- // 记录子设备 IP,供 UI 展示使用
- aggregator?.setDeviceIp(remoteId, host)
- client.connect(remoteId, host, port)
- }
+ onDeviceFound(remoteId, host, port)
}
discovery.onDeviceLost = { remoteId ->
Log.d(TAG, "子设备离线 $remoteId, 断开连接并清除数据")
client.disconnect(remoteId)
aggregator?.removeDevice(remoteId)
+ udpReceiver?.removeDevice(remoteId)
}
discovery.startDiscovery()
}
+
+ // UDP 广播接收:作为 mDNS 的兜底,发现 mDNS 未能触发的子设备
+ udpReceiver = UdpBroadcastReceiver().also { receiver ->
+ receiver.onDeviceFound = { remoteId, host, port ->
+ onDeviceFound(remoteId, host, port)
+ }
+ receiver.start()
+ }
}
/**
@@ -118,12 +135,16 @@ object ScaleServiceManager {
*/
fun stop() {
mdnsDiscovery?.stopDiscovery()
+ udpReceiver?.stop()
+ udpSender?.stop()
wsClient?.shutdown()
aggregator?.stop()
wsServer?.stop()
mdnsRegister?.unregister()
mdnsDiscovery = null
+ udpReceiver = null
+ udpSender = null
wsClient = null
aggregator = null
wsServer = null
diff --git a/app/src/main/java/com/shuwei/dish/match/scale/UdpBroadcastReceiver.kt b/app/src/main/java/com/shuwei/dish/match/scale/UdpBroadcastReceiver.kt
new file mode 100644
index 0000000..379e6a3
--- /dev/null
+++ b/app/src/main/java/com/shuwei/dish/match/scale/UdpBroadcastReceiver.kt
@@ -0,0 +1,95 @@
+package com.shuwei.dish.match.scale
+
+import android.util.Log
+import com.google.gson.Gson
+import java.net.DatagramPacket
+import java.net.DatagramSocket
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.Executors
+
+/**
+ * UDP 广播接收器(主设备运行)
+ * 监听子设备发来的 UDP 广播包,解析后通过回调通知连接管理器
+ * 与 mDNS 互为备份,任意一条路发现子设备均可建立 WebSocket 连接
+ */
+class UdpBroadcastReceiver {
+
+ companion object {
+ private const val TAG = "UdpBroadcastReceiver"
+ private const val BUFFER_SIZE = 1024
+ }
+
+ private val gson = Gson()
+ private val executor = Executors.newSingleThreadExecutor()
+
+ /** 已发现的设备集合,避免重复触发回调,key=deviceId, value="host:port" */
+ private val knownDevices = ConcurrentHashMap()
+
+ @Volatile
+ private var running = false
+ private var socket: DatagramSocket? = null
+
+ /**
+ * 发现新设备时的回调:(deviceId, host, port)
+ * 与 MdnsDiscoveryManager.onDeviceFound 签名一致,可共用同一处理逻辑
+ */
+ var onDeviceFound: ((deviceId: String, host: String, port: Int) -> Unit)? = null
+
+ /**
+ * 开始监听 UDP 广播
+ */
+ fun start() {
+ if (running) return
+ running = true
+ executor.execute { listenLoop() }
+ Log.d(TAG, "UDP 广播接收已启动, 监听端口=${UdpBroadcastSender.BROADCAST_PORT}")
+ }
+
+ private fun listenLoop() {
+ try {
+ socket = DatagramSocket(UdpBroadcastSender.BROADCAST_PORT)
+ val buf = ByteArray(BUFFER_SIZE)
+ while (running) {
+ val packet = DatagramPacket(buf, buf.size)
+ socket?.receive(packet) ?: break
+ val json = String(packet.data, 0, packet.length, Charsets.UTF_8)
+ handlePacket(json)
+ }
+ } catch (e: Exception) {
+ if (running) Log.w(TAG, "UDP 接收异常: ${e.message}")
+ } finally {
+ socket?.close()
+ socket = null
+ }
+ }
+
+ private fun handlePacket(json: String) {
+ try {
+ val payload = gson.fromJson(json, UdpBroadcastSender.Payload::class.java)
+ val key = "${payload.ip}:${payload.port}"
+ // 同一设备已通知过则跳过,避免每5秒重复触发连接
+ if (knownDevices.put(payload.deviceId, key) == key) return
+ Log.d(TAG, "UDP 发现设备: ${payload.deviceId} -> ${payload.ip}:${payload.port}")
+ onDeviceFound?.invoke(payload.deviceId, payload.ip, payload.port)
+ } catch (e: Exception) {
+ Log.w(TAG, "解析 UDP 广播包失败: ${e.message}")
+ }
+ }
+
+ /**
+ * 移除已知设备记录,设备离线时调用,确保设备重新上线后能再次触发回调
+ */
+ fun removeDevice(deviceId: String) {
+ knownDevices.remove(deviceId)
+ }
+
+ /**
+ * 停止监听,释放资源
+ */
+ fun stop() {
+ running = false
+ socket?.close()
+ executor.shutdownNow()
+ Log.d(TAG, "UDP 广播接收已停止")
+ }
+}
diff --git a/app/src/main/java/com/shuwei/dish/match/scale/UdpBroadcastSender.kt b/app/src/main/java/com/shuwei/dish/match/scale/UdpBroadcastSender.kt
new file mode 100644
index 0000000..0eab2d6
--- /dev/null
+++ b/app/src/main/java/com/shuwei/dish/match/scale/UdpBroadcastSender.kt
@@ -0,0 +1,76 @@
+package com.shuwei.dish.match.scale
+
+import android.util.Log
+import com.google.gson.Gson
+import java.net.DatagramPacket
+import java.net.DatagramSocket
+import java.net.InetAddress
+import java.util.concurrent.Executors
+import java.util.concurrent.ScheduledFuture
+import java.util.concurrent.TimeUnit
+
+/**
+ * UDP 广播发送器(子设备运行)
+ * 每隔固定间隔向局域网广播本机的 deviceId、IP、WebSocket 端口
+ * 作为 mDNS 的兜底发现机制,确保主设备能可靠发现子设备
+ */
+class UdpBroadcastSender(
+ private val deviceId: String,
+ private val localIp: String,
+ private val wsPort: Int = MdnsRegisterManager.WS_PORT
+) {
+
+ companion object {
+ private const val TAG = "UdpBroadcastSender"
+ /** UDP 广播目标端口,与 UdpBroadcastReceiver 保持一致 */
+ const val BROADCAST_PORT = 8766
+ /** 广播间隔(秒) */
+ private const val INTERVAL_SEC = 5L
+ }
+
+ private val gson = Gson()
+ private val scheduler = Executors.newSingleThreadScheduledExecutor()
+ private var task: ScheduledFuture<*>? = null
+
+ /** 广播数据包结构 */
+ data class Payload(val deviceId: String, val ip: String, val port: Int)
+
+ /**
+ * 开始定时广播
+ */
+ fun start() {
+ if (task != null) return
+ task = scheduler.scheduleWithFixedDelay({
+ sendBroadcast()
+ }, 0, INTERVAL_SEC, TimeUnit.SECONDS)
+ Log.d(TAG, "UDP 广播已启动, deviceId=$deviceId, ip=$localIp, port=$wsPort")
+ }
+
+ private fun sendBroadcast() {
+ try {
+ val payload = gson.toJson(Payload(deviceId, localIp, wsPort))
+ val data = payload.toByteArray(Charsets.UTF_8)
+ DatagramSocket().use { socket ->
+ socket.broadcast = true
+ val packet = DatagramPacket(
+ data, data.size,
+ InetAddress.getByName("255.255.255.255"),
+ BROADCAST_PORT
+ )
+ socket.send(packet)
+ }
+ } catch (e: Exception) {
+ Log.w(TAG, "UDP 广播发送失败: ${e.message}")
+ }
+ }
+
+ /**
+ * 停止广播,释放资源
+ */
+ fun stop() {
+ task?.cancel(false)
+ task = null
+ scheduler.shutdownNow()
+ Log.d(TAG, "UDP 广播已停止")
+ }
+}
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 cc5bfe7..91ae3d1 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
@@ -12,7 +12,6 @@ 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.http.UrlConfig
-import com.shuwei.dish.match.objbox.FoodModule
import com.shuwei.dish.match.utils.AppUtil
import com.shuwei.dish.match.utils.QRCodeUtil
import com.shuwei.dish.match.utils.SpTool
@@ -42,9 +41,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/fragment/CollectFragment.kt b/app/src/main/java/com/shuwei/dish/match/ui/fragment/CollectFragment.kt
index 25177cc..ffcf1c4 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,7 +17,6 @@ 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
@@ -120,38 +119,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 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 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()
+// 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/java/com/shuwei/dish/match/utils/BootReceiver.kt b/app/src/main/java/com/shuwei/dish/match/utils/BootReceiver.kt
index a2a5ee3..35f6830 100644
--- a/app/src/main/java/com/shuwei/dish/match/utils/BootReceiver.kt
+++ b/app/src/main/java/com/shuwei/dish/match/utils/BootReceiver.kt
@@ -3,25 +3,15 @@ package com.shuwei.dish.match.utils
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
+import com.shuwei.dish.match.ui.InitActivity
class BootReceiver : BroadcastReceiver() {
-
- companion object {
- /** 兼容多种设备的开机广播 action 集合 */
- private val BOOT_ACTIONS = setOf(
- Intent.ACTION_BOOT_COMPLETED,
- "android.intent.action.QUICKBOOT_POWERON",
- "android.intent.action.LOCKED_BOOT_COMPLETED"
- )
- }
-
override fun onReceive(context: Context, intent: Intent) {
- val action = intent.action ?: return
- if (action in BOOT_ACTIONS) {
- // Android 10+ 禁止在后台直接启动 Activity
- // 改为启动前台服务,由 BootService 负责拉起 InitActivity
- val serviceIntent = Intent(context, BootService::class.java)
- context.startForegroundService(serviceIntent)
+ if (Intent.ACTION_BOOT_COMPLETED == intent.action) {
+ // 启动服务或Activity
+ val launchIntent = Intent(context, InitActivity::class.java)
+ launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ context.startActivity(launchIntent)
}
}
}
diff --git a/app/src/main/java/com/shuwei/dish/match/utils/BootService.kt b/app/src/main/java/com/shuwei/dish/match/utils/BootService.kt
deleted file mode 100644
index 7275c18..0000000
--- a/app/src/main/java/com/shuwei/dish/match/utils/BootService.kt
+++ /dev/null
@@ -1,95 +0,0 @@
-package com.shuwei.dish.match.utils
-
-import android.app.ActivityManager
-import android.app.Notification
-import android.app.NotificationChannel
-import android.app.NotificationManager
-import android.app.PendingIntent
-import android.app.Service
-import android.content.Intent
-import android.content.pm.ServiceInfo
-import android.os.Build
-import android.os.IBinder
-import com.shuwei.dish.match.R
-import com.shuwei.dish.match.ui.InitActivity
-
-/**
- * 开机自启中转前台服务
- * 用于绕过 Android 10+ 对后台直接启动 Activity 的限制
- * 执行流程:BootReceiver → startForegroundService() → 本服务 → setFullScreenIntent → InitActivity
- */
-class BootService : Service() {
-
- companion object {
- /** 通知渠道 ID */
- private const val CHANNEL_ID = "boot_channel"
- /** 前台通知 ID */
- private const val NOTIFICATION_ID = 1001
- }
-
- override fun onBind(intent: Intent?): IBinder? = null
-
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- // 若 App 进程已在前台运行(主设备系统签名场景),则不重复启动,避免闪退
- val activityManager = getSystemService(ACTIVITY_SERVICE) as ActivityManager
- val isAppForeground = activityManager.runningAppProcesses?.any {
- it.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
- && it.processName == packageName
- } ?: false
-
- // App 未运行时才设置 fullScreenIntent,由系统自动启动 Activity
- // 直接调用 startActivity() 在部分 Android 10 设备上会被 ActivityTaskManager 拦截
- val pendingIntent = if (!isAppForeground) createLaunchPendingIntent() else null
-
- // Android 14+ startForeground 需要传入 foregroundServiceType
- val notification = buildNotification(pendingIntent)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
- startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
- } else {
- startForeground(NOTIFICATION_ID, notification)
- }
-
- stopSelf()
- return START_NOT_STICKY
- }
-
- /** 创建启动 InitActivity 的 PendingIntent */
- private fun createLaunchPendingIntent(): PendingIntent {
- val launchIntent = Intent(this, InitActivity::class.java).apply {
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- }
- return PendingIntent.getActivity(
- this, 0, launchIntent,
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
- )
- }
-
- /**
- * 构建前台服务通知
- * fullScreenIntent 不为空时设置全屏意图,系统会在显示通知时自动启动 Activity
- * 通知渠道必须为 IMPORTANCE_HIGH,否则 fullScreenIntent 不会触发
- */
- private fun buildNotification(fullScreenIntent: PendingIntent?): Notification {
- val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
- val channel = NotificationChannel(
- CHANNEL_ID,
- "开机自启动",
- NotificationManager.IMPORTANCE_HIGH
- ).apply {
- description = "应用开机自动启动通知渠道"
- setShowBadge(false)
- }
- manager.createNotificationChannel(channel)
-
- return Notification.Builder(this, CHANNEL_ID)
- .setContentTitle("正在启动应用")
- .setSmallIcon(R.mipmap.ic_logo512)
- .apply {
- if (fullScreenIntent != null) {
- setFullScreenIntent(fullScreenIntent, true)
- }
- }
- .build()
- }
-}
-