|
@@ -61,16 +61,29 @@ abstract class ISCSDatabase : RoomDatabase() {
|
|
|
private val logger: Logger = LoggerFactory.getLogger(ISCSDatabase::class.java)
|
|
private val logger: Logger = LoggerFactory.getLogger(ISCSDatabase::class.java)
|
|
|
private val MIGRATION_LOCK = Any()
|
|
private val MIGRATION_LOCK = Any()
|
|
|
|
|
|
|
|
|
|
+ enum class ReopenMode { NORMAL, RESTORE_STRICT }
|
|
|
|
|
+
|
|
|
// 外部存储目录路径
|
|
// 外部存储目录路径
|
|
|
val EXTERNAL_DB_FILE: File by lazy {
|
|
val EXTERNAL_DB_FILE: File by lazy {
|
|
|
File(Environment.getExternalStorageDirectory(), "ISCS/database/$DB_NAME")
|
|
File(Environment.getExternalStorageDirectory(), "ISCS/database/$DB_NAME")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ @Volatile
|
|
|
|
|
+ private var _instance: ISCSDatabase? = null
|
|
|
|
|
+
|
|
|
@JvmStatic
|
|
@JvmStatic
|
|
|
- val instance: ISCSDatabase by lazy {
|
|
|
|
|
|
|
+ val instance: ISCSDatabase
|
|
|
|
|
+ get() = _instance ?: synchronized(ISCSDatabase::class.java) {
|
|
|
|
|
+ _instance ?: buildDatabase(mode = ReopenMode.NORMAL).also { _instance = it }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 构建 Room 实例:强制使用“外部绝对路径 + SQLCipher 工厂”
|
|
|
|
|
+ */
|
|
|
|
|
+ private fun buildDatabase(mode: ReopenMode): ISCSDatabase {
|
|
|
val context = SIKCore.getApplication()
|
|
val context = SIKCore.getApplication()
|
|
|
|
|
|
|
|
- // 目录
|
|
|
|
|
|
|
+ // 目录与首启落地(保持你原逻辑)
|
|
|
val parentDir = EXTERNAL_DB_FILE.parentFile
|
|
val parentDir = EXTERNAL_DB_FILE.parentFile
|
|
|
if (parentDir != null && !parentDir.exists()) {
|
|
if (parentDir != null && !parentDir.exists()) {
|
|
|
if (!parentDir.mkdirs()) {
|
|
if (!parentDir.mkdirs()) {
|
|
@@ -79,8 +92,6 @@ abstract class ISCSDatabase : RoomDatabase() {
|
|
|
logger.info("创建目录: ${parentDir.absolutePath}")
|
|
logger.info("创建目录: ${parentDir.absolutePath}")
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // 首次落地模板库(可能是明文,也可能本来就是加密库)
|
|
|
|
|
if (!EXTERNAL_DB_FILE.exists()) {
|
|
if (!EXTERNAL_DB_FILE.exists()) {
|
|
|
runCatching {
|
|
runCatching {
|
|
|
context.assets.open("data.db").use { input ->
|
|
context.assets.open("data.db").use { input ->
|
|
@@ -89,16 +100,12 @@ abstract class ISCSDatabase : RoomDatabase() {
|
|
|
logger.info("已从 assets 复制数据库到: ${EXTERNAL_DB_FILE.absolutePath}")
|
|
logger.info("已从 assets 复制数据库到: ${EXTERNAL_DB_FILE.absolutePath}")
|
|
|
}.onFailure { e -> logger.error("复制数据库失败", e) }
|
|
}.onFailure { e -> logger.error("复制数据库失败", e) }
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // 确保成为加密库(首启自动迁移)
|
|
|
|
|
ensureCiphered(EXTERNAL_DB_FILE)
|
|
ensureCiphered(EXTERNAL_DB_FILE)
|
|
|
|
|
|
|
|
- // Room + SQLCipher
|
|
|
|
|
|
|
+ // SQLCipher Hook
|
|
|
val passphrase: ByteArray = AESConfig.defaultConfig.key()
|
|
val passphrase: ByteArray = AESConfig.defaultConfig.key()
|
|
|
val hook = object : SQLiteDatabaseHook {
|
|
val hook = object : SQLiteDatabaseHook {
|
|
|
- override fun preKey(connection: SQLiteConnection) { /* no-op */
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ override fun preKey(connection: SQLiteConnection) { /* no-op */ }
|
|
|
override fun postKey(connection: SQLiteConnection) {
|
|
override fun postKey(connection: SQLiteConnection) {
|
|
|
connection.execute("PRAGMA foreign_keys=ON", null, null)
|
|
connection.execute("PRAGMA foreign_keys=ON", null, null)
|
|
|
connection.executeForString("PRAGMA journal_mode=WAL", null, null)
|
|
connection.executeForString("PRAGMA journal_mode=WAL", null, null)
|
|
@@ -108,28 +115,33 @@ abstract class ISCSDatabase : RoomDatabase() {
|
|
|
// ✅ 修正:把 hook 传进来
|
|
// ✅ 修正:把 hook 传进来
|
|
|
val factory = SupportOpenHelperFactory(passphrase, hook, true)
|
|
val factory = SupportOpenHelperFactory(passphrase, hook, true)
|
|
|
|
|
|
|
|
- val db = Room.databaseBuilder(
|
|
|
|
|
|
|
+ val builder = Room.databaseBuilder(
|
|
|
context,
|
|
context,
|
|
|
ISCSDatabase::class.java,
|
|
ISCSDatabase::class.java,
|
|
|
- EXTERNAL_DB_FILE.absolutePath
|
|
|
|
|
|
|
+ EXTERNAL_DB_FILE.absolutePath // 这里也给绝对路径,配合上面的工厂双保险
|
|
|
)
|
|
)
|
|
|
.openHelperFactory(factory)
|
|
.openHelperFactory(factory)
|
|
|
- .setJournalMode(RoomDatabase.JournalMode.WRITE_AHEAD_LOGGING) // ✅ 显式 WAL
|
|
|
|
|
|
|
+ .setJournalMode(JournalMode.WRITE_AHEAD_LOGGING)
|
|
|
.addMigrations(*ISCSMigrations.migrationData)
|
|
.addMigrations(*ISCSMigrations.migrationData)
|
|
|
.addCallback(object : RoomDatabase.Callback() {
|
|
.addCallback(object : RoomDatabase.Callback() {
|
|
|
override fun onOpen(db: SupportSQLiteDatabase) {
|
|
override fun onOpen(db: SupportSQLiteDatabase) {
|
|
|
- // 数据库已经可读写,放行初始化
|
|
|
|
|
DbReadyGate.open()
|
|
DbReadyGate.open()
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
- .build()
|
|
|
|
|
|
|
|
|
|
- // 强制触发实际打开(避免“懒打开”延后 onOpen 回调)
|
|
|
|
|
- db.openHelper.writableDatabase
|
|
|
|
|
|
|
+ // 恢复模式下**不要** fallbackToDestructiveMigration,避免清库
|
|
|
|
|
+ if (mode == ReopenMode.NORMAL) {
|
|
|
|
|
+ // 如需保留 fallback,可在正常运行模式启用:
|
|
|
|
|
+ // builder.fallbackToDestructiveMigration()
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- db
|
|
|
|
|
|
|
+ val db = builder.build()
|
|
|
|
|
+ // 触发实际打开
|
|
|
|
|
+ db.openHelper.writableDatabase
|
|
|
|
|
+ return db
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 提供给备份/维护窗口:
|
|
* 提供给备份/维护窗口:
|
|
|
* - 先做 FULL checkpoint,尽量把 -wal 合并进主库,降低锁竞争
|
|
* - 先做 FULL checkpoint,尽量把 -wal 合并进主库,降低锁竞争
|
|
@@ -144,6 +156,8 @@ abstract class ISCSDatabase : RoomDatabase() {
|
|
|
runCatching { db.close() }
|
|
runCatching { db.close() }
|
|
|
.onSuccess { logger.info("Room 已关闭,进入维护窗口") }
|
|
.onSuccess { logger.info("Room 已关闭,进入维护窗口") }
|
|
|
.onFailure { logger.warn("关闭 Room 失败:${it.message}") }
|
|
.onFailure { logger.warn("关闭 Room 失败:${it.message}") }
|
|
|
|
|
+
|
|
|
|
|
+ try { Thread.sleep(150) } catch (_: InterruptedException) {}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|