From 71f22dd05d803ada5f44d75612e90ab03ebf556a Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Fri, 25 Nov 2022 08:43:02 -0600 Subject: [PATCH] Read-only support --- app/build.gradle.kts | 2 +- .../com.todoroo.astrid.dao.Database/87.json | 1503 +++++++++++++++++ .../org/tasks/opentasks/TestOpenTaskDao.kt | 3 + .../astrid/activity/TaskEditFragment.kt | 80 +- .../astrid/activity/TaskListFragment.kt | 28 +- .../com/todoroo/astrid/api/CaldavFilter.java | 15 +- .../java/com/todoroo/astrid/api/Filter.java | 8 + .../java/com/todoroo/astrid/dao/Database.kt | 2 +- .../main/java/com/todoroo/astrid/data/Task.kt | 9 +- .../todoroo/astrid/service/TaskCompleter.kt | 1 + .../com/todoroo/astrid/service/TaskDeleter.kt | 22 +- .../todoroo/astrid/service/TaskDuplicator.kt | 21 +- .../com/todoroo/astrid/service/TaskMover.kt | 29 +- .../org/tasks/caldav/CaldavSynchronizer.kt | 6 +- .../main/java/org/tasks/caldav/iCalendar.kt | 2 + .../org/tasks/caldav/property/ShareAccess.kt | 1 + .../main/java/org/tasks/data/OpenTaskDao.kt | 8 +- .../java/org/tasks/data/TaskContainer.java | 4 + app/src/main/java/org/tasks/db/Migrations.kt | 9 + .../tasks/dialogs/FilterPickerViewModel.kt | 2 +- .../org/tasks/fragments/CommentBarFragment.kt | 5 +- .../notifications/NotificationManager.kt | 10 +- .../tasks/opentasks/OpenTasksSynchronizer.kt | 18 +- .../preferences/DefaultFilterProvider.kt | 5 +- .../tasks/reminders/NotificationActivity.kt | 7 +- .../tasks/reminders/NotificationDialog.java | 16 +- .../java/org/tasks/ui/TaskEditViewModel.kt | 6 +- .../main/java/org/tasks/widget/TasksWidget.kt | 4 + deps_fdroid.txt | 2 +- deps_googleplay.txt | 2 +- 30 files changed, 1729 insertions(+), 101 deletions(-) create mode 100644 app/schemas/com.todoroo.astrid.dao.Database/87.json diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e07ae72fa..bc97c21b6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -164,7 +164,7 @@ dependencies { implementation("com.github.bitfireAT:dav4jvm:2.2") { exclude(group = "junit") } - implementation("com.github.tasks:ical4android:2fb465b") { + implementation("com.github.tasks:ical4android:ce7919d") { exclude(group = "commons-logging") exclude(group = "org.json", module = "json") exclude(group = "org.codehaus.groovy", module = "groovy") diff --git a/app/schemas/com.todoroo.astrid.dao.Database/87.json b/app/schemas/com.todoroo.astrid.dao.Database/87.json new file mode 100644 index 000000000..84f70abe8 --- /dev/null +++ b/app/schemas/com.todoroo.astrid.dao.Database/87.json @@ -0,0 +1,1503 @@ +{ + "formatVersion": 1, + "database": { + "version": 87, + "identityHash": "c09b376c6d87fd6d41f6883c8da840a8", + "entities": [ + { + "tableName": "notification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `task` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `type` INTEGER NOT NULL, `location` INTEGER, FOREIGN KEY(`task`) REFERENCES `tasks`(`_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "taskId", + "columnName": "task", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_notification_task", + "unique": true, + "columnNames": [ + "task" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_notification_task` ON `${TABLE_NAME}` (`task`)" + } + ], + "foreignKeys": [ + { + "table": "tasks", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "task" + ], + "referencedColumns": [ + "_id" + ] + } + ] + }, + { + "tableName": "tagdata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `remoteId` TEXT, `name` TEXT, `color` INTEGER, `tagOrdering` TEXT, `td_icon` INTEGER, `td_order` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "remoteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "tagOrdering", + "columnName": "tagOrdering", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "icon", + "columnName": "td_icon", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "td_order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "userActivity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `remoteId` TEXT, `message` TEXT, `picture` TEXT, `target_id` TEXT, `created_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "remoteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "picture", + "columnName": "picture", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "targetId", + "columnName": "target_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "attachment_file", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`file_id` INTEGER PRIMARY KEY AUTOINCREMENT, `file_uuid` TEXT NOT NULL, `filename` TEXT NOT NULL, `uri` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "file_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "file_uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "file_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "task_list_metadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `tag_uuid` TEXT, `filter` TEXT, `task_ids` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "tagUuid", + "columnName": "tag_uuid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filter", + "columnName": "filter", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "taskIds", + "columnName": "task_ids", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tasks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT, `importance` INTEGER NOT NULL, `dueDate` INTEGER NOT NULL, `hideUntil` INTEGER NOT NULL, `created` INTEGER NOT NULL, `modified` INTEGER NOT NULL, `completed` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `notes` TEXT, `estimatedSeconds` INTEGER NOT NULL, `elapsedSeconds` INTEGER NOT NULL, `timerStart` INTEGER NOT NULL, `notificationFlags` INTEGER NOT NULL, `lastNotified` INTEGER NOT NULL, `recurrence` TEXT, `repeat_from` INTEGER NOT NULL DEFAULT 0, `calendarUri` TEXT, `remoteId` TEXT, `collapsed` INTEGER NOT NULL, `parent` INTEGER NOT NULL, `read_only` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "priority", + "columnName": "importance", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dueDate", + "columnName": "dueDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hideUntil", + "columnName": "hideUntil", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "creationDate", + "columnName": "created", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "modificationDate", + "columnName": "modified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "completionDate", + "columnName": "completed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deletionDate", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "estimatedSeconds", + "columnName": "estimatedSeconds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "elapsedSeconds", + "columnName": "elapsedSeconds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timerStart", + "columnName": "timerStart", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ringFlags", + "columnName": "notificationFlags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reminderLast", + "columnName": "lastNotified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recurrence", + "columnName": "recurrence", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "repeatFrom", + "columnName": "repeat_from", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "calendarURI", + "columnName": "calendarUri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "remoteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isCollapsed", + "columnName": "collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readOnly", + "columnName": "read_only", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "t_rid", + "unique": true, + "columnNames": [ + "remoteId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `t_rid` ON `${TABLE_NAME}` (`remoteId`)" + }, + { + "name": "active_and_visible", + "unique": false, + "columnNames": [ + "completed", + "deleted", + "hideUntil" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `active_and_visible` ON `${TABLE_NAME}` (`completed`, `deleted`, `hideUntil`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "alarms", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `task` INTEGER NOT NULL, `time` INTEGER NOT NULL, `type` INTEGER NOT NULL DEFAULT 0, `repeat` INTEGER NOT NULL DEFAULT 0, `interval` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`task`) REFERENCES `tasks`(`_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "task", + "columnName": "task", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "repeat", + "columnName": "repeat", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "interval", + "columnName": "interval", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_alarms_task", + "unique": false, + "columnNames": [ + "task" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_alarms_task` ON `${TABLE_NAME}` (`task`)" + } + ], + "foreignKeys": [ + { + "table": "tasks", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "task" + ], + "referencedColumns": [ + "_id" + ] + } + ] + }, + { + "tableName": "places", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`place_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uid` TEXT, `name` TEXT, `address` TEXT, `phone` TEXT, `url` TEXT, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `place_color` INTEGER NOT NULL, `place_icon` INTEGER NOT NULL, `place_order` INTEGER NOT NULL, `radius` INTEGER NOT NULL DEFAULT 250)", + "fields": [ + { + "fieldPath": "id", + "columnName": "place_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "phone", + "columnName": "phone", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "place_color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "place_icon", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "place_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "radius", + "columnName": "radius", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "250" + } + ], + "primaryKey": { + "columnNames": [ + "place_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "place_uid", + "unique": true, + "columnNames": [ + "uid" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `place_uid` ON `${TABLE_NAME}` (`uid`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "geofences", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`geofence_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `task` INTEGER NOT NULL, `place` TEXT, `arrival` INTEGER NOT NULL, `departure` INTEGER NOT NULL, FOREIGN KEY(`task`) REFERENCES `tasks`(`_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "geofence_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "task", + "columnName": "task", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "place", + "columnName": "place", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isArrival", + "columnName": "arrival", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDeparture", + "columnName": "departure", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "geofence_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_geofences_task", + "unique": false, + "columnNames": [ + "task" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_geofences_task` ON `${TABLE_NAME}` (`task`)" + } + ], + "foreignKeys": [ + { + "table": "tasks", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "task" + ], + "referencedColumns": [ + "_id" + ] + } + ] + }, + { + "tableName": "tags", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `task` INTEGER NOT NULL, `name` TEXT, `tag_uid` TEXT, `task_uid` TEXT, FOREIGN KEY(`task`) REFERENCES `tasks`(`_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "task", + "columnName": "task", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tagUid", + "columnName": "tag_uid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "taskUid", + "columnName": "task_uid", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_tags_task", + "unique": false, + "columnNames": [ + "task" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tags_task` ON `${TABLE_NAME}` (`task`)" + } + ], + "foreignKeys": [ + { + "table": "tasks", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "task" + ], + "referencedColumns": [ + "_id" + ] + } + ] + }, + { + "tableName": "google_tasks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`gt_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `gt_task` INTEGER NOT NULL, `gt_remote_id` TEXT, `gt_list_id` TEXT, `gt_parent` INTEGER NOT NULL, `gt_remote_parent` TEXT, `gt_moved` INTEGER NOT NULL, `gt_order` INTEGER NOT NULL, `gt_remote_order` INTEGER NOT NULL, `gt_last_sync` INTEGER NOT NULL, `gt_deleted` INTEGER NOT NULL, FOREIGN KEY(`gt_task`) REFERENCES `tasks`(`_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "gt_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "task", + "columnName": "gt_task", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteId", + "columnName": "gt_remote_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "listId", + "columnName": "gt_list_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parent", + "columnName": "gt_parent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteParent", + "columnName": "gt_remote_parent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isMoved", + "columnName": "gt_moved", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "gt_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteOrder", + "columnName": "gt_remote_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastSync", + "columnName": "gt_last_sync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "gt_deleted", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "gt_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "gt_list_parent", + "unique": false, + "columnNames": [ + "gt_list_id", + "gt_parent" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `gt_list_parent` ON `${TABLE_NAME}` (`gt_list_id`, `gt_parent`)" + }, + { + "name": "index_google_tasks_gt_task", + "unique": false, + "columnNames": [ + "gt_task" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_google_tasks_gt_task` ON `${TABLE_NAME}` (`gt_task`)" + } + ], + "foreignKeys": [ + { + "table": "tasks", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "gt_task" + ], + "referencedColumns": [ + "_id" + ] + } + ] + }, + { + "tableName": "filters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT, `sql` TEXT, `values` TEXT, `criterion` TEXT, `f_color` INTEGER, `f_icon` INTEGER, `f_order` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sql", + "columnName": "sql", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "values", + "columnName": "values", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "criterion", + "columnName": "criterion", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "f_color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "icon", + "columnName": "f_icon", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "f_order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "google_task_lists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`gtl_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `gtl_account` TEXT, `gtl_remote_id` TEXT, `gtl_title` TEXT, `gtl_remote_order` INTEGER NOT NULL, `gtl_last_sync` INTEGER NOT NULL, `gtl_color` INTEGER, `gtl_icon` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "gtl_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "account", + "columnName": "gtl_account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "gtl_remote_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "gtl_title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "gtl_remote_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastSync", + "columnName": "gtl_last_sync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "gtl_color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "icon", + "columnName": "gtl_icon", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "gtl_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caldav_lists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`cdl_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `cdl_account` TEXT, `cdl_uuid` TEXT, `cdl_name` TEXT, `cdl_color` INTEGER NOT NULL, `cdl_ctag` TEXT, `cdl_url` TEXT, `cdl_icon` INTEGER, `cdl_order` INTEGER NOT NULL, `cdl_access` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "cdl_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "account", + "columnName": "cdl_account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uuid", + "columnName": "cdl_uuid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "cdl_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "cdl_color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ctag", + "columnName": "cdl_ctag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "cdl_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "icon", + "columnName": "cdl_icon", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "cdl_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "access", + "columnName": "cdl_access", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "cdl_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caldav_tasks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`cd_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `cd_task` INTEGER NOT NULL, `cd_calendar` TEXT, `cd_object` TEXT, `cd_remote_id` TEXT, `cd_etag` TEXT, `cd_last_sync` INTEGER NOT NULL, `cd_deleted` INTEGER NOT NULL, `cd_remote_parent` TEXT, `cd_order` INTEGER, FOREIGN KEY(`cd_task`) REFERENCES `tasks`(`_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "cd_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "task", + "columnName": "cd_task", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "calendar", + "columnName": "cd_calendar", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "object", + "columnName": "cd_object", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "cd_remote_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "cd_etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastSync", + "columnName": "cd_last_sync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "cd_deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteParent", + "columnName": "cd_remote_parent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "cd_order", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "cd_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_caldav_tasks_cd_task", + "unique": false, + "columnNames": [ + "cd_task" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_caldav_tasks_cd_task` ON `${TABLE_NAME}` (`cd_task`)" + } + ], + "foreignKeys": [ + { + "table": "tasks", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "cd_task" + ], + "referencedColumns": [ + "_id" + ] + } + ] + }, + { + "tableName": "caldav_accounts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`cda_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `cda_uuid` TEXT, `cda_name` TEXT, `cda_url` TEXT, `cda_username` TEXT, `cda_password` TEXT, `cda_error` TEXT, `cda_account_type` INTEGER NOT NULL, `cda_collapsed` INTEGER NOT NULL, `cda_server_type` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "cda_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uuid", + "columnName": "cda_uuid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "cda_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "cda_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "username", + "columnName": "cda_username", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "password", + "columnName": "cda_password", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "error", + "columnName": "cda_error", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountType", + "columnName": "cda_account_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCollapsed", + "columnName": "cda_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serverType", + "columnName": "cda_server_type", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "cda_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "google_task_accounts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`gta_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `gta_account` TEXT, `gta_error` TEXT, `gta_etag` TEXT, `gta_collapsed` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "gta_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "account", + "columnName": "gta_account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "error", + "columnName": "gta_error", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "gta_etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isCollapsed", + "columnName": "gta_collapsed", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "gta_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "principals", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account` INTEGER NOT NULL, `href` TEXT NOT NULL, `email` TEXT, `display_name` TEXT, FOREIGN KEY(`account`) REFERENCES `caldav_accounts`(`cda_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "href", + "columnName": "href", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_principals_account_href", + "unique": true, + "columnNames": [ + "account", + "href" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_principals_account_href` ON `${TABLE_NAME}` (`account`, `href`)" + } + ], + "foreignKeys": [ + { + "table": "caldav_accounts", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "account" + ], + "referencedColumns": [ + "cda_id" + ] + } + ] + }, + { + "tableName": "principal_access", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `principal` INTEGER NOT NULL, `list` INTEGER NOT NULL, `invite` INTEGER NOT NULL, `access` INTEGER NOT NULL, FOREIGN KEY(`principal`) REFERENCES `principals`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`list`) REFERENCES `caldav_lists`(`cdl_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "principal", + "columnName": "principal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "list", + "columnName": "list", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "invite", + "columnName": "invite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "access", + "columnName": "access", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_principal_access_list_principal", + "unique": true, + "columnNames": [ + "list", + "principal" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_principal_access_list_principal` ON `${TABLE_NAME}` (`list`, `principal`)" + }, + { + "name": "index_principal_access_principal", + "unique": false, + "columnNames": [ + "principal" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_principal_access_principal` ON `${TABLE_NAME}` (`principal`)" + } + ], + "foreignKeys": [ + { + "table": "principals", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "principal" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "caldav_lists", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "list" + ], + "referencedColumns": [ + "cdl_id" + ] + } + ] + }, + { + "tableName": "attachment", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`attachment_id` INTEGER PRIMARY KEY AUTOINCREMENT, `task` INTEGER NOT NULL, `file` INTEGER NOT NULL, `file_uuid` TEXT NOT NULL, FOREIGN KEY(`task`) REFERENCES `tasks`(`_id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`file`) REFERENCES `attachment_file`(`file_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "attachment_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "task", + "columnName": "task", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileId", + "columnName": "file", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachmentUid", + "columnName": "file_uuid", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "attachment_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_attachment_task_file", + "unique": true, + "columnNames": [ + "task", + "file" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_attachment_task_file` ON `${TABLE_NAME}` (`task`, `file`)" + }, + { + "name": "index_attachment_task", + "unique": false, + "columnNames": [ + "task" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_attachment_task` ON `${TABLE_NAME}` (`task`)" + }, + { + "name": "index_attachment_file", + "unique": false, + "columnNames": [ + "file" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_attachment_file` ON `${TABLE_NAME}` (`file`)" + } + ], + "foreignKeys": [ + { + "table": "tasks", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "task" + ], + "referencedColumns": [ + "_id" + ] + }, + { + "table": "attachment_file", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "file" + ], + "referencedColumns": [ + "file_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, 'c09b376c6d87fd6d41f6883c8da840a8')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/tasks/opentasks/TestOpenTaskDao.kt b/app/src/androidTest/java/org/tasks/opentasks/TestOpenTaskDao.kt index 2d03b07f7..2fcd12f36 100644 --- a/app/src/androidTest/java/org/tasks/opentasks/TestOpenTaskDao.kt +++ b/app/src/androidTest/java/org/tasks/opentasks/TestOpenTaskDao.kt @@ -7,6 +7,7 @@ import at.bitfire.ical4android.Task import com.todoroo.astrid.helper.UUIDHelper import dagger.hilt.android.qualifiers.ApplicationContext import org.dmfs.tasks.contract.TaskContract +import org.dmfs.tasks.contract.TaskContract.TaskListColumns.ACCESS_LEVEL_OWNER import org.tasks.caldav.iCalendar import org.tasks.data.CaldavCalendar import org.tasks.data.CaldavDao @@ -23,6 +24,7 @@ class TestOpenTaskDao @Inject constructor( type: String = DEFAULT_TYPE, account: String = DEFAULT_ACCOUNT, url: String = UUIDHelper.newUUID(), + accessLevel: Int = ACCESS_LEVEL_OWNER, ): Pair { val uri = taskLists.buildUpon() .appendQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER, "true") @@ -34,6 +36,7 @@ class TestOpenTaskDao @Inject constructor( .withValue(TaskContract.CommonSyncColumns._SYNC_ID, url) .withValue(TaskContract.TaskListColumns.LIST_NAME, name) .withValue(TaskContract.TaskLists.SYNC_ENABLED, "1") + .withValue(TaskContract.TaskLists.ACCESS_LEVEL, accessLevel) ) val calendar = CaldavCalendar( uuid = UUIDHelper.newUUID(), diff --git a/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.kt b/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.kt index 5bcb83b55..cee8be79d 100755 --- a/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.kt @@ -18,17 +18,16 @@ import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.widget.Toolbar import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.* import androidx.compose.material.Divider -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.viewinterop.AndroidViewBinding import androidx.coordinatorlayout.widget.CoordinatorLayout @@ -126,26 +125,34 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener { val view: View = binding.root val model = editViewModel.task val toolbar = binding.toolbar - toolbar.navigationIcon = context.getDrawable(R.drawable.ic_outline_save_24px) + toolbar.navigationIcon = AppCompatResources.getDrawable( + context, + if (editViewModel.isReadOnly) + R.drawable.ic_outline_arrow_back_24px + else + R.drawable.ic_outline_save_24px + ) toolbar.setNavigationOnClickListener { lifecycleScope.launch { save() } } val backButtonSavesTask = preferences.backButtonSavesTask() - toolbar.setNavigationContentDescription(if (backButtonSavesTask) { - R.string.discard - } else { - R.string.save - }) + toolbar.setNavigationContentDescription( + when { + editViewModel.isReadOnly -> R.string.back + backButtonSavesTask -> R.string.discard + else -> R.string.save + } + ) toolbar.inflateMenu(R.menu.menu_task_edit_fragment) val menu = toolbar.menu val delete = menu.findItem(R.id.menu_delete) - delete.isVisible = !model.isNew + delete.isVisible = !model.isNew && editViewModel.isWritable delete.setShowAsAction( if (backButtonSavesTask) MenuItem.SHOW_AS_ACTION_NEVER else MenuItem.SHOW_AS_ACTION_IF_ROOM) val discard = menu.findItem(R.id.menu_discard) - discard.isVisible = backButtonSavesTask + discard.isVisible = backButtonSavesTask && editViewModel.isWritable discard.setShowAsAction( if (model.isNew) MenuItem.SHOW_AS_ACTION_IF_ROOM else MenuItem.SHOW_AS_ACTION_NEVER) if (savedInstanceState == null) { @@ -173,7 +180,11 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener { title.setText(model.title) title.setHorizontallyScrolling(false) title.maxLines = 5 - if (model.isNew || preferences.getBoolean(R.string.p_hide_check_button, false)) { + if ( + model.isNew || + preferences.getBoolean(R.string.p_hide_check_button, false) || + editViewModel.isReadOnly + ) { binding.fab.visibility = View.INVISIBLE } else if (editViewModel.completed) { title.paintFlags = title.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG @@ -211,7 +222,7 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener { } binding.composeView.setContent { MdcTheme { - Column { + Column(modifier = Modifier.gesturesDisabled(editViewModel.isReadOnly)) { taskEditControlSetFragmentManager.displayOrder.forEachIndexed { index, tag -> if (index < taskEditControlSetFragmentManager.visibleSize) { // TODO: remove ui-viewbinding library when these are all migrated @@ -222,8 +233,12 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener { TAG_LIST -> ListRow() TAG_CREATION -> CreationRow() CalendarControlSet.TAG -> AndroidViewBinding(TaskEditCalendarBinding::inflate) - StartDateControlSet.TAG -> AndroidViewBinding(TaskEditStartDateBinding::inflate) - ReminderControlSet.TAG -> AndroidViewBinding(TaskEditRemindersBinding::inflate) + StartDateControlSet.TAG -> AndroidViewBinding( + TaskEditStartDateBinding::inflate + ) + ReminderControlSet.TAG -> AndroidViewBinding( + TaskEditRemindersBinding::inflate + ) LocationControlSet.TAG -> AndroidViewBinding(TaskEditLocationBinding::inflate) FilesControlSet.TAG -> AndroidViewBinding(TaskEditFilesBinding::inflate) TimerControlSet.TAG -> AndroidViewBinding(TaskEditTimerBinding::inflate) @@ -496,7 +511,8 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener { const val EXTRA_TAGS = "extra_tags" const val EXTRA_ALARMS = "extra_alarms" - private const val FRAG_TAG_GOOGLE_TASK_LIST_SELECTION = "frag_tag_google_task_list_selection" + private const val FRAG_TAG_GOOGLE_TASK_LIST_SELECTION = + "frag_tag_google_task_list_selection" const val FRAG_TAG_CALENDAR_PICKER = "frag_tag_calendar_picker" private const val FRAG_TAG_DATE_PICKER = "frag_tag_date_picker" const val REQUEST_CODE_PICK_CALENDAR = 70 @@ -510,11 +526,11 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener { } fun newTaskEditFragment( - task: Task, - list: Filter, - location: Location?, - tags: ArrayList, - alarms: ArrayList, + task: Task, + list: Filter, + location: Location?, + tags: ArrayList, + alarms: ArrayList, ): TaskEditFragment { val taskEditFragment = TaskEditFragment() val arguments = Bundle() @@ -526,5 +542,21 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener { taskEditFragment.arguments = arguments return taskEditFragment } + + fun Modifier.gesturesDisabled(disabled: Boolean = true) = + if (disabled) { + pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + awaitPointerEvent(pass = PointerEventPass.Initial) + .changes + .filter { it.position == it.previousPosition } + .forEach { it.consume() } + } + } + } + } else { + this + } } } \ No newline at end of file diff --git a/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt b/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt index ab2eec37f..cb6f2fa5a 100644 --- a/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt @@ -243,14 +243,15 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { binding = FragmentTaskListBinding.inflate(inflater, container, false) + filter = getFilter() with (binding) { swipeRefreshLayout = bodyStandard.swipeLayout emptyRefreshLayout = bodyEmpty.swipeLayoutEmpty coordinatorLayout = taskListCoordinator recyclerView = bodyStandard.recyclerView fab.setOnClickListener { createNewTask() } + fab.isVisible = filter.isWritable } - filter = getFilter() themeColor = if (filter.tint != 0) colorProvider.getThemeColor(filter.tint, true) else defaultThemeColor filter.setFilterQueryOverride(null) @@ -373,8 +374,9 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL menu.findItem(R.id.menu_collapse_subtasks).isVisible = false menu.findItem(R.id.menu_expand_subtasks).isVisible = false } - menu.findItem(R.id.menu_voice_add).isVisible = device.voiceInputAvailable() + menu.findItem(R.id.menu_voice_add).isVisible = device.voiceInputAvailable() && filter.isWritable search = binding.toolbar.menu.findItem(R.id.menu_search).setOnActionExpandListener(this) + menu.findItem(R.id.menu_clear_completed).isVisible = filter.isWritable } private fun openFilter(filter: Filter?) { @@ -633,10 +635,12 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL REQUEST_TAG_TASKS -> if (resultCode == Activity.RESULT_OK) { lifecycleScope.launch { val modified = tagDataDao.applyTags( - taskDao.fetch( - data!!.getSerializableExtra(TagPickerActivity.EXTRA_TASKS) as ArrayList), + taskDao + .fetch(data!!.getSerializableExtra(TagPickerActivity.EXTRA_TASKS) as ArrayList) + .filterNot { it.readOnly }, data.getParcelableArrayListExtra(TagPickerActivity.EXTRA_PARTIALLY_SELECTED)!!, - data.getParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED)!!) + data.getParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED)!! + ) taskDao.touch(modified) } finishActionMode() @@ -693,6 +697,10 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL override fun onCreateActionMode(actionMode: ActionMode, menu: Menu): Boolean { val inflater = actionMode.menuInflater inflater.inflate(R.menu.menu_multi_select, menu) + if (filter.isReadOnly) { + listOf(R.id.edit_tags, R.id.move_tasks, R.id.reschedule, R.id.copy_tasks, R.id.delete) + .forEach { menu.findItem(it).isVisible = false } + } return true } @@ -729,6 +737,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL lifecycleScope.launch { taskDao .fetch(selected) + .filterNot { it.readOnly } .takeIf { it.isNotEmpty() } ?.let { newDateTimePicker( @@ -800,7 +809,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL } } - fun showDateTimePicker(task: TaskContainer) { + private fun showDateTimePicker(task: TaskContainer) { val fragmentManager = parentFragmentManager if (fragmentManager.findFragmentByTag(FRAG_TAG_DATE_TIME_PICKER) == null) { newDateTimePicker( @@ -859,6 +868,9 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL fun clearCollapsed() = taskAdapter.clearCollapsed() override fun onCompletedTask(task: TaskContainer, newState: Boolean) { + if (task.isReadOnly) { + return + } lifecycleScope.launch { taskCompleter.setComplete(task.getTask(), newState) taskAdapter.onCompletedTask(task, newState) @@ -900,6 +912,9 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL } override fun onChangeDueDate(task: TaskContainer) { + if (task.isReadOnly) { + return + } showDateTimePicker(task) } @@ -921,6 +936,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL val tasks = (intent.getSerializableExtra(EXTRAS_TASK_ID) as? ArrayList) ?.let { taskDao.fetch(it) } + ?.filterNot { it.readOnly } ?.takeIf { it.isNotEmpty() } ?: return@launch val isRecurringCompletion = diff --git a/app/src/main/java/com/todoroo/astrid/api/CaldavFilter.java b/app/src/main/java/com/todoroo/astrid/api/CaldavFilter.java index 27ab19352..49fe09559 100644 --- a/app/src/main/java/com/todoroo/astrid/api/CaldavFilter.java +++ b/app/src/main/java/com/todoroo/astrid/api/CaldavFilter.java @@ -2,19 +2,23 @@ package com.todoroo.astrid.api; import android.os.Parcel; import android.os.Parcelable; + import androidx.annotation.NonNull; + import com.todoroo.andlib.sql.Criterion; import com.todoroo.andlib.sql.Join; import com.todoroo.andlib.sql.QueryTemplate; import com.todoroo.astrid.data.Task; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; + import org.tasks.R; import org.tasks.data.CaldavCalendar; import org.tasks.data.CaldavTask; import org.tasks.data.TaskDao; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + public class CaldavFilter extends Filter { /** Parcelable Creator Object */ @@ -82,6 +86,11 @@ public class CaldavFilter extends Filter { return calendar; } + @Override + public boolean isReadOnly() { + return calendar.getAccess() == CaldavCalendar.ACCESS_READ_ONLY; + } + /** {@inheritDoc} */ @Override public void writeToParcel(Parcel dest, int flags) { diff --git a/app/src/main/java/com/todoroo/astrid/api/Filter.java b/app/src/main/java/com/todoroo/astrid/api/Filter.java index 71d598dfc..51d6d4b08 100644 --- a/app/src/main/java/com/todoroo/astrid/api/Filter.java +++ b/app/src/main/java/com/todoroo/astrid/api/Filter.java @@ -196,6 +196,14 @@ public class Filter extends FilterListItem { return true; } + public boolean isWritable() { + return !isReadOnly(); + } + + public boolean isReadOnly() { + return false; + } + public boolean hasBeginningMenu() { return getBeginningMenu() != 0; } diff --git a/app/src/main/java/com/todoroo/astrid/dao/Database.kt b/app/src/main/java/com/todoroo/astrid/dao/Database.kt index 3201608c9..cd20c7ae9 100644 --- a/app/src/main/java/com/todoroo/astrid/dao/Database.kt +++ b/app/src/main/java/com/todoroo/astrid/dao/Database.kt @@ -36,7 +36,7 @@ import org.tasks.notifications.NotificationDao autoMigrations = [ AutoMigration(from = 83, to = 84, spec = Migrations.AutoMigrate83to84::class), ], - version = 86 + version = 87 ) abstract class Database : RoomDatabase() { abstract fun notificationDao(): NotificationDao diff --git a/app/src/main/java/com/todoroo/astrid/data/Task.kt b/app/src/main/java/com/todoroo/astrid/data/Task.kt index 00b01ad10..13de3323c 100644 --- a/app/src/main/java/com/todoroo/astrid/data/Task.kt +++ b/app/src/main/java/com/todoroo/astrid/data/Task.kt @@ -103,6 +103,9 @@ class Task : Parcelable { @Transient var parent = 0L + @ColumnInfo(name = "read_only", defaultValue = "0") + var readOnly: Boolean = false + @Ignore @Transient private var transitoryData: HashMap? = null @@ -132,6 +135,7 @@ class Task : Parcelable { transitoryData = parcel.readHashMap(ContentValues::class.java.classLoader) as HashMap? isCollapsed = ParcelCompat.readBoolean(parcel) parent = parcel.readLong() + readOnly = ParcelCompat.readBoolean(parcel) } var uuid: String @@ -265,6 +269,7 @@ class Task : Parcelable { dest.writeMap(transitoryData as Map<*, *>?) ParcelCompat.writeBoolean(dest, isCollapsed) dest.writeLong(parent) + ParcelCompat.writeBoolean(dest, readOnly) } fun insignificantChange(task: Task?): Boolean { @@ -408,6 +413,7 @@ class Task : Parcelable { if (isCollapsed != other.isCollapsed) return false if (parent != other.parent) return false if (transitoryData != other.transitoryData) return false + if (readOnly != other.readOnly) return false return true } @@ -434,11 +440,12 @@ class Task : Parcelable { result = 31 * result + isCollapsed.hashCode() result = 31 * result + parent.hashCode() result = 31 * result + (transitoryData?.hashCode() ?: 0) + result = 31 * result + readOnly.hashCode() return result } override fun toString(): String { - return "Task(id=$id, title=$title, priority=$priority, dueDate=$dueDate, hideUntil=$hideUntil, creationDate=$creationDate, modificationDate=$modificationDate, completionDate=$completionDate, deletionDate=$deletionDate, notes=$notes, estimatedSeconds=$estimatedSeconds, elapsedSeconds=$elapsedSeconds, timerStart=$timerStart, ringFlags=$ringFlags, reminderLast=$reminderLast, recurrence=$recurrence, calendarURI=$calendarURI, remoteId='$remoteId', isCollapsed=$isCollapsed, parent=$parent, transitoryData=$transitoryData)" + return "Task(id=$id, title=$title, priority=$priority, dueDate=$dueDate, hideUntil=$hideUntil, creationDate=$creationDate, modificationDate=$modificationDate, completionDate=$completionDate, deletionDate=$deletionDate, notes=$notes, estimatedSeconds=$estimatedSeconds, elapsedSeconds=$elapsedSeconds, timerStart=$timerStart, ringFlags=$ringFlags, reminderLast=$reminderLast, recurrence=$recurrence, calendarURI=$calendarURI, remoteId='$remoteId', isCollapsed=$isCollapsed, parent=$parent, transitoryData=$transitoryData, readOnly=$readOnly)" } @Retention(AnnotationRetention.SOURCE) diff --git a/app/src/main/java/com/todoroo/astrid/service/TaskCompleter.kt b/app/src/main/java/com/todoroo/astrid/service/TaskCompleter.kt index 7cec2381d..d52fab2a1 100644 --- a/app/src/main/java/com/todoroo/astrid/service/TaskCompleter.kt +++ b/app/src/main/java/com/todoroo/astrid/service/TaskCompleter.kt @@ -46,6 +46,7 @@ class TaskCompleter @Inject internal constructor( } .filterNotNull() .filter { it.isCompleted != completionDate > 0 } + .filterNot { it.readOnly } .let { setComplete(it, completionDate) if (completed && !item.isRecurring) { diff --git a/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.kt b/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.kt index 1af8e5b4c..9e04be686 100644 --- a/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.kt +++ b/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.kt @@ -5,14 +5,7 @@ import com.todoroo.astrid.data.Task import kotlinx.coroutines.runBlocking import org.tasks.LocalBroadcastManager import org.tasks.caldav.VtodoCache -import org.tasks.data.CaldavAccount -import org.tasks.data.CaldavCalendar -import org.tasks.data.DeletionDao -import org.tasks.data.GoogleTaskAccount -import org.tasks.data.GoogleTaskDao -import org.tasks.data.GoogleTaskList -import org.tasks.data.TaskContainer -import org.tasks.data.TaskDao +import org.tasks.data.* import org.tasks.db.QueryUtils import org.tasks.db.SuspendDbUtils.chunkedMap import org.tasks.jobs.WorkManager @@ -34,14 +27,18 @@ class TaskDeleter @Inject constructor( suspend fun markDeleted(item: Task) = markDeleted(listOf(item.id)) suspend fun markDeleted(taskIds: List): List { - val ids: MutableSet = HashSet(taskIds) - ids.addAll(taskIds.chunkedMap(googleTaskDao::getChildren)) - ids.addAll(taskIds.chunkedMap(taskDao::getChildren)) + val ids = taskIds + .toSet() + .plus(taskIds.chunkedMap(googleTaskDao::getChildren)) + .plus(taskIds.chunkedMap(taskDao::getChildren)) + .let { taskDao.fetch(it.toList()) } + .filterNot { it.readOnly } + .map { it.id } deletionDao.markDeleted(ids) workManager.cleanup(ids) syncAdapters.sync() localBroadcastManager.broadcastRefresh() - return ids.chunkedMap(taskDao::fetch) + return taskDao.fetch(ids) } suspend fun clearCompleted(filter: Filter): Int { @@ -50,6 +47,7 @@ class TaskDeleter @Inject constructor( QueryUtils.removeOrder(QueryUtils.showHiddenAndCompleted(filter.originalSqlQuery))) val completed = taskDao.fetchTasks(preferences, deleteFilter) .filter(TaskContainer::isCompleted) + .filterNot(TaskContainer::isReadOnly) .map(TaskContainer::getId) .toMutableList() completed.removeAll(deletionDao.hasRecurringAncestors(completed)) diff --git a/app/src/main/java/com/todoroo/astrid/service/TaskDuplicator.kt b/app/src/main/java/com/todoroo/astrid/service/TaskDuplicator.kt index a487c474c..a112c1c23 100644 --- a/app/src/main/java/com/todoroo/astrid/service/TaskDuplicator.kt +++ b/app/src/main/java/com/todoroo/astrid/service/TaskDuplicator.kt @@ -25,17 +25,16 @@ class TaskDuplicator @Inject constructor( ) { suspend fun duplicate(taskIds: List): List { - val result: MutableList = ArrayList() - val tasks = ArrayList(taskIds) - taskIds.dbchunk().forEach { - tasks.removeAll(googleTaskDao.getChildren(it)) - tasks.removeAll(taskDao.getChildren(it)) - } - for (task in taskDao.fetch(tasks)) { - result.add(clone(task, task.parent)) - } - localBroadcastManager.broadcastRefresh() - return result + return taskIds + .dbchunk() + .flatMap { + it.minus(googleTaskDao.getChildren(it).toSet()) + .minus(taskDao.getChildren(it).toSet()) + } + .let { taskDao.fetch(it) } + .filterNot { it.readOnly } + .map { clone(it, it.parent) } + .also { localBroadcastManager.broadcastRefresh() } } private suspend fun clone(clone: Task, parentId: Long): Task { diff --git a/app/src/main/java/com/todoroo/astrid/service/TaskMover.kt b/app/src/main/java/com/todoroo/astrid/service/TaskMover.kt index c9b312d81..3c012b1f7 100644 --- a/app/src/main/java/com/todoroo/astrid/service/TaskMover.kt +++ b/app/src/main/java/com/todoroo/astrid/service/TaskMover.kt @@ -10,12 +10,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import org.tasks.BuildConfig import org.tasks.LocalBroadcastManager import org.tasks.caldav.VtodoCache -import org.tasks.data.CaldavDao -import org.tasks.data.CaldavTask -import org.tasks.data.GoogleTask -import org.tasks.data.GoogleTaskDao -import org.tasks.data.GoogleTaskListDao -import org.tasks.data.TaskDao +import org.tasks.data.* import org.tasks.db.DbUtils.dbchunk import org.tasks.preferences.Preferences import org.tasks.sync.SyncAdapters @@ -49,19 +44,21 @@ class TaskMover @Inject constructor( } suspend fun move(ids: List, selectedList: Filter) { - val tasks = ArrayList(ids) - ids.dbchunk().forEach { - tasks.removeAll(googleTaskDao.getChildren(it)) - tasks.removeAll(taskDao.getChildren(it)) - } - taskDao.setParent(0, tasks) - for (task in taskDao.fetch(tasks)) { - performMove(task, selectedList) - } + val tasks = ids + .dbchunk() + .flatMap { + it.minus(googleTaskDao.getChildren(it).toSet()) + .minus(taskDao.getChildren(it).toSet()) + } + .let { taskDao.fetch(it) } + .filterNot { it.readOnly } + val taskIds = tasks.map { it.id } + taskDao.setParent(0, ids.intersect(taskIds.toSet()).toList()) + tasks.forEach { performMove(it, selectedList) } if (selectedList is CaldavFilter) { caldavDao.updateParents(selectedList.uuid) } - tasks.dbchunk().forEach { + taskIds.dbchunk().forEach { taskDao.touch(it) } localBroadcastManager.broadcastRefresh() diff --git a/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt b/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt index bab01564f..3b5bbe8af 100644 --- a/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt +++ b/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt @@ -30,6 +30,8 @@ import org.tasks.billing.Inventory import org.tasks.caldav.iCalendar.Companion.fromVtodo import org.tasks.caldav.property.* import org.tasks.caldav.property.PropertyUtils.register +import org.tasks.caldav.property.ShareAccess.Companion.NOT_SHARED +import org.tasks.caldav.property.ShareAccess.Companion.NO_ACCESS import org.tasks.caldav.property.ShareAccess.Companion.READ import org.tasks.caldav.property.ShareAccess.Companion.READ_WRITE import org.tasks.caldav.property.ShareAccess.Companion.SHARED_OWNER @@ -427,9 +429,9 @@ class CaldavSynchronizer @Inject constructor( get() { this[ShareAccess::class.java]?.let { return when (it.access) { - SHARED_OWNER -> ACCESS_OWNER + NOT_SHARED, SHARED_OWNER -> ACCESS_OWNER READ_WRITE -> ACCESS_READ_WRITE - READ -> ACCESS_READ_ONLY + NO_ACCESS, READ -> ACCESS_READ_ONLY else -> ACCESS_UNKNOWN } } diff --git a/app/src/main/java/org/tasks/caldav/iCalendar.kt b/app/src/main/java/org/tasks/caldav/iCalendar.kt index c42e3088a..f4b60a81b 100644 --- a/app/src/main/java/org/tasks/caldav/iCalendar.kt +++ b/app/src/main/java/org/tasks/caldav/iCalendar.kt @@ -27,6 +27,7 @@ import org.tasks.caldav.extensions.toVAlarms import org.tasks.data.* import org.tasks.data.Alarm.Companion.TYPE_RANDOM import org.tasks.data.Alarm.Companion.TYPE_SNOOZE +import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_ONLY import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.date.DateTimeUtils.toDateTime import org.tasks.date.DateTimeUtils.toLocal @@ -175,6 +176,7 @@ class iCalendar @Inject constructor( ) { val task = existing?.task?.let { taskDao.fetch(it) } ?: taskCreator.createWithValues("").apply { + readOnly = calendar.access == ACCESS_READ_ONLY taskDao.createNew(this) existing?.task = id } diff --git a/app/src/main/java/org/tasks/caldav/property/ShareAccess.kt b/app/src/main/java/org/tasks/caldav/property/ShareAccess.kt index c5911eb01..a43c3bf9c 100644 --- a/app/src/main/java/org/tasks/caldav/property/ShareAccess.kt +++ b/app/src/main/java/org/tasks/caldav/property/ShareAccess.kt @@ -16,6 +16,7 @@ data class ShareAccess(val access: Property.Name): Property { val SHARED_OWNER = Property.Name(XmlUtils.NS_WEBDAV, "shared-owner") val READ_WRITE = Property.Name(XmlUtils.NS_WEBDAV, "read-write") val NOT_SHARED = Property.Name(XmlUtils.NS_WEBDAV, "not-shared") + val NO_ACCESS = Property.Name(XmlUtils.NS_WEBDAV, "no-access") val READ = Property.Name(XmlUtils.NS_WEBDAV, "read") } diff --git a/app/src/main/java/org/tasks/data/OpenTaskDao.kt b/app/src/main/java/org/tasks/data/OpenTaskDao.kt index d72e643ba..c0b55a587 100644 --- a/app/src/main/java/org/tasks/data/OpenTaskDao.kt +++ b/app/src/main/java/org/tasks/data/OpenTaskDao.kt @@ -56,7 +56,12 @@ open class OpenTaskDao @Inject constructor( url = it.getString(CommonSyncColumns._SYNC_ID), ctag = it.getString(TaskLists.SYNC_VERSION) ?.let(::JSONObject) - ?.getString("value") + ?.getString("value"), + access = when (it.getInt(TaskLists.ACCESS_LEVEL)) { + TaskLists.ACCESS_LEVEL_OWNER -> CaldavCalendar.ACCESS_OWNER + TaskLists.ACCESS_LEVEL_READ -> CaldavCalendar.ACCESS_READ_ONLY + else -> CaldavCalendar.ACCESS_READ_WRITE + }, ) ) } @@ -174,6 +179,7 @@ open class OpenTaskDao @Inject constructor( color = color, name = name, account = account, + access = access, ) } } diff --git a/app/src/main/java/org/tasks/data/TaskContainer.java b/app/src/main/java/org/tasks/data/TaskContainer.java index bc41dd6e3..4ce0a47e6 100644 --- a/app/src/main/java/org/tasks/data/TaskContainer.java +++ b/app/src/main/java/org/tasks/data/TaskContainer.java @@ -108,6 +108,10 @@ public class TaskContainer { targetIndent = indent; } + public boolean isReadOnly() { + return task.getReadOnly(); + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/app/src/main/java/org/tasks/db/Migrations.kt b/app/src/main/java/org/tasks/db/Migrations.kt index 5c77e7704..99e48302a 100644 --- a/app/src/main/java/org/tasks/db/Migrations.kt +++ b/app/src/main/java/org/tasks/db/Migrations.kt @@ -16,6 +16,7 @@ import org.tasks.data.Alarm.Companion.TYPE_REL_END import org.tasks.data.Alarm.Companion.TYPE_REL_START import org.tasks.data.Alarm.Companion.TYPE_SNOOZE import org.tasks.data.CaldavAccount.Companion.SERVER_UNKNOWN +import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_ONLY import org.tasks.data.OpenTaskDao.Companion.getLong import org.tasks.extensions.getLongOrNull import org.tasks.extensions.getString @@ -580,6 +581,13 @@ object Migrations { } } + private val MIGRATION_86_87 = object : Migration(86, 87) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE `tasks` ADD COLUMN `read_only` INTEGER NOT NULL DEFAULT 0") + database.execSQL("UPDATE `tasks` SET `read_only` = 1 WHERE `_id` IN (SELECT `cd_task` FROM `caldav_tasks` INNER JOIN `caldav_lists` ON `caldav_tasks`.`cd_calendar` = `caldav_lists`.`cdl_uuid` WHERE `cdl_access` = $ACCESS_READ_ONLY)") + } + } + fun migrations(fileStorage: FileStorage) = arrayOf( MIGRATION_35_36, MIGRATION_36_37, @@ -622,6 +630,7 @@ object Migrations { MIGRATION_82_83, MIGRATION_84_85, MIGRATION_85_86, + MIGRATION_86_87, ) private fun noop(from: Int, to: Int): Migration = object : Migration(from, to) { diff --git a/app/src/main/java/org/tasks/dialogs/FilterPickerViewModel.kt b/app/src/main/java/org/tasks/dialogs/FilterPickerViewModel.kt index 1116fb12f..3997b0d79 100644 --- a/app/src/main/java/org/tasks/dialogs/FilterPickerViewModel.kt +++ b/app/src/main/java/org/tasks/dialogs/FilterPickerViewModel.kt @@ -58,7 +58,7 @@ class FilterPickerViewModel @Inject constructor( private fun refresh() = viewModelScope.launch { val items = if (listsOnly) { - filterProvider.listPickerItems() + filterProvider.listPickerItems().filterNot { it is Filter && it.isReadOnly } } else { filterProvider.filterPickerItems() } diff --git a/app/src/main/java/org/tasks/fragments/CommentBarFragment.kt b/app/src/main/java/org/tasks/fragments/CommentBarFragment.kt index a17c74db4..a1402c94d 100644 --- a/app/src/main/java/org/tasks/fragments/CommentBarFragment.kt +++ b/app/src/main/java/org/tasks/fragments/CommentBarFragment.kt @@ -65,7 +65,10 @@ class CommentBarFragment : Fragment() { } commentField.setHorizontallyScrolling(false) commentField.maxLines = Int.MAX_VALUE - if (preferences.getBoolean(R.string.p_show_task_edit_comments, true)) { + if ( + preferences.getBoolean(R.string.p_show_task_edit_comments, true) && + viewModel.isWritable + ) { commentBar.visibility = View.VISIBLE } commentBar.setBackgroundColor(themeColor.primaryColor) diff --git a/app/src/main/java/org/tasks/notifications/NotificationManager.kt b/app/src/main/java/org/tasks/notifications/NotificationManager.kt index 3466beb41..9d5746548 100644 --- a/app/src/main/java/org/tasks/notifications/NotificationManager.kt +++ b/app/src/main/java/org/tasks/notifications/NotificationManager.kt @@ -317,7 +317,7 @@ class NotificationManager @Inject constructor( .setOnlyAlertOnce(false) .setShowWhen(true) .setTicker(taskTitle) - val intent = NotificationActivity.newIntent(context, taskTitle.toString(), id) + val intent = NotificationActivity.newIntent(context, taskTitle.toString(), id, task.readOnly) builder.setContentIntent( PendingIntent.getActivity( context, @@ -360,7 +360,9 @@ class NotificationManager @Inject constructor( PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT ) val wearableExtender = NotificationCompat.WearableExtender() - wearableExtender.addAction(completeAction) + if (!task.readOnly) { + wearableExtender.addAction(completeAction) + } for (snoozeOption in SnoozeDialog.getSnoozeOptions(preferences)) { val timestamp = snoozeOption.dateTime.millis val wearableIntent = SnoozeActivity.newIntent(context, id) @@ -379,8 +381,10 @@ class NotificationManager @Inject constructor( wearablePendingIntent) .build()) } + if (!task.readOnly) { + builder.addAction(completeAction) + } return builder - .addAction(completeAction) .addAction( R.drawable.ic_snooze_white_24dp, context.getString(R.string.rmd_NoA_snooze), diff --git a/app/src/main/java/org/tasks/opentasks/OpenTasksSynchronizer.kt b/app/src/main/java/org/tasks/opentasks/OpenTasksSynchronizer.kt index c087d6e23..785b198ce 100644 --- a/app/src/main/java/org/tasks/opentasks/OpenTasksSynchronizer.kt +++ b/app/src/main/java/org/tasks/opentasks/OpenTasksSynchronizer.kt @@ -15,12 +15,7 @@ import org.tasks.analytics.Constants import org.tasks.analytics.Firebase import org.tasks.billing.Inventory import org.tasks.caldav.iCalendar -import org.tasks.data.CaldavAccount -import org.tasks.data.CaldavCalendar -import org.tasks.data.CaldavDao -import org.tasks.data.CaldavTask -import org.tasks.data.MyAndroidTask -import org.tasks.data.OpenTaskDao +import org.tasks.data.* import org.tasks.data.OpenTaskDao.Companion.filterActive import org.tasks.data.OpenTaskDao.Companion.isDavx5 import org.tasks.data.OpenTaskDao.Companion.isDecSync @@ -90,7 +85,9 @@ class OpenTasksSynchronizer @Inject constructor( .forEach { taskDeleter.delete(it) } lists.forEach { val calendar = toLocalCalendar(it) - pushChanges(account, calendar, it.id) + if (calendar.access != CaldavCalendar.ACCESS_READ_ONLY) { + pushChanges(account, calendar, it.id) + } fetchChanges(account, calendar, it.ctag, it.id) } } @@ -102,9 +99,14 @@ class OpenTasksSynchronizer @Inject constructor( caldavDao.insert(local) Timber.d("Created calendar: $local") localBroadcastManager.broadcastRefreshList() - } else if (local.name != remote.name || local.color != remote.color) { + } else if ( + local.name != remote.name || + local.color != remote.color || + local.access != remote.access + ) { local.color = remote.color local.name = remote.name + local.access = remote.access caldavDao.update(local) Timber.d("Updated calendar: $local") localBroadcastManager.broadcastRefreshList() diff --git a/app/src/main/java/org/tasks/preferences/DefaultFilterProvider.kt b/app/src/main/java/org/tasks/preferences/DefaultFilterProvider.kt index aade4d7c2..282dd5d47 100644 --- a/app/src/main/java/org/tasks/preferences/DefaultFilterProvider.kt +++ b/app/src/main/java/org/tasks/preferences/DefaultFilterProvider.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.runBlocking import org.tasks.R import org.tasks.Strings.isNullOrEmpty import org.tasks.data.* +import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_ONLY import org.tasks.filters.PlaceFilter import timber.log.Timber import javax.inject.Inject @@ -48,6 +49,7 @@ class DefaultFilterProvider @Inject constructor( suspend fun getDefaultList() = getFilterFromPreference(preferences.getStringValue(R.string.p_default_list), null) + ?.takeIf { it.isWritable } ?: getAnyList() suspend fun getLastViewedFilter() = getFilterFromPreference(R.string.p_last_viewed_list) @@ -77,7 +79,7 @@ class DefaultFilterProvider @Inject constructor( private suspend fun getAnyList(): Filter { val filter = googleTaskListDao.getAllLists().getOrNull(0)?.let(::GtasksFilter) - ?: caldavDao.getCalendars().getOrElse(0) { caldavDao.getLocalList(context) }.let(::CaldavFilter) + ?: caldavDao.getCalendars().filterNot { it.access == ACCESS_READ_ONLY }.getOrElse(0) { caldavDao.getLocalList(context) }.let(::CaldavFilter) defaultList = filter return filter } @@ -158,6 +160,7 @@ class DefaultFilterProvider @Inject constructor( } } else if (task.hasTransitory(CaldavTask.KEY)) { val caldav = caldavDao.getCalendarByUuid(task.getTransitory(CaldavTask.KEY)!!) + ?.takeIf { it.access != ACCESS_READ_ONLY } if (caldav != null) { originalList = CaldavFilter(caldav) } diff --git a/app/src/main/java/org/tasks/reminders/NotificationActivity.kt b/app/src/main/java/org/tasks/reminders/NotificationActivity.kt index 5724628bf..05590b2e4 100644 --- a/app/src/main/java/org/tasks/reminders/NotificationActivity.kt +++ b/app/src/main/java/org/tasks/reminders/NotificationActivity.kt @@ -32,6 +32,9 @@ class NotificationActivity : InjectingAppCompatActivity(), NotificationDialog.No var fragment = fragmentManager.findFragmentByTag(FRAG_TAG_NOTIFICATION_FRAGMENT) as NotificationDialog? if (fragment == null) { fragment = NotificationDialog() + fragment.arguments = Bundle().apply { + putBoolean(EXTRA_READ_ONLY, intent.getBooleanExtra(EXTRA_READ_ONLY, false)) + } fragment.show(fragmentManager, FRAG_TAG_NOTIFICATION_FRAGMENT) } fragment.setTitle(intent.getStringExtra(EXTRA_TITLE)) @@ -69,12 +72,14 @@ class NotificationActivity : InjectingAppCompatActivity(), NotificationDialog.No companion object { const val EXTRA_TITLE = "extra_title" const val EXTRA_TASK_ID = "extra_task_id" + const val EXTRA_READ_ONLY = "extra_read_only" private const val FRAG_TAG_NOTIFICATION_FRAGMENT = "frag_tag_notification_fragment" - fun newIntent(context: Context?, title: String?, id: Long): Intent { + fun newIntent(context: Context?, title: String?, id: Long, readOnly: Boolean): Intent { val intent = Intent(context, NotificationActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK intent.putExtra(EXTRA_TASK_ID, id) intent.putExtra(EXTRA_TITLE, title) + intent.putExtra(EXTRA_READ_ONLY, readOnly) return intent } } diff --git a/app/src/main/java/org/tasks/reminders/NotificationDialog.java b/app/src/main/java/org/tasks/reminders/NotificationDialog.java index 96c41e3b1..22bf9a159 100644 --- a/app/src/main/java/org/tasks/reminders/NotificationDialog.java +++ b/app/src/main/java/org/tasks/reminders/NotificationDialog.java @@ -1,18 +1,24 @@ package org.tasks.reminders; +import static org.tasks.reminders.NotificationActivity.EXTRA_READ_ONLY; import static java.util.Arrays.asList; import android.app.Dialog; import android.content.DialogInterface; import android.os.Bundle; + import androidx.annotation.NonNull; import androidx.fragment.app.DialogFragment; -import dagger.hilt.android.AndroidEntryPoint; -import java.util.List; -import javax.inject.Inject; + import org.tasks.R; import org.tasks.dialogs.DialogBuilder; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; + @AndroidEntryPoint public class NotificationDialog extends DialogFragment { @@ -30,11 +36,11 @@ public class NotificationDialog extends DialogFragment { getString(R.string.rmd_NoA_done)); handler = (NotificationHandler) getActivity(); - + boolean readOnly = getArguments().getBoolean(EXTRA_READ_ONLY); return dialogBuilder .newDialog(title) .setItems( - items, + readOnly ? items.subList(0, 2) : items, (dialog, which) -> { switch (which) { case 0: diff --git a/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt b/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt index 92109152c..92536db77 100644 --- a/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt +++ b/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt @@ -178,6 +178,10 @@ class TaskEditViewModel @Inject constructor( } } + val isReadOnly = task.readOnly + + val isWritable = !isReadOnly + fun hasChanges(): Boolean = (task.title != title || (isNew && title?.isNotBlank() == true)) || task.isCompleted != completed || @@ -221,7 +225,7 @@ class TaskEditViewModel @Inject constructor( if (cleared) { return@withContext false } - if (!hasChanges()) { + if (!hasChanges() || isReadOnly) { discard(remove) return@withContext false } diff --git a/app/src/main/java/org/tasks/widget/TasksWidget.kt b/app/src/main/java/org/tasks/widget/TasksWidget.kt index f619524c3..8a0e02d64 100644 --- a/app/src/main/java/org/tasks/widget/TasksWidget.kt +++ b/app/src/main/java/org/tasks/widget/TasksWidget.kt @@ -117,6 +117,10 @@ class TasksWidget : AppWidgetProvider() { setRipple( remoteViews, color, R.id.widget_button, R.id.widget_change_list, R.id.widget_reconfigure) remoteViews.setOnClickPendingIntent(R.id.widget_title, getOpenListIntent(context, filter, id)) + remoteViews.setViewVisibility( + R.id.widget_button, + if (filter.isWritable) View.VISIBLE else View.GONE + ) remoteViews.setOnClickPendingIntent(R.id.widget_button, getNewTaskIntent(context, filter, id)) remoteViews.setOnClickPendingIntent(R.id.widget_change_list, getChooseListIntent(context, filter, id)) remoteViews.setOnClickPendingIntent( diff --git a/deps_fdroid.txt b/deps_fdroid.txt index 9562e4969..a6876dc66 100644 --- a/deps_fdroid.txt +++ b/deps_fdroid.txt @@ -75,7 +75,7 @@ +| | \--- org.jetbrains.kotlin:kotlin-stdlib:1.6.20 -> 1.7.20 (*) +| \--- org.ogce:xpp3:1.1.6 +| \--- jakarta-regexp:jakarta-regexp:1.4 -++--- com.github.tasks:ical4android:2fb465b +++--- com.github.tasks:ical4android:ce7919d +| +--- org.mnode.ical4j:ical4j:3.2.5 +| | +--- javax.cache:cache-api:1.1.1 +| | +--- org.threeten:threeten-extra:1.7.0 diff --git a/deps_googleplay.txt b/deps_googleplay.txt index 2bb963b46..9a9842490 100644 --- a/deps_googleplay.txt +++ b/deps_googleplay.txt @@ -322,7 +322,7 @@ +| | \--- org.jetbrains.kotlin:kotlin-stdlib:1.6.20 -> 1.7.20 (*) +| \--- org.ogce:xpp3:1.1.6 +| \--- jakarta-regexp:jakarta-regexp:1.4 -++--- com.github.tasks:ical4android:2fb465b +++--- com.github.tasks:ical4android:ce7919d +| +--- org.mnode.ical4j:ical4j:3.2.5 +| | +--- javax.cache:cache-api:1.1.1 +| | +--- org.threeten:threeten-extra:1.7.0