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
@@ -17,7 +17,9 @@
package com.sw.dualscreen.objbox
import android.content.Context
import android.os.Environment
import android.util.Log
import com.sw.plate.App
import io.objectbox.Box
import io.objectbox.BoxStore
import io.objectbox.BoxStoreBuilder
@@ -26,12 +28,22 @@ import io.objectbox.android.ObjectBoxLiveData
import io.objectbox.config.DebugFlags
import io.objectbox.exception.DbException
import io.objectbox.exception.FileCorruptException
import io.objectbox.kotlin.boxFor
import io.objectbox.query.Query
import io.objectbox.sync.Sync
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.util.Date
import java.util.zip.GZIPOutputStream
import kotlin.also
import kotlin.and
import kotlin.io.copyTo
import kotlin.io.inputStream
import kotlin.io.outputStream
@@ -52,8 +64,7 @@ object ObjectBox {
private const val TAG = "ObjectBox"
lateinit var boxStore: BoxStore
private set
var boxStore: BoxStore? = null
/**
* If building the [boxStore] failed, contains the thrown error message.
@@ -65,9 +76,9 @@ object ObjectBox {
// On Android make sure to pass a Context when building the Store.
boxStore = try {
MyObjectBox.builder()
.androidContext(context.applicationContext)
.debugFlags(DebugFlags.LOG_QUERY_PARAMETERS)
.build()
.androidContext(context.applicationContext)
.debugFlags(DebugFlags.LOG_QUERY_PARAMETERS)
.build()
} catch (e: DbException) {
if (e.javaClass == DbException::class.java || e is FileCorruptException) {
// Failed to build BoxStore due to database file issue, store message;
@@ -81,12 +92,12 @@ object ObjectBox {
}
// if (BuildConfig.DEBUG) {
val syncAvailable = if (Sync.isAvailable()) "available" else "unavailable"
Timber.tag(TAG)
val syncAvailable = if (Sync.isAvailable()) "available" else "unavailable"
Timber.tag(TAG)
.d("Using ObjectBox ${BoxStore.getVersion()} (${BoxStore.getVersionNative()}, sync $syncAvailable)")
// Enable ObjectBox Admin on debug builds.
// https://docs.objectbox.io/data-browser
Admin(boxStore).start(context.applicationContext)
// Enable ObjectBox Admin on debug builds.
// https://docs.objectbox.io/data-browser
Admin(boxStore).start(context.applicationContext)
// }
}
@@ -114,42 +125,166 @@ object ObjectBox {
return true
}
// /**
// * 安全获取 Food 实体 foodVector 字段的向量索引(推荐写法)
// */
// fun getFoodVectorIndex(box: Box<Food>): VectorIndex? {
// return try {
// // 核心修复:使用自动生成的 Food_.foodVector 而非字符串,避免拼写错误
// val foodVectorProperty = Food_.foodVector
// // 获取向量索引(适配 ObjectBox 3.5+ 所有版本)
// foodVectorProperty.vectorIndex
// } catch (e: Exception) {
// // 容错处理:打印错误信息,避免崩溃
// e.printStackTrace()
// null
// }
// }
//
// // 刷新索引的调用示例(结合你之前的更新场景)
// fun refreshFoodVectorIndex(box: Box<Food>) {
// val vectorIndex = getFoodVectorIndex(box)
// vectorIndex?.run {
// // 同步刷新索引(解决查询异常问题)
// refresh()
// // 等待索引构建完成(超时 5 秒,避免无限等待)
// waitUntilBuilt(5000)
// }
// }
//
// // 更新 isDel 字段后的完整调用流程
// fun markFoodAsDeleted(box: Box<Food>, foodId: Long) {
// val food = box[foodId]
// if (food != null) {
// food.isDel = true
// box.put(food)
// // 更新后刷新索引
// refreshFoodVectorIndex(box)
// }
// }
// 获取指定实体的Box,自动关联BoxStore
inline fun <reified T> getBox(): Box<T>? = boxStore?.boxFor()
// 协程安全执行数据库操作(推荐所有操作使用此方法)
suspend fun <T> safeDbOp(operation: suspend () -> T): T? {
return withContext(Dispatchers.IO) {
try {
dbMutex.withLock { operation() }
} catch (e: FileCorruptException) {
// 操作中触发损坏,尝试重建数据库
//val context = boxStore.context
//deleteDbFiles(context)
init(App.getContext()!!)
null
} catch (e: Exception) {
e.printStackTrace()
null
}
}
}
suspend fun query(floatArray: FloatArray, queryCount: Int) = safeDbOp {
val query: Query<Food>? = getBox<Food>()?.query()
?.equal(Food_.isDel, false)
?.and()
?.nearestNeighbors(Food_.foodVector, floatArray, queryCount)
?.build()
try {
query?.findWithScores()
} finally {
// 先关闭Query,释放Cursor
query?.close()
}
} ?: emptyList()
suspend fun get(id: Long) = safeDbOp {
getBox<Food>()?.get(id)
}
suspend fun put(entity: Food) = safeDbOp {
getBox<Food>()?.put(entity)
}
suspend fun putAll(entities: List<Food>) = safeDbOp {
getBox<Food>()?.put(entities)
}
suspend fun getAll() = safeDbOp {
getBox<Food>()?.all
} ?: emptyList()
suspend fun filter(name: String?, isDistinct: Boolean = false) = safeDbOp {
val list = if (isDistinct) {
getBox<Food>()?.all?.distinctBy { it.foodName }
} else {
getBox<Food>()?.all
}
if (name.isNullOrBlank().not()) {
list?.filter { it.foodName?.contains(name) == true }
}
list
} ?: emptyList()
suspend fun removeAll() = safeDbOp {
getBox<Food>()?.removeAll()
}
suspend fun remove(name: String) = safeDbOp {
getBox<Food>()?.run {
val filterIdList = all.filter { it.foodName == name }.map { it.id }
removeByIds(filterIdList)
}
}
private val dbMutex = Mutex() // 协程并发锁,保证写入操作原子性
private const val DB_DIR_NAME = "objectbox" // ObjectBox 默认数据库目录
private const val BACKUP_DIR_NAME = "objectbox_backup" // 备份目录
// 检查存储是否可读写(操作数据库前调用)
fun isStorageAvailable(context: Context): Boolean {
return try {
val state = Environment.getExternalStorageState()
val innerDir = context.filesDir
Environment.MEDIA_MOUNTED == state && innerDir.canRead() && innerDir.canWrite()
} catch (e: Exception) {
false
}
}
// 备份数据库到应用私有目录(无权限要求,推荐)
suspend fun backupDb(context: Context): Boolean = withContext(Dispatchers.IO) {
if (!isStorageAvailable(context)) return@withContext false
val dbDir = File(context.filesDir, DB_DIR_NAME)
val backupDir = File(context.filesDir, BACKUP_DIR_NAME)
return@withContext copyDir(dbDir, backupDir)
}
// 从备份恢复数据库(恢复后会重建BoxStore)
suspend fun restoreDb(context: Context): Boolean = withContext(Dispatchers.IO) {
if (!isStorageAvailable(context)) return@withContext false
val dbDir = File(context.filesDir, DB_DIR_NAME)
val backupDir = File(context.filesDir, BACKUP_DIR_NAME)
if (!backupDir.exists()) return@withContext false
// 先关闭旧的BoxStore,删除损坏文件,再恢复备份
boxStore?.close()
deleteDbFiles(context)
val isSuccess = copyDir(backupDir, dbDir)
// 重新初始化
init(context)
return@withContext isSuccess
}
// 递归删除数据库文件
private fun deleteDbFiles(context: Context) {
val dbDir = File(context.filesDir, DB_DIR_NAME)
if (dbDir.exists()) deleteDirRecursively(dbDir)
}
// 递归删除目录
private fun deleteDirRecursively(file: File) {
if (file.isDirectory) {
file.listFiles()?.forEach { deleteDirRecursively(it) }
}
file.delete()
}
// 递归复制目录(核心备份/恢复逻辑)
private fun copyDir(srcDir: File, destDir: File): Boolean {
return try {
if (!srcDir.exists()) return false
if (!destDir.exists()) destDir.mkdirs()
srcDir.listFiles()?.forEach { srcFile ->
val destFile = File(destDir, srcFile.name)
if (srcFile.isDirectory) {
copyDir(srcFile, destFile)
} else {
copyFile(srcFile, destFile)
}
}
true
} catch (e: IOException) {
e.printStackTrace()
false
}
}
// 复制单个文件(使用NIO,高效稳定)
private fun copyFile(srcFile: File, destFile: File) {
FileInputStream(srcFile).channel.use { srcChannel ->
FileOutputStream(destFile).channel.use { destChannel ->
destChannel.transferFrom(srcChannel, 0, srcChannel.size())
}
}
}
// 关闭BoxStore(应用退出时调用,可选)
fun close() {
boxStore?.close()
boxStore = null
}
}