285 lines
9.7 KiB
Kotlin
285 lines
9.7 KiB
Kotlin
/*
|
|
* 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 <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
|
|
} finally {
|
|
// 线程资源清理
|
|
boxStore?.closeThreadResources()
|
|
}
|
|
}
|
|
}
|
|
|
|
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?) = safeDbOp {
|
|
if (!name.isNullOrBlank()) {
|
|
getBox<Food>()?.query(Food_.foodName.contains(name))?.build()?.find()
|
|
?.distinctBy { it.foodName }
|
|
} else {
|
|
getBox<Food>()?.all?.distinctBy { it.foodName }
|
|
} ?: emptyList()
|
|
} ?: emptyList()
|
|
|
|
suspend fun removeAll() = safeDbOp {
|
|
getBox<Food>()?.removeAll()
|
|
}
|
|
|
|
suspend fun remove(name: String) = safeDbOp {
|
|
getBox<Food>()?.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
|
|
}
|
|
|
|
} |