mirror of
https://github.com/oliexdev/openScale.git
synced 2025-08-19 23:12:12 +02:00
Implement database migration from version 6 to 7
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 7,
|
||||
"identityHash": "3eb1fa6c355e18713262b7da7d1ffbdd",
|
||||
"identityHash": "6ae361e9bd8dbaaa952bdfdb45272187",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "User",
|
||||
@@ -281,12 +281,23 @@
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_MeasurementType_key",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"key"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_MeasurementType_key` ON `${TABLE_NAME}` (`key`)"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3eb1fa6c355e18713262b7da7d1ffbdd')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6ae361e9bd8dbaaa952bdfdb45272187')"
|
||||
]
|
||||
}
|
||||
}
|
@@ -20,9 +20,12 @@ package com.health.openscale.core.data
|
||||
import android.content.Context
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity
|
||||
@Entity(
|
||||
indices = [Index(value = ["key"], unique = true)]
|
||||
)
|
||||
data class MeasurementType(
|
||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||
val key: MeasurementTypeKey = MeasurementTypeKey.CUSTOM,
|
||||
|
@@ -22,6 +22,8 @@ import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.health.openscale.core.data.Measurement
|
||||
import com.health.openscale.core.data.MeasurementType
|
||||
import com.health.openscale.core.data.MeasurementValue
|
||||
@@ -106,8 +108,206 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
AppDatabase::class.java,
|
||||
DATABASE_NAME
|
||||
)
|
||||
// TODO Add any other configurations like .addCallback(), .setQueryExecutor(), etc. here if needed.
|
||||
.addMigrations(MIGRATION_6_7)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATION_6_7 = object : Migration(6, 7) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("PRAGMA foreign_keys=OFF")
|
||||
|
||||
// --- Create tables ---
|
||||
db.execSQL("""
|
||||
CREATE TABLE IF NOT EXISTS `User`(
|
||||
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`name` TEXT NOT NULL,
|
||||
`birthDate` INTEGER NOT NULL,
|
||||
`gender` TEXT NOT NULL,
|
||||
`heightCm` REAL NOT NULL,
|
||||
`activityLevel` TEXT NOT NULL
|
||||
)
|
||||
""".trimIndent())
|
||||
|
||||
db.execSQL("""
|
||||
CREATE TABLE IF NOT EXISTS `Measurement`(
|
||||
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`userId` INTEGER NOT NULL,
|
||||
`timestamp` INTEGER NOT NULL,
|
||||
FOREIGN KEY(`userId`) REFERENCES `User`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE
|
||||
)
|
||||
""".trimIndent())
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS `index_Measurement_userId` ON `Measurement` (`userId`)")
|
||||
|
||||
db.execSQL("""
|
||||
CREATE TABLE IF NOT EXISTS `MeasurementType`(
|
||||
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`key` TEXT NOT NULL,
|
||||
`name` TEXT,
|
||||
`color` INTEGER NOT NULL,
|
||||
`icon` TEXT NOT NULL,
|
||||
`unit` TEXT NOT NULL,
|
||||
`inputType` TEXT NOT NULL,
|
||||
`displayOrder` INTEGER NOT NULL,
|
||||
`isDerived` INTEGER NOT NULL,
|
||||
`isEnabled` INTEGER NOT NULL,
|
||||
`isPinned` INTEGER NOT NULL,
|
||||
`isOnRightYAxis` INTEGER NOT NULL
|
||||
)
|
||||
""".trimIndent())
|
||||
|
||||
db.execSQL("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS `index_MeasurementType_key`
|
||||
ON `MeasurementType`(`key`)
|
||||
""".trimIndent())
|
||||
|
||||
db.execSQL("""
|
||||
CREATE TABLE IF NOT EXISTS `MeasurementValue`(
|
||||
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`measurementId` INTEGER NOT NULL,
|
||||
`typeId` INTEGER NOT NULL,
|
||||
`floatValue` REAL,
|
||||
`intValue` INTEGER,
|
||||
`textValue` TEXT,
|
||||
`dateValue` INTEGER,
|
||||
FOREIGN KEY(`measurementId`) REFERENCES `Measurement`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE,
|
||||
FOREIGN KEY(`typeId`) REFERENCES `MeasurementType`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE
|
||||
)
|
||||
""".trimIndent())
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS `index_MeasurementValue_measurementId` ON `MeasurementValue` (`measurementId`)")
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS `index_MeasurementValue_typeId` ON `MeasurementValue` (`typeId`)")
|
||||
|
||||
// --- Create measurement type idempotent (INSERT OR IGNORE, used UNIQUE-Index) ---
|
||||
fun ensureType(
|
||||
key: String,
|
||||
unit: String,
|
||||
color: Int,
|
||||
icon: String,
|
||||
inputType: String = "FLOAT",
|
||||
displayOrder: Int,
|
||||
isDerived: Int = 0,
|
||||
isEnabled: Int = 1,
|
||||
isPinned: Int = 0,
|
||||
isOnRightYAxis: Int = 0
|
||||
) {
|
||||
db.execSQL(
|
||||
"""
|
||||
INSERT OR IGNORE INTO MeasurementType
|
||||
(`key`,`name`,`color`,`icon`,`unit`,`inputType`,`displayOrder`,
|
||||
`isDerived`,`isEnabled`,`isPinned`,`isOnRightYAxis`)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
||||
""".trimIndent(),
|
||||
arrayOf(key, null, color, icon, unit, inputType, displayOrder, isDerived, isEnabled, isPinned, isOnRightYAxis)
|
||||
)
|
||||
}
|
||||
|
||||
var order = 1
|
||||
ensureType("WEIGHT", "KG", 0xFF7E57C2.toInt(), "IC_WEIGHT", displayOrder = order++, isPinned = 1, isOnRightYAxis = 1)
|
||||
ensureType("BMI", "NONE", 0xFFFFCA28.toInt(), "IC_BMI", displayOrder = order++, isDerived = 1, isPinned = 1)
|
||||
ensureType("BODY_FAT", "PERCENT", 0xFFEF5350.toInt(), "IC_BODY_FAT", displayOrder = order++, isPinned = 1)
|
||||
ensureType("WATER", "PERCENT", 0xFF29B6F6.toInt(), "IC_WATER", displayOrder = order++, isPinned = 1)
|
||||
ensureType("MUSCLE", "PERCENT", 0xFF66BB6A.toInt(), "IC_MUSCLE", displayOrder = order++, isPinned = 1)
|
||||
ensureType("LBM", "KG", 0xFF4DBAC0.toInt(), "IC_LBM", displayOrder = order++)
|
||||
ensureType("BONE", "KG", 0xFFBDBDBD.toInt(), "IC_BONE", displayOrder = order++)
|
||||
ensureType("WAIST", "CM", 0xFF78909C.toInt(), "IC_WAIST", displayOrder = order++)
|
||||
ensureType("WHR", "NONE", 0xFFFFA726.toInt(), "IC_WHR", displayOrder = order++, isDerived = 1)
|
||||
ensureType("WHTR", "NONE", 0xFFFF7043.toInt(), "IC_WHTR", displayOrder = order++, isDerived = 1)
|
||||
ensureType("HIPS", "CM", 0xFF5C6BC0.toInt(), "IC_HIPS", displayOrder = order++)
|
||||
ensureType("VISCERAL_FAT","NONE", 0xFFD84315.toInt(), "IC_VISCERAL_FAT", displayOrder = order++)
|
||||
ensureType("CHEST", "CM", 0xFF8E24AA.toInt(), "IC_CHEST", displayOrder = order++)
|
||||
ensureType("THIGH", "CM", 0xFFA1887F.toInt(), "IC_THIGH", displayOrder = order++)
|
||||
ensureType("BICEPS", "CM", 0xFFEC407A.toInt(), "IC_BICEPS", displayOrder = order++)
|
||||
ensureType("NECK", "CM", 0xFFB0BEC5.toInt(), "IC_NECK", displayOrder = order++)
|
||||
ensureType("CALIPER_1","CM", 0xFFFFF59D.toInt(), "IC_CALIPER1", displayOrder = order++)
|
||||
ensureType("CALIPER_2","CM", 0xFFFFE082.toInt(), "IC_CALIPER2", displayOrder = order++)
|
||||
ensureType("CALIPER_3","CM", 0xFFFFCC80.toInt(), "IC_CALIPER3", displayOrder = order++)
|
||||
ensureType("CALIPER", "PERCENT", 0xFFFB8C00.toInt(), "IC_FAT_CALIPER", displayOrder = order++, isDerived = 1)
|
||||
ensureType("BMR", "KCAL", 0xFFAB47BC.toInt(), "IC_BMR", displayOrder = order++, isDerived = 1)
|
||||
ensureType("TDEE", "KCAL", 0xFF26A69A.toInt(), "IC_TDEE", displayOrder = order++, isDerived = 1)
|
||||
ensureType("CALORIES", "KCAL", 0xFF4CAF50.toInt(), "IC_CALORIES", displayOrder = order++)
|
||||
ensureType("COMMENT", "NONE", 0xFFE0E0E0.toInt(), "IC_COMMENT", inputType = "TEXT", displayOrder = order++, isPinned = 1)
|
||||
ensureType("DATE", "NONE", 0xFF9E9E9E.toInt(), "IC_DATE", inputType = "DATE", displayOrder = order++)
|
||||
ensureType("TIME", "NONE", 0xFF757575.toInt(), "IC_TIME", inputType = "TIME", displayOrder = order++)
|
||||
|
||||
// --- Migrate users ---
|
||||
db.execSQL("""
|
||||
INSERT INTO `User` (id, name, birthDate, gender, heightCm, activityLevel)
|
||||
SELECT
|
||||
u.id,
|
||||
u.username,
|
||||
u.birthday,
|
||||
CASE u.gender WHEN 0 THEN 'MALE' ELSE 'FEMALE' END,
|
||||
u.bodyHeight,
|
||||
CASE u.activityLevel
|
||||
WHEN 0 THEN 'SEDENTARY'
|
||||
WHEN 1 THEN 'MILD'
|
||||
WHEN 2 THEN 'MODERATE'
|
||||
WHEN 3 THEN 'HEAVY'
|
||||
WHEN 4 THEN 'EXTREME'
|
||||
ELSE 'SEDENTARY' END
|
||||
FROM `scaleUsers` u
|
||||
""".trimIndent())
|
||||
|
||||
// --- Migrate measurements (only enabled = 1) ---
|
||||
db.execSQL("""
|
||||
INSERT INTO `Measurement` (id, userId, timestamp)
|
||||
SELECT m.id, m.userId, COALESCE(m.datetime, 0)
|
||||
FROM `scaleMeasurements` m
|
||||
WHERE m.enabled = 1
|
||||
""".trimIndent())
|
||||
|
||||
// --- Migrate values ---
|
||||
fun insertFloat(column: String, key: String) {
|
||||
db.execSQL("""
|
||||
INSERT INTO MeasurementValue (measurementId, typeId, floatValue)
|
||||
SELECT m.id,
|
||||
(SELECT id FROM MeasurementType WHERE `key` = ?),
|
||||
m.`$column`
|
||||
FROM scaleMeasurements m
|
||||
WHERE m.enabled = 1
|
||||
AND m.`$column` IS NOT NULL
|
||||
AND m.`$column` != 0
|
||||
""".trimIndent(), arrayOf(key))
|
||||
}
|
||||
|
||||
fun insertText(column: String, key: String) {
|
||||
db.execSQL("""
|
||||
INSERT INTO MeasurementValue (measurementId, typeId, textValue)
|
||||
SELECT m.id,
|
||||
(SELECT id FROM MeasurementType WHERE `key` = ?),
|
||||
m.`$column`
|
||||
FROM scaleMeasurements m
|
||||
WHERE m.enabled = 1
|
||||
AND m.`$column` IS NOT NULL
|
||||
AND m.`$column` != ''
|
||||
""".trimIndent(), arrayOf(key))
|
||||
}
|
||||
|
||||
insertFloat("weight", "WEIGHT")
|
||||
insertFloat("fat", "BODY_FAT")
|
||||
insertFloat("water", "WATER")
|
||||
insertFloat("muscle", "MUSCLE")
|
||||
insertFloat("visceralFat", "VISCERAL_FAT")
|
||||
insertFloat("lbm", "LBM")
|
||||
insertFloat("waist", "WAIST")
|
||||
insertFloat("hip", "HIPS")
|
||||
insertFloat("bone", "BONE")
|
||||
insertFloat("chest", "CHEST")
|
||||
insertFloat("thigh", "THIGH")
|
||||
insertFloat("biceps", "BICEPS")
|
||||
insertFloat("neck", "NECK")
|
||||
insertFloat("caliper1", "CALIPER_1")
|
||||
insertFloat("caliper2", "CALIPER_2")
|
||||
insertFloat("caliper3", "CALIPER_3")
|
||||
insertFloat("calories", "CALORIES")
|
||||
insertText ("comment", "COMMENT")
|
||||
|
||||
// --- Cleanup ---
|
||||
db.execSQL("DROP INDEX IF EXISTS `index_scaleMeasurements_userId_datetime`")
|
||||
db.execSQL("DROP TABLE IF EXISTS `scaleMeasurements`")
|
||||
db.execSQL("DROP TABLE IF EXISTS `scaleUsers`")
|
||||
|
||||
db.execSQL("PRAGMA foreign_keys=ON")
|
||||
}
|
||||
}
|
||||
|
@@ -31,7 +31,7 @@ interface MeasurementTypeDao {
|
||||
@Insert
|
||||
suspend fun insert(type: MeasurementType): Long
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertAll(types: List<MeasurementType>)
|
||||
|
||||
@Update
|
||||
|
@@ -768,12 +768,28 @@ class SharedViewModel(
|
||||
userSettingRepository.setCurrentUserId(null)
|
||||
}
|
||||
} else {
|
||||
LogManager.i(TAG, "Init: No user ID found in settings. No user auto-selected. (Initialization Logic)")
|
||||
LogManager.i(TAG, "Init: No user ID found in settings. Attempting auto-selection. (Initialization Logic)")
|
||||
|
||||
// --- Auto-selection logic if no user ID was saved ---
|
||||
val users = databaseRepository.getAllUsers().first()
|
||||
|
||||
if (users.isNotEmpty()) {
|
||||
// Case A: user exists -> select automatically
|
||||
val only = users.first()
|
||||
_selectedUserId.value = only.id
|
||||
userSettingRepository.setCurrentUserId(only.id)
|
||||
LogManager.i(TAG, "Init: Auto-selected only user ${only.id}. (Initialization Result)")
|
||||
} else {
|
||||
// Case B: No users at all -> leave selection empty
|
||||
LogManager.i(TAG, "Init: No users found in DB. Leaving selection null. (Initialization Logic)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogManager.i(TAG, "ViewModel initialization complete. (Lifecycle Event)")
|
||||
}
|
||||
|
||||
|
||||
private fun triggerSyncInsertMeasurement(
|
||||
measurementToSave: Measurement,
|
||||
valuesToSave: List<MeasurementValue>,
|
||||
|
@@ -319,7 +319,7 @@ fun DataManagementSettingsScreen(
|
||||
buildList {
|
||||
add(DataManagementSettingListItem.ActionItem(context.getString(R.string.settings_export_measurements_csv), Icons.Default.FileDownload, { if (!isAnyOperationLoading) settingsViewModel.startExportProcess() }, users.isNotEmpty() && !isAnyOperationLoading, isLoading = isLoadingExport))
|
||||
add(DataManagementSettingListItem.ActionItem(context.getString(R.string.settings_import_measurements_csv), Icons.Default.FileUpload, { if (!isAnyOperationLoading) settingsViewModel.startImportProcess() }, users.isNotEmpty() && !isAnyOperationLoading, isLoading = isLoadingImport))
|
||||
add(DataManagementSettingListItem.ActionItem(context.getString(R.string.settings_backup_database_manual), Icons.Default.CloudDownload, { if (!isAnyOperationLoading) settingsViewModel.startDatabaseBackup() }, !isAnyOperationLoading, isLoading = isLoadingBackup))
|
||||
add(DataManagementSettingListItem.ActionItem(context.getString(R.string.settings_backup_database), Icons.Default.CloudDownload, { if (!isAnyOperationLoading) settingsViewModel.startDatabaseBackup() }, !isAnyOperationLoading, isLoading = isLoadingBackup))
|
||||
add(DataManagementSettingListItem.ActionItem(context.getString(R.string.settings_restore_database), Icons.Filled.CloudUpload, { if (!isAnyOperationLoading) showRestoreConfirmationDialog = true }, !isAnyOperationLoading, isLoading = isLoadingRestore))
|
||||
}
|
||||
}
|
||||
|
@@ -978,75 +978,106 @@ class SettingsViewModel(
|
||||
return@launch
|
||||
}
|
||||
|
||||
// Close the database before attempting to overwrite files
|
||||
LogManager.d(TAG, "Attempting to close database before restore.")
|
||||
repository.closeDatabase() // Ensure this method exists and correctly closes Room
|
||||
repository.closeDatabase()
|
||||
LogManager.i(TAG, "Database closed for restore operation.")
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
// Detect format by checking ZIP magic header
|
||||
val isZip = contentResolver.openInputStream(restoreUri)?.use { ins ->
|
||||
val header = ByteArray(4)
|
||||
val read = ins.read(header)
|
||||
read == 4 &&
|
||||
header[0].toInt() == 0x50 &&
|
||||
header[1].toInt() == 0x4B &&
|
||||
header[2].toInt() == 0x03 &&
|
||||
header[3].toInt() == 0x04
|
||||
} ?: false
|
||||
|
||||
var restoreSuccessful = false
|
||||
var mainDbRestored = false
|
||||
|
||||
fun deleteIfExists(file: File) {
|
||||
if (file.exists() && !file.delete()) {
|
||||
LogManager.w(TAG, "Could not delete ${file.absolutePath} before restore.")
|
||||
}
|
||||
}
|
||||
|
||||
val shm = File(dbDir, "$dbName-shm")
|
||||
val wal = File(dbDir, "$dbName-wal")
|
||||
|
||||
try {
|
||||
contentResolver.openInputStream(restoreUri)?.use { inputStream ->
|
||||
ZipInputStream(inputStream).use { zipInputStream ->
|
||||
var entry: ZipEntry? = zipInputStream.nextEntry
|
||||
while (entry != null) {
|
||||
val outputFile = File(dbDir, entry.name)
|
||||
// Basic path traversal protection
|
||||
if (!outputFile.canonicalPath.startsWith(dbDir.canonicalPath)) {
|
||||
LogManager.e(TAG, "Skipping restore of entry '${entry.name}' due to path traversal attempt.")
|
||||
entry = zipInputStream.nextEntry
|
||||
continue
|
||||
}
|
||||
if (isZip) {
|
||||
// --- New format: ZIP archive ---
|
||||
LogManager.i(TAG, "Detected ZIP backup format.")
|
||||
contentResolver.openInputStream(restoreUri)?.use { inputStream ->
|
||||
ZipInputStream(inputStream).use { zipInputStream ->
|
||||
deleteIfExists(dbFile)
|
||||
deleteIfExists(shm)
|
||||
deleteIfExists(wal)
|
||||
|
||||
// Delete existing file before restoring (important for WAL mode)
|
||||
if (outputFile.exists()) {
|
||||
if (!outputFile.delete()) {
|
||||
LogManager.w(TAG, "Could not delete existing file ${outputFile.name} before restore. Restore might fail or be incomplete.")
|
||||
var entry: ZipEntry? = zipInputStream.nextEntry
|
||||
var mainDbRestored = false
|
||||
while (entry != null) {
|
||||
val out = File(dbDir, entry.name)
|
||||
if (!out.canonicalPath.startsWith(dbDir.canonicalPath)) {
|
||||
LogManager.e(TAG, "Skipping '${entry.name}' due to path traversal.")
|
||||
entry = zipInputStream.nextEntry
|
||||
continue
|
||||
}
|
||||
deleteIfExists(out)
|
||||
FileOutputStream(out).use { zipInputStream.copyTo(it) }
|
||||
LogManager.d(TAG, "Restored ${entry.name} to ${out.absolutePath}")
|
||||
if (entry.name == dbName) mainDbRestored = true
|
||||
entry = zipInputStream.nextEntry
|
||||
}
|
||||
|
||||
FileOutputStream(outputFile).use { fileOutputStream ->
|
||||
zipInputStream.copyTo(fileOutputStream)
|
||||
if (!mainDbRestored) {
|
||||
LogManager.e(TAG, "Main DB file '$dbName' missing in ZIP.")
|
||||
sharedViewModel.showSnackbar(R.string.restore_error_db_files_missing)
|
||||
return@withContext
|
||||
}
|
||||
LogManager.d(TAG, "Restored ${entry.name} from backup archive to ${outputFile.absolutePath}.")
|
||||
if (entry.name == dbName) {
|
||||
mainDbRestored = true
|
||||
}
|
||||
entry = zipInputStream.nextEntry
|
||||
}
|
||||
} ?: run {
|
||||
sharedViewModel.showSnackbar(R.string.restore_error_no_input_stream)
|
||||
LogManager.e(TAG, "Restore failed: Could not open InputStream for Uri: $restoreUri")
|
||||
return@withContext
|
||||
}
|
||||
} ?: run {
|
||||
sharedViewModel.showSnackbar(R.string.restore_error_no_input_stream)
|
||||
LogManager.e(TAG, "Restore failed: Could not open InputStream for Uri: $restoreUri")
|
||||
return@withContext
|
||||
}
|
||||
restoreSuccessful = true
|
||||
} else {
|
||||
// --- Legacy format: single .db file ---
|
||||
LogManager.i(TAG, "Detected legacy backup format (.db).")
|
||||
contentResolver.openInputStream(restoreUri)?.use { inputStream ->
|
||||
deleteIfExists(dbFile)
|
||||
deleteIfExists(shm)
|
||||
deleteIfExists(wal)
|
||||
|
||||
if (!mainDbRestored) {
|
||||
LogManager.e(TAG, "Restore failed: Main database file '$dbName' not found in the backup archive.")
|
||||
sharedViewModel.showSnackbar(R.string.restore_error_db_files_missing)
|
||||
// Attempt to clean up partially restored files might be needed here, or let the user handle it.
|
||||
return@withContext
|
||||
val tmp = File(dbDir, "$dbName.tmp-restore")
|
||||
FileOutputStream(tmp).use { output -> inputStream.copyTo(output) }
|
||||
if (!tmp.renameTo(dbFile)) {
|
||||
LogManager.w(TAG, "Rename temp restore file failed, using copy as final.")
|
||||
}
|
||||
LogManager.d(TAG, "Restored legacy DB file to ${dbFile.absolutePath}")
|
||||
} ?: run {
|
||||
sharedViewModel.showSnackbar(R.string.restore_error_no_input_stream)
|
||||
LogManager.e(TAG, "Restore failed: Could not open InputStream for Uri: $restoreUri")
|
||||
return@withContext
|
||||
}
|
||||
restoreSuccessful = true
|
||||
}
|
||||
restoreSuccessful = true
|
||||
|
||||
} catch (e: IOException) {
|
||||
LogManager.e(TAG, "IO Error during database restore from URI $restoreUri", e)
|
||||
val errorMsg = e.localizedMessage ?: "Unknown I/O error"
|
||||
sharedViewModel.showSnackbar(R.string.restore_error_generic, listOf(errorMsg))
|
||||
LogManager.e(TAG, "IO Error during restore from URI $restoreUri", e)
|
||||
val msg = e.localizedMessage ?: "Unknown I/O error"
|
||||
sharedViewModel.showSnackbar(R.string.restore_error_generic, listOf(msg))
|
||||
return@withContext
|
||||
} catch (e: IllegalStateException) { // Can be thrown by ZipInputStream
|
||||
LogManager.e(TAG, "Error processing ZIP file during restore from URI $restoreUri", e)
|
||||
} catch (e: IllegalStateException) {
|
||||
// ZIP stream error (not a valid archive)
|
||||
LogManager.e(TAG, "ZIP processing error during restore.", e)
|
||||
sharedViewModel.showSnackbar(R.string.restore_error_zip_format)
|
||||
return@withContext
|
||||
}
|
||||
|
||||
|
||||
if (restoreSuccessful) {
|
||||
LogManager.i(TAG, "Database restore from $restoreUri successful. App restart is required.")
|
||||
LogManager.i(TAG, "Database restore successful. App restart recommended.")
|
||||
sharedViewModel.showSnackbar(R.string.restore_successful)
|
||||
// The app needs to be restarted for Room to pick up the new database files correctly.
|
||||
// This usually involves sharedViewModel.requestAppRestart() or similar mechanism.
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -1054,23 +1085,6 @@ class SettingsViewModel(
|
||||
val errorMsg = e.localizedMessage ?: "Unknown error"
|
||||
sharedViewModel.showSnackbar(R.string.restore_error_generic, listOf(errorMsg))
|
||||
} finally {
|
||||
// Re-open the database regardless of success, unless app is restarting
|
||||
// If an app restart is requested, reopening might not be necessary or could cause issues.
|
||||
// However, if the restore failed and no restart is pending, the DB should be reopened.
|
||||
if (!_isLoadingRestore.value) { // Check if not already restarting
|
||||
try {
|
||||
LogManager.d(TAG, "Attempting to re-open database after restore attempt.")
|
||||
// This might require re-initialization of the Room database instance
|
||||
// if the underlying files were changed.
|
||||
// For simplicity, we assume the repository handles this.
|
||||
// A full app restart is generally the safest way after a DB restore.
|
||||
// TODO repository.reopenDatabase() // Ensure this method exists and correctly re-opens Room
|
||||
LogManager.i(TAG, "Database re-opened after restore attempt.")
|
||||
} catch (reopenError: Exception) {
|
||||
LogManager.e(TAG, "Error re-opening database after restore attempt. App restart is highly recommended.", reopenError)
|
||||
sharedViewModel.showSnackbar(R.string.restore_error_generic, listOf("Error re-opening database."))
|
||||
}
|
||||
}
|
||||
_isLoadingRestore.value = false
|
||||
LogManager.i(TAG, "Database restore process finished for URI: $restoreUri.")
|
||||
}
|
||||
|
@@ -355,7 +355,6 @@
|
||||
<string name="settings_backup_location_not_configured">Sicherungsort nicht konfiguriert</string>
|
||||
<string name="settings_backup_location_not_configured_for_auto">Ort nicht konfiguriert. Automatische Sicherungen pausiert.</string>
|
||||
<string name="settings_backup_location_error_accessing">Fehler beim Zugriff auf Sicherungsort</string>
|
||||
<string name="settings_backup_database_manual">Datenbank sichern (Manuell)</string>
|
||||
<string name="content_desc_delete_icon">Löschen-Symbol</string>
|
||||
<string name="dialog_title_select_backup_directory">Sicherungsverzeichnis auswählen</string>
|
||||
<string name="settings_backup_location_select_action">Sicherungsort auswählen</string>
|
||||
|
@@ -357,7 +357,6 @@
|
||||
<string name="settings_backup_location_not_configured">Backup location not configured</string>
|
||||
<string name="settings_backup_location_not_configured_for_auto">Location not configured. Automatic backups paused.</string>
|
||||
<string name="settings_backup_location_error_accessing">Error accessing backup location</string>
|
||||
<string name="settings_backup_database_manual">Backup database (Manual)</string>
|
||||
<string name="content_desc_delete_icon">Delete icon</string>
|
||||
<string name="dialog_title_select_backup_directory">Select Backup Directory</string>
|
||||
<string name="settings_backup_location_select_action">Select Backup Location</string>
|
||||
|
Reference in New Issue
Block a user