diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 0ce866e..88f33ae 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -21,4 +21,24 @@ #-renamesourcefileattribute SourceFile # Suppress Guava J2ObjC annotation missing classes --dontwarn com.google.j2objc.annotations.** \ No newline at end of file +-dontwarn com.google.j2objc.annotations.** + +# Keep kotlinx.serialization generated serializers +-keepclassmembers class **$$serializer { *; } + +# Keep Serializable classes +-keep @kotlinx.serialization.Serializable class * { *; } + +# Keep sealed subclasses of RecurrenceRule +-keep class com.flux.data.model.RecurrenceRule** { *; } + +# Keep enum constants +-keepclassmembers enum * { + *; +} + +# Keep polymorphic serialization metadata +-keepattributes *Annotation* + +# Keep Kotlin metadata (critical) +-keep class kotlin.Metadata { *; } diff --git a/app/src/main/java/com/flux/data/database/FluxDatabase.kt b/app/src/main/java/com/flux/data/database/FluxDatabase.kt index 7a619e9..d67484b 100644 --- a/app/src/main/java/com/flux/data/database/FluxDatabase.kt +++ b/app/src/main/java/com/flux/data/database/FluxDatabase.kt @@ -30,10 +30,11 @@ import com.google.gson.Gson import com.google.gson.reflect.TypeToken import com.vladsch.flexmark.html2md.converter.FlexmarkHtmlConverter import kotlinx.serialization.json.Json +import java.util.UUID @Database( entities = [EventModel::class, LabelModel::class, EventInstanceModel::class, SettingsModel::class, NotesModel::class, HabitModel::class, HabitInstanceModel::class, WorkspaceModel::class, TodoModel::class, JournalModel::class], - version = 9, + version = 4, exportSchema = false ) @TypeConverters(Converter::class) @@ -64,48 +65,56 @@ val MIGRATION_2_3 = object : Migration(2, 3) { db.execSQL("ALTER TABLE EventModel ADD COLUMN endDateTime INTEGER NOT NULL DEFAULT -1") } } - val MIGRATION_3_4 = object : Migration(3, 4) { + override fun migrate(db: SupportSQLiteDatabase) { - // Add temp column + /* ============================================================ + 1. WorkspaceModel — selectedSpaces normalization (CRASH FIX) + ============================================================ */ + db.execSQL(""" ALTER TABLE WorkspaceModel - ADD COLUMN selectedSpaces_new TEXT NOT NULL DEFAULT '[]' + ADD COLUMN selectedSpaces_new TEXT NOT NULL DEFAULT '' """) - val cursor = db.query( + val wsCursor = db.query( "SELECT workspaceId, selectedSpaces FROM WorkspaceModel" ) - while (cursor.moveToNext()) { - val id = cursor.getString(0) - val raw = cursor.getString(1) + while (wsCursor.moveToNext()) { - val spaces = Json.decodeFromString>(raw) - .toMutableSet() + val id = wsCursor.getString(0) + val raw = wsCursor.getString(1) ?: "" - // 1. Merge Calendar (4) → Events (3) - if (spaces.remove(4)) { - spaces.add(3) + // ---- SAFE PARSE (CSV + JSON tolerant) ---- + val spaces = try { + Json.decodeFromString>(raw).toMutableSet() + } catch (_: Exception) { + raw.split(",") + .mapNotNull { it.trim().toIntOrNull() } + .toMutableSet() } - // 2. Downgrade IDs above 4 + // Merge Calendar (4) → Events (3) + if (spaces.remove(4)) spaces.add(3) + + // Shift IDs > 4 val normalized = spaces.map { if (it > 4) it - 1 else it }.toSet() - val newJson = Json.encodeToString(normalized.toList()) + // Store as CSV (since converter still CSV) + val newCsv = normalized.joinToString(",") db.execSQL( "UPDATE WorkspaceModel SET selectedSpaces_new = ? WHERE workspaceId = ?", - arrayOf(newJson, id) + arrayOf(newCsv, id) ) } - cursor.close() + wsCursor.close() - // Recreate table (SQLite cannot drop columns) db.execSQL(""" CREATE TABLE WorkspaceModel_new ( workspaceId TEXT NOT NULL PRIMARY KEY, @@ -130,109 +139,86 @@ val MIGRATION_3_4 = object : Migration(3, 4) { db.execSQL("DROP TABLE WorkspaceModel") db.execSQL("ALTER TABLE WorkspaceModel_new RENAME TO WorkspaceModel") - } -} -val MIGRATION_4_5 = object : Migration(4, 5) { - override fun migrate(db: SupportSQLiteDatabase) { - // Add storageRootUri column with default value null - db.execSQL("ALTER TABLE SettingsModel ADD COLUMN storageRootUri TEXT") - } -} -val MIGRATION_5_6 = object : Migration(5, 6) { - override fun migrate(db: SupportSQLiteDatabase) { + /* ============================================================ + 2. SettingsModel columns (4→6) + ============================================================ */ + db.execSQL( - """ + "ALTER TABLE SettingsModel ADD COLUMN storageRootUri TEXT" + ) + + db.execSQL(""" ALTER TABLE SettingsModel ADD COLUMN startWithReadView INTEGER NOT NULL DEFAULT 0 - """.trimIndent() - ) + """) - db.execSQL( - """ + db.execSQL(""" ALTER TABLE SettingsModel ADD COLUMN isLineNumbersVisible INTEGER NOT NULL DEFAULT 0 - """.trimIndent() - ) + """) - db.execSQL( - """ + db.execSQL(""" ALTER TABLE SettingsModel ADD COLUMN isLintValid INTEGER NOT NULL DEFAULT 0 - """.trimIndent() - ) - } -} + """) -val MIGRATION_6_7 = object : Migration(6, 7) { - override fun migrate(db: SupportSQLiteDatabase) { - // Migration to add 'id' field to TodoItem objects within the items JSON array - // Since TodoItem is stored as JSON in the 'items' column of TodoModel, - // we need to parse, update, and re-save the JSON data - val cursor = db.query("SELECT id, items FROM TodoModel") + /* ============================================================ + 3. TodoItem ID migration (6→7) + ============================================================ */ + + val todoCursor = db.query("SELECT id, items FROM TodoModel") val gson = Gson() - while (cursor.moveToNext()) { - val todoId = cursor.getString(0) - val itemsJson = cursor.getString(1) + while (todoCursor.moveToNext()) { + + val todoId = todoCursor.getString(0) + val itemsJson = todoCursor.getString(1) try { - // Define a temporary class to represent old TodoItem without id data class OldTodoItem( val value: String, val isChecked: Boolean ) - // Define new TodoItem with id field data class NewTodoItem( val id: String, val value: String, val isChecked: Boolean ) - // Parse old format - val oldItemsType = object : TypeToken>() {}.type - val oldItems: List = try { - gson.fromJson(itemsJson, oldItemsType) - } catch (_: Exception) { - // If it already has the new format, skip - continue - } + val type = object : TypeToken>() {}.type - // Convert to new format with generated IDs - val newItems = oldItems.map { oldItem -> + val oldItems: List = + gson.fromJson(itemsJson, type) ?: continue + + val newItems = oldItems.map { NewTodoItem( - id = java.util.UUID.randomUUID().toString(), - value = oldItem.value, - isChecked = oldItem.isChecked + id = UUID.randomUUID().toString(), + value = it.value, + isChecked = it.isChecked ) } - // Serialize back to JSON - val newItemsJson = gson.toJson(newItems) - - // Update the database db.execSQL( "UPDATE TodoModel SET items = ? WHERE id = ?", - arrayOf(newItemsJson, todoId) + arrayOf(gson.toJson(newItems), todoId) ) - } catch (e: Exception) { - // If migration fails for this record, log or skip - // The TypeConverter will handle it at runtime as fallback - e.printStackTrace() + + } catch (_: Exception) { + // Skip malformed rows } } - cursor.close() - } -} + todoCursor.close() -val MIGRATION_7_8 = object : Migration(7, 8) { - override fun migrate(db: SupportSQLiteDatabase) { - // 1. Create new table without `images` + /* ============================================================ + 4. Journal + Notes table rebuild (7→8) + ============================================================ */ + db.execSQL(""" CREATE TABLE journalmodel_new ( journalId TEXT NOT NULL, @@ -241,23 +227,21 @@ val MIGRATION_7_8 = object : Migration(7, 8) { dateTime INTEGER NOT NULL, PRIMARY KEY(journalId) ) - """.trimIndent()) + """) - // 2. Copy data (ignore `images`) db.execSQL(""" - INSERT INTO journalmodel_new (journalId, workspaceId, text, dateTime) + INSERT INTO journalmodel_new SELECT journalId, workspaceId, text, dateTime FROM JournalModel - """.trimIndent()) + """) - // 3. Drop old table db.execSQL("DROP TABLE JournalModel") - - // 4. Rename new table - db.execSQL("ALTER TABLE journalmodel_new RENAME TO JournalModel") - db.execSQL( - """ + "ALTER TABLE journalmodel_new RENAME TO JournalModel" + ) + + + db.execSQL(""" CREATE TABLE NotesModel_new ( notesId TEXT NOT NULL, workspaceId TEXT NOT NULL, @@ -268,21 +252,10 @@ val MIGRATION_7_8 = object : Migration(7, 8) { lastEdited INTEGER NOT NULL, PRIMARY KEY(notesId) ) - """.trimIndent() - ) + """) - // 2. Copy data (skip `images`) - db.execSQL( - """ - INSERT INTO NotesModel_new ( - notesId, - workspaceId, - title, - description, - isPinned, - labels, - lastEdited - ) + db.execSQL(""" + INSERT INTO NotesModel_new SELECT notesId, workspaceId, @@ -292,25 +265,20 @@ val MIGRATION_7_8 = object : Migration(7, 8) { labels, lastEdited FROM NotesModel - """.trimIndent() + """) + + db.execSQL("DROP TABLE NotesModel") + db.execSQL( + "ALTER TABLE NotesModel_new RENAME TO NotesModel" ) - // 3. Drop old table - db.execSQL("DROP TABLE NotesModel") - // 4. Rename new table - db.execSQL("ALTER TABLE NotesModel_new RENAME TO NotesModel") - } -} - -val Migration_8_9 = object : Migration(8, 9) { - - override fun migrate(db: SupportSQLiteDatabase) { + /* ============================================================ + 5. HTML → Markdown migration (8→9) + ============================================================ */ val converter = FlexmarkHtmlConverter.builder().build() - /* ---------------- Notes Migration ---------------- */ - db.query( "SELECT notesId, description FROM NotesModel" ).use { cursor -> @@ -325,19 +293,13 @@ val Migration_8_9 = object : Migration(8, 9) { val markdown = converter.convert(html) db.execSQL( - """ - UPDATE NotesModel - SET description = ? - WHERE notesId = ? - """.trimIndent(), + "UPDATE NotesModel SET description = ? WHERE notesId = ?", arrayOf(markdown, id) ) } } } - /* ---------------- Journals Migration ---------------- */ - db.query( "SELECT journalId, text FROM JournalModel" ).use { cursor -> @@ -352,11 +314,7 @@ val Migration_8_9 = object : Migration(8, 9) { val markdown = converter.convert(html) db.execSQL( - """ - UPDATE JournalModel - SET text = ? - WHERE journalId = ? - """.trimIndent(), + "UPDATE JournalModel SET text = ? WHERE journalId = ?", arrayOf(markdown, id) ) } diff --git a/app/src/main/java/com/flux/di/DataModule.kt b/app/src/main/java/com/flux/di/DataModule.kt index fcb71ed..9f28730 100644 --- a/app/src/main/java/com/flux/di/DataModule.kt +++ b/app/src/main/java/com/flux/di/DataModule.kt @@ -16,11 +16,6 @@ import com.flux.data.database.FluxDatabase import com.flux.data.database.MIGRATION_1_2 import com.flux.data.database.MIGRATION_2_3 import com.flux.data.database.MIGRATION_3_4 -import com.flux.data.database.MIGRATION_4_5 -import com.flux.data.database.MIGRATION_5_6 -import com.flux.data.database.MIGRATION_6_7 -import com.flux.data.database.MIGRATION_7_8 -import com.flux.data.database.Migration_8_9 import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -40,7 +35,7 @@ object DataModule { FluxDatabase::class.java, "FluxDatabase" ) - .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, Migration_8_9) + .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4) .build() @Singleton