/* * Copyright 2024 ObjectBox Ltd. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 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 import io.objectbox.android.Admin 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 import kotlin.io.use import kotlin.jvm.java import kotlin.jvm.javaClass /** * Singleton to keep BoxStore reference and provide current list of Notes Objects. * Inserts demo data if no Objects are stored. */ object ObjectBox { // 确保所有设备/版本使用相同的objectbox-models/default.json // 避免直接修改自动生成的MyObjectBox类 // 跨版本升级时使用boxStore.runInTx执行数据迁移 private const val TAG = "ObjectBox" var boxStore: BoxStore? = null /** * If building the [boxStore] failed, contains the thrown error message. */ var dbExceptionMessage: String? = null private set fun init(context: Context) { // 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() } catch (e: DbException) { if (e.javaClass == DbException::class.java || e is FileCorruptException) { // Failed to build BoxStore due to database file issue, store message; // checked in NoteListActivity to notify user. dbExceptionMessage = e.toString() return } else { // Failed to build BoxStore due to developer error. throw e } } // if (BuildConfig.DEBUG) { 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) // } } /** * If the database file is not in use, compresses (GZIP) and copies it to the given [target]. */ fun copyAndGzipDatabaseFileTo(target: File, context: Context): Boolean { if (BoxStore.isDatabaseOpen(context, null)) { // Do not copy if database file is still in use. // If it would be open, the copy will likely get corrupted // as BoxStore may currently write data to the file. Timber.tag(TAG).e("Database file is still in use, can not copy.") return false } // If a name was given when building BoxStore use that instead of the default below. val dbName = BoxStoreBuilder.DEFAULT_NAME File(context.filesDir, "objectbox/$dbName/data.mdb").inputStream().use { input -> target.parentFile?.mkdirs() GZIPOutputStream(target.outputStream()).use { output -> input.copyTo(output, DEFAULT_BUFFER_SIZE) } } return true } // 获取指定实体的Box,自动关联BoxStore inline fun getBox(): Box? = boxStore?.boxFor() // 协程安全执行数据库操作(推荐所有操作使用此方法) suspend fun 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 } finally { // 线程资源清理 boxStore?.closeThreadResources() } } } suspend fun query(floatArray: FloatArray, queryCount: Int) = safeDbOp { val query: Query? = getBox()?.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()?.get(id) } suspend fun put(entity: Food) = safeDbOp { getBox()?.put(entity) } suspend fun putAll(entities: List) = safeDbOp { getBox()?.put(entities) } suspend fun getAll() = safeDbOp { getBox()?.all } ?: emptyList() suspend fun filter(name: String?) = safeDbOp { if (!name.isNullOrBlank()) { getBox()?.query(Food_.foodName.contains(name))?.build()?.find() ?.distinctBy { it.foodName } } else { getBox()?.all?.distinctBy { it.foodName } } ?: emptyList() } ?: emptyList() suspend fun removeAll() = safeDbOp { getBox()?.removeAll() } suspend fun remove(name: String) = safeDbOp { getBox()?.query(Food_.foodName.equal(name))?.build()?.remove() } 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 { context.filesDir.canRead() && context.filesDir.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 } }