diff --git a/app/schemas/org.fossify.calendar.databases.EventsDatabase/9.json b/app/schemas/org.fossify.calendar.databases.EventsDatabase/9.json new file mode 100644 index 000000000..5fbac42bc --- /dev/null +++ b/app/schemas/org.fossify.calendar.databases.EventsDatabase/9.json @@ -0,0 +1,369 @@ +{ + "formatVersion": 1, + "database": { + "version": 9, + "identityHash": "e86c636a21679b3cc37edf60fb5ca93f", + "entities": [ + { + "tableName": "events", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `start_ts` INTEGER NOT NULL, `end_ts` INTEGER NOT NULL, `title` TEXT NOT NULL, `location` TEXT NOT NULL, `description` TEXT NOT NULL, `reminder_1_minutes` INTEGER NOT NULL, `reminder_2_minutes` INTEGER NOT NULL, `reminder_3_minutes` INTEGER NOT NULL, `reminder_1_type` INTEGER NOT NULL, `reminder_2_type` INTEGER NOT NULL, `reminder_3_type` INTEGER NOT NULL, `repeat_interval` INTEGER NOT NULL, `repeat_rule` INTEGER NOT NULL, `repeat_limit` INTEGER NOT NULL, `repetition_exceptions` TEXT NOT NULL, `attendees` TEXT NOT NULL, `import_id` TEXT NOT NULL, `time_zone` TEXT NOT NULL, `flags` INTEGER NOT NULL, `event_type` INTEGER NOT NULL, `parent_id` INTEGER NOT NULL, `last_updated` INTEGER NOT NULL, `source` TEXT NOT NULL, `availability` INTEGER NOT NULL, `color` INTEGER NOT NULL, `type` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startTS", + "columnName": "start_ts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endTS", + "columnName": "end_ts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "reminder1Minutes", + "columnName": "reminder_1_minutes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reminder2Minutes", + "columnName": "reminder_2_minutes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reminder3Minutes", + "columnName": "reminder_3_minutes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reminder1Type", + "columnName": "reminder_1_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reminder2Type", + "columnName": "reminder_2_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reminder3Type", + "columnName": "reminder_3_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatInterval", + "columnName": "repeat_interval", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatRule", + "columnName": "repeat_rule", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatLimit", + "columnName": "repeat_limit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repetitionExceptions", + "columnName": "repetition_exceptions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attendees", + "columnName": "attendees", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "importId", + "columnName": "import_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timeZone", + "columnName": "time_zone", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eventType", + "columnName": "event_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "availability", + "columnName": "availability", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_events_id", + "unique": true, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_events_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "event_types", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `title` TEXT NOT NULL, `color` INTEGER NOT NULL, `caldav_calendar_id` INTEGER NOT NULL, `caldav_display_name` TEXT NOT NULL, `caldav_email` TEXT NOT NULL, `type` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "caldavCalendarId", + "columnName": "caldav_calendar_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "caldavDisplayName", + "columnName": "caldav_display_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "caldavEmail", + "columnName": "caldav_email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_event_types_id", + "unique": true, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_event_types_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "widgets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `widget_id` INTEGER NOT NULL, `period` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "widgetId", + "columnName": "widget_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "period", + "columnName": "period", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_widgets_widget_id", + "unique": true, + "columnNames": [ + "widget_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_widgets_widget_id` ON `${TABLE_NAME}` (`widget_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "tasks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `task_id` INTEGER NOT NULL, `start_ts` INTEGER NOT NULL, `flags` INTEGER NOT NULL, FOREIGN KEY(`task_id`) REFERENCES `events`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "task_id", + "columnName": "task_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startTS", + "columnName": "start_ts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_tasks_id_task_id", + "unique": true, + "columnNames": [ + "id", + "task_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tasks_id_task_id` ON `${TABLE_NAME}` (`id`, `task_id`)" + } + ], + "foreignKeys": [ + { + "table": "events", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "task_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "views": [], + "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, 'e86c636a21679b3cc37edf60fb5ca93f')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/org/fossify/calendar/databases/EventsDatabase.kt b/app/src/main/kotlin/org/fossify/calendar/databases/EventsDatabase.kt index 716958631..dbdbca2c7 100644 --- a/app/src/main/kotlin/org/fossify/calendar/databases/EventsDatabase.kt +++ b/app/src/main/kotlin/org/fossify/calendar/databases/EventsDatabase.kt @@ -22,7 +22,7 @@ import org.fossify.calendar.models.Widget import org.fossify.commons.extensions.getProperPrimaryColor import java.util.concurrent.Executors -@Database(entities = [Event::class, EventType::class, Widget::class, Task::class], version = 8) +@Database(entities = [Event::class, EventType::class, Widget::class, Task::class], version = 9) @TypeConverters(Converters::class) abstract class EventsDatabase : RoomDatabase() { @@ -55,6 +55,7 @@ abstract class EventsDatabase : RoomDatabase() { .addMigrations(MIGRATION_5_6) .addMigrations(MIGRATION_6_7) .addMigrations(MIGRATION_7_8) + .addMigrations(MIGRATION_8_9) .build() db!!.openHelper.setWriteAheadLoggingEnabled(true) } @@ -136,5 +137,21 @@ abstract class EventsDatabase : RoomDatabase() { } } } + + private val MIGRATION_8_9 = object : Migration(8, 9) { + override fun migrate(database: SupportSQLiteDatabase) { + database.apply { + // remove old, invalid entries + execSQL("DELETE FROM `tasks` WHERE `task_id` NOT IN (SELECT e.id FROM events AS e)") + // SQLite doesn't support ALTER TABLE ADD CONSTRAINT, so we need to recreate the table + execSQL("ALTER TABLE `tasks` RENAME TO `_tasks_tmp`") + execSQL("DROP INDEX `index_tasks_id_task_id`") + execSQL("CREATE TABLE `tasks` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `task_id` INTEGER NOT NULL, `start_ts` INTEGER NOT NULL, `flags` INTEGER NOT NULL, CONSTRAINT fk_task_id FOREIGN KEY (task_id) REFERENCES events(id) ON DELETE CASCADE)") + execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_tasks_id_task_id` ON `tasks` (`id`, `task_id`)") + execSQL("INSERT INTO `tasks` SELECT * FROM `_tasks_tmp`") + execSQL("DROP TABLE `_tasks_tmp`") + } + } + } } } diff --git a/app/src/main/kotlin/org/fossify/calendar/helpers/EventsHelper.kt b/app/src/main/kotlin/org/fossify/calendar/helpers/EventsHelper.kt index d7faba95d..feac21b75 100644 --- a/app/src/main/kotlin/org/fossify/calendar/helpers/EventsHelper.kt +++ b/app/src/main/kotlin/org/fossify/calendar/helpers/EventsHelper.kt @@ -18,6 +18,7 @@ class EventsHelper(val context: Context) { private val config = context.config private val eventsDB = context.eventsDB private val eventTypesDB = context.eventTypesDB + private val completedTasksDB = context.completedTasksDB fun getEventTypes(activity: Activity, showWritableOnly: Boolean, callback: (eventTypes: ArrayList) -> Unit) { ensureBackgroundThread { @@ -315,6 +316,10 @@ class EventsHelper(val context: Context) { context.calDAVHelper.updateCalDAVEvent(event) } } + + if (event.isTask()) { + completedTasksDB.deleteTaskFutureOccurences(eventId, occurrenceTS) + } } fun doEventTypesContainEventsOrTasks(eventTypeIds: ArrayList, callback: (contain: Boolean) -> Unit) { @@ -335,6 +340,10 @@ class EventsHelper(val context: Context) { if (addToCalDAV && config.caldavSync) { context.calDAVHelper.insertEventRepeatException(parentEvent, occurrenceTS) } + + if (parentEvent.isTask()) { + completedTasksDB.deleteTaskWithIdAndTs(parentEventId, occurrenceTS) + } } } diff --git a/app/src/main/kotlin/org/fossify/calendar/interfaces/TasksDao.kt b/app/src/main/kotlin/org/fossify/calendar/interfaces/TasksDao.kt index 4bfd8d707..5e7fabf18 100644 --- a/app/src/main/kotlin/org/fossify/calendar/interfaces/TasksDao.kt +++ b/app/src/main/kotlin/org/fossify/calendar/interfaces/TasksDao.kt @@ -16,4 +16,7 @@ interface TasksDao { @Query("DELETE FROM tasks WHERE task_id = :id AND start_ts = :startTs") fun deleteTaskWithIdAndTs(id: Long, startTs: Long) + + @Query("DELETE FROM tasks WHERE task_id = :id AND start_ts >= :startTs") + fun deleteTaskFutureOccurences(id: Long, startTs: Long) } diff --git a/app/src/main/kotlin/org/fossify/calendar/models/Task.kt b/app/src/main/kotlin/org/fossify/calendar/models/Task.kt index 2356c6374..3cbd06430 100644 --- a/app/src/main/kotlin/org/fossify/calendar/models/Task.kt +++ b/app/src/main/kotlin/org/fossify/calendar/models/Task.kt @@ -4,9 +4,19 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey +import androidx.room.ForeignKey import org.fossify.calendar.helpers.FLAG_TASK_COMPLETED -@Entity(tableName = "tasks", indices = [(Index(value = ["id", "task_id"], unique = true))]) +@Entity( + tableName = "tasks", + indices = [(Index(value = ["id", "task_id"], unique = true))], + foreignKeys = [ForeignKey( + entity = Event::class, + parentColumns = arrayOf("id"), + childColumns = arrayOf("task_id"), + onDelete = ForeignKey.CASCADE + )] +) data class Task( @PrimaryKey(autoGenerate = true) var id: Long?, @ColumnInfo(name = "task_id") var task_id: Long,