From 8c7a62c401227454f137c87314e8b9430fc98edb Mon Sep 17 00:00:00 2001 From: oliexdev Date: Tue, 19 Aug 2025 20:50:13 +0200 Subject: [PATCH] Implement database migration from version 6 to 7 --- .../7.json | 17 +- .../openscale/core/data/MeasurementType.kt | 5 +- .../openscale/core/database/AppDatabase.kt | 202 +++++++++++++++++- .../core/database/MeasurementTypeDao.kt | 2 +- .../openscale/ui/screen/SharedViewModel.kt | 18 +- .../settings/DataManagementSettingsScreen.kt | 2 +- .../ui/screen/settings/SettingsViewModel.kt | 142 ++++++------ .../app/src/main/res/values-de/strings.xml | 1 - .../app/src/main/res/values/strings.xml | 1 - 9 files changed, 316 insertions(+), 74 deletions(-) diff --git a/android_app/app/schemas/com.health.openscale.core.database.AppDatabase/7.json b/android_app/app/schemas/com.health.openscale.core.database.AppDatabase/7.json index 5aa07288..ffc039c9 100644 --- a/android_app/app/schemas/com.health.openscale.core.database.AppDatabase/7.json +++ b/android_app/app/schemas/com.health.openscale.core.database.AppDatabase/7.json @@ -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')" ] } } \ No newline at end of file diff --git a/android_app/app/src/main/java/com/health/openscale/core/data/MeasurementType.kt b/android_app/app/src/main/java/com/health/openscale/core/data/MeasurementType.kt index f24fb330..e8fda364 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/data/MeasurementType.kt +++ b/android_app/app/src/main/java/com/health/openscale/core/data/MeasurementType.kt @@ -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, diff --git a/android_app/app/src/main/java/com/health/openscale/core/database/AppDatabase.kt b/android_app/app/src/main/java/com/health/openscale/core/database/AppDatabase.kt index c1f6d250..08d6cd81 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/database/AppDatabase.kt +++ b/android_app/app/src/main/java/com/health/openscale/core/database/AppDatabase.kt @@ -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") + } +} diff --git a/android_app/app/src/main/java/com/health/openscale/core/database/MeasurementTypeDao.kt b/android_app/app/src/main/java/com/health/openscale/core/database/MeasurementTypeDao.kt index b59b619a..39c67c5c 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/database/MeasurementTypeDao.kt +++ b/android_app/app/src/main/java/com/health/openscale/core/database/MeasurementTypeDao.kt @@ -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) @Update diff --git a/android_app/app/src/main/java/com/health/openscale/ui/screen/SharedViewModel.kt b/android_app/app/src/main/java/com/health/openscale/ui/screen/SharedViewModel.kt index f77b4d95..330c74dc 100644 --- a/android_app/app/src/main/java/com/health/openscale/ui/screen/SharedViewModel.kt +++ b/android_app/app/src/main/java/com/health/openscale/ui/screen/SharedViewModel.kt @@ -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, diff --git a/android_app/app/src/main/java/com/health/openscale/ui/screen/settings/DataManagementSettingsScreen.kt b/android_app/app/src/main/java/com/health/openscale/ui/screen/settings/DataManagementSettingsScreen.kt index f011b4ed..919a1689 100644 --- a/android_app/app/src/main/java/com/health/openscale/ui/screen/settings/DataManagementSettingsScreen.kt +++ b/android_app/app/src/main/java/com/health/openscale/ui/screen/settings/DataManagementSettingsScreen.kt @@ -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)) } } diff --git a/android_app/app/src/main/java/com/health/openscale/ui/screen/settings/SettingsViewModel.kt b/android_app/app/src/main/java/com/health/openscale/ui/screen/settings/SettingsViewModel.kt index 6bce37bd..429f16c2 100644 --- a/android_app/app/src/main/java/com/health/openscale/ui/screen/settings/SettingsViewModel.kt +++ b/android_app/app/src/main/java/com/health/openscale/ui/screen/settings/SettingsViewModel.kt @@ -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.") } diff --git a/android_app/app/src/main/res/values-de/strings.xml b/android_app/app/src/main/res/values-de/strings.xml index aca4738d..fc43786e 100644 --- a/android_app/app/src/main/res/values-de/strings.xml +++ b/android_app/app/src/main/res/values-de/strings.xml @@ -355,7 +355,6 @@ Sicherungsort nicht konfiguriert Ort nicht konfiguriert. Automatische Sicherungen pausiert. Fehler beim Zugriff auf Sicherungsort - Datenbank sichern (Manuell) Löschen-Symbol Sicherungsverzeichnis auswählen Sicherungsort auswählen diff --git a/android_app/app/src/main/res/values/strings.xml b/android_app/app/src/main/res/values/strings.xml index 9dbdb2a1..0873193b 100644 --- a/android_app/app/src/main/res/values/strings.xml +++ b/android_app/app/src/main/res/values/strings.xml @@ -357,7 +357,6 @@ Backup location not configured Location not configured. Automatic backups paused. Error accessing backup location - Backup database (Manual) Delete icon Select Backup Directory Select Backup Location