From 85ac64402e0c5f324407a7403d015d70cc0337b6 Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Thu, 5 Dec 2024 22:40:15 +0530 Subject: [PATCH] [Data collection] Restore position to last active task when restoring from draft (#2871) * Save currentTaskId to DraftSubmission * Set currentTaskId from draftSubmission to DataCollectionFragment * Update unit test * Add schema for db config 121 * Update unit tests * Update ktdoc formatting --------- Co-authored-by: Gino Miceli <228050+gino-m@users.noreply.github.com> --- .../121.json | 1124 +++++++++++++++++ .../java/com/google/android/ground/Config.kt | 2 +- .../model/submission/DraftSubmission.kt | 1 + .../persistence/local/room/LocalDatabase.kt | 2 + .../local/room/converter/ConverterExt.kt | 2 + .../room/entity/DraftSubmissionEntity.kt | 1 + .../ground/repository/SubmissionRepository.kt | 3 +- .../datacollection/DataCollectionFragment.kt | 2 +- .../datacollection/DataCollectionViewModel.kt | 3 +- .../ground/ui/home/HomeScreenFragment.kt | 1 + .../HomeScreenMapContainerFragment.kt | 2 + ground/src/main/res/navigation/nav_graph.xml | 8 + .../DataCollectionFragmentTest.kt | 9 +- 13 files changed, 1152 insertions(+), 8 deletions(-) create mode 100644 ground/schemas/com.google.android.ground.persistence.local.room.LocalDatabase/121.json diff --git a/ground/schemas/com.google.android.ground.persistence.local.room.LocalDatabase/121.json b/ground/schemas/com.google.android.ground.persistence.local.room.LocalDatabase/121.json new file mode 100644 index 0000000000..3ea4f7f410 --- /dev/null +++ b/ground/schemas/com.google.android.ground.persistence.local.room.LocalDatabase/121.json @@ -0,0 +1,1124 @@ +{ + "formatVersion": 1, + "database": { + "version": 121, + "identityHash": "9269a153abd81404333bc618b8ecf6bd", + "entities": [ + { + "tableName": "draft_submission", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `job_id` TEXT NOT NULL, `loi_id` TEXT, `survey_id` TEXT NOT NULL, `deltas` TEXT, `loi_name` TEXT, `current_task_id` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "jobId", + "columnName": "job_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loiId", + "columnName": "loi_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "surveyId", + "columnName": "survey_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deltas", + "columnName": "deltas", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loiName", + "columnName": "loi_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currentTaskId", + "columnName": "current_task_id", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_draft_submission_loi_id_job_id_survey_id", + "unique": false, + "columnNames": [ + "loi_id", + "job_id", + "survey_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_submission_loi_id_job_id_survey_id` ON `${TABLE_NAME}` (`loi_id`, `job_id`, `survey_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "location_of_interest", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `survey_id` TEXT NOT NULL, `job_id` TEXT NOT NULL, `state` INTEGER NOT NULL, `geometry` BLOB, `customId` TEXT NOT NULL, `submissionCount` INTEGER NOT NULL, `properties` TEXT NOT NULL, `isPredefined` INTEGER, `created_clientTimestamp` INTEGER NOT NULL, `created_serverTimestamp` INTEGER, `created_user_id` TEXT NOT NULL, `created_user_email` TEXT NOT NULL, `created_user_display_name` TEXT NOT NULL, `modified_clientTimestamp` INTEGER NOT NULL, `modified_serverTimestamp` INTEGER, `modified_user_id` TEXT NOT NULL, `modified_user_email` TEXT NOT NULL, `modified_user_display_name` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "surveyId", + "columnName": "survey_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "jobId", + "columnName": "job_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deletionState", + "columnName": "state", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "geometry", + "columnName": "geometry", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "customId", + "columnName": "customId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "submissionCount", + "columnName": "submissionCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "properties", + "columnName": "properties", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPredefined", + "columnName": "isPredefined", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created.clientTimestamp", + "columnName": "created_clientTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "created.serverTimestamp", + "columnName": "created_serverTimestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created.user.id", + "columnName": "created_user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "created.user.email", + "columnName": "created_user_email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "created.user.displayName", + "columnName": "created_user_display_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastModified.clientTimestamp", + "columnName": "modified_clientTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastModified.serverTimestamp", + "columnName": "modified_serverTimestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastModified.user.id", + "columnName": "modified_user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastModified.user.email", + "columnName": "modified_user_email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastModified.user.displayName", + "columnName": "modified_user_display_name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_location_of_interest_survey_id", + "unique": false, + "columnNames": [ + "survey_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_location_of_interest_survey_id` ON `${TABLE_NAME}` (`survey_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "location_of_interest_mutation", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `survey_id` TEXT NOT NULL, `type` INTEGER NOT NULL, `state` INTEGER NOT NULL, `retry_count` INTEGER NOT NULL, `last_error` TEXT NOT NULL, `user_id` TEXT NOT NULL, `client_timestamp` INTEGER NOT NULL, `location_of_interest_id` TEXT NOT NULL, `job_id` TEXT NOT NULL, `is_predefined` INTEGER, `collection_id` TEXT NOT NULL, `newGeometry` BLOB, `newProperties` TEXT NOT NULL, `newCustomId` TEXT NOT NULL, FOREIGN KEY(`location_of_interest_id`) REFERENCES `location_of_interest`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "surveyId", + "columnName": "survey_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "syncStatus", + "columnName": "state", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "retryCount", + "columnName": "retry_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastError", + "columnName": "last_error", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientTimestamp", + "columnName": "client_timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "locationOfInterestId", + "columnName": "location_of_interest_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "jobId", + "columnName": "job_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPredefined", + "columnName": "is_predefined", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "collectionId", + "columnName": "collection_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "newGeometry", + "columnName": "newGeometry", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "newProperties", + "columnName": "newProperties", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "newCustomId", + "columnName": "newCustomId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_location_of_interest_mutation_location_of_interest_id", + "unique": false, + "columnNames": [ + "location_of_interest_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_location_of_interest_mutation_location_of_interest_id` ON `${TABLE_NAME}` (`location_of_interest_id`)" + } + ], + "foreignKeys": [ + { + "table": "location_of_interest", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "location_of_interest_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "task", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `index` INTEGER NOT NULL, `task_type` INTEGER NOT NULL, `label` TEXT, `is_required` INTEGER NOT NULL, `job_id` TEXT, `is_add_loi_task` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`job_id`) REFERENCES `job`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "taskType", + "columnName": "task_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isRequired", + "columnName": "is_required", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "jobId", + "columnName": "job_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isAddLoiTask", + "columnName": "is_add_loi_task", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_task_job_id", + "unique": false, + "columnNames": [ + "job_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_task_job_id` ON `${TABLE_NAME}` (`job_id`)" + } + ], + "foreignKeys": [ + { + "table": "job", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "job_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "job", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `survey_id` TEXT, `strategy` TEXT NOT NULL, `style_color` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`survey_id`) REFERENCES `survey`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "surveyId", + "columnName": "survey_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "strategy", + "columnName": "strategy", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "style.color", + "columnName": "style_color", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_job_survey_id", + "unique": false, + "columnNames": [ + "survey_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_job_survey_id` ON `${TABLE_NAME}` (`survey_id`)" + } + ], + "foreignKeys": [ + { + "table": "survey", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "survey_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "multiple_choice", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`task_id` TEXT NOT NULL, `type` INTEGER NOT NULL, `has_other_option` INTEGER NOT NULL, PRIMARY KEY(`task_id`), FOREIGN KEY(`task_id`) REFERENCES `task`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "taskId", + "columnName": "task_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasOtherOption", + "columnName": "has_other_option", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "task_id" + ] + }, + "indices": [ + { + "name": "index_multiple_choice_task_id", + "unique": false, + "columnNames": [ + "task_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_multiple_choice_task_id` ON `${TABLE_NAME}` (`task_id`)" + } + ], + "foreignKeys": [ + { + "table": "task", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "task_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "option", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `code` TEXT NOT NULL, `label` TEXT NOT NULL, `task_id` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`task_id`) REFERENCES `task`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "code", + "columnName": "code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "taskId", + "columnName": "task_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_option_task_id", + "unique": false, + "columnNames": [ + "task_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_option_task_id` ON `${TABLE_NAME}` (`task_id`)" + } + ], + "foreignKeys": [ + { + "table": "task", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "task_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "survey", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `description` TEXT, `acl` TEXT, `data_sharing_terms` BLOB, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "acl", + "columnName": "acl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dataSharingTerms", + "columnName": "data_sharing_terms", + "affinity": "BLOB", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "submission", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `location_of_interest_id` TEXT NOT NULL, `job_id` TEXT NOT NULL, `state` INTEGER NOT NULL, `data` TEXT, `created_clientTimestamp` INTEGER NOT NULL, `created_serverTimestamp` INTEGER, `created_user_id` TEXT NOT NULL, `created_user_email` TEXT NOT NULL, `created_user_display_name` TEXT NOT NULL, `modified_clientTimestamp` INTEGER NOT NULL, `modified_serverTimestamp` INTEGER, `modified_user_id` TEXT NOT NULL, `modified_user_email` TEXT NOT NULL, `modified_user_display_name` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`location_of_interest_id`) REFERENCES `location_of_interest`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "locationOfInterestId", + "columnName": "location_of_interest_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "jobId", + "columnName": "job_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deletionState", + "columnName": "state", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "created.clientTimestamp", + "columnName": "created_clientTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "created.serverTimestamp", + "columnName": "created_serverTimestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created.user.id", + "columnName": "created_user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "created.user.email", + "columnName": "created_user_email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "created.user.displayName", + "columnName": "created_user_display_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastModified.clientTimestamp", + "columnName": "modified_clientTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastModified.serverTimestamp", + "columnName": "modified_serverTimestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastModified.user.id", + "columnName": "modified_user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastModified.user.email", + "columnName": "modified_user_email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastModified.user.displayName", + "columnName": "modified_user_display_name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_submission_location_of_interest_id_job_id_state", + "unique": false, + "columnNames": [ + "location_of_interest_id", + "job_id", + "state" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_submission_location_of_interest_id_job_id_state` ON `${TABLE_NAME}` (`location_of_interest_id`, `job_id`, `state`)" + } + ], + "foreignKeys": [ + { + "table": "location_of_interest", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "location_of_interest_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "submission_mutation", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `survey_id` TEXT NOT NULL, `type` INTEGER NOT NULL, `state` INTEGER NOT NULL, `retry_count` INTEGER NOT NULL, `last_error` TEXT NOT NULL, `user_id` TEXT NOT NULL, `client_timestamp` INTEGER NOT NULL, `location_of_interest_id` TEXT NOT NULL, `job_id` TEXT NOT NULL, `submission_id` TEXT NOT NULL, `collection_id` TEXT NOT NULL, `deltas` TEXT, FOREIGN KEY(`location_of_interest_id`) REFERENCES `location_of_interest`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`submission_id`) REFERENCES `submission`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "surveyId", + "columnName": "survey_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "syncStatus", + "columnName": "state", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "retryCount", + "columnName": "retry_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastError", + "columnName": "last_error", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientTimestamp", + "columnName": "client_timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "locationOfInterestId", + "columnName": "location_of_interest_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "jobId", + "columnName": "job_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submission_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "collectionId", + "columnName": "collection_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deltas", + "columnName": "deltas", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_submission_mutation_location_of_interest_id", + "unique": false, + "columnNames": [ + "location_of_interest_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_submission_mutation_location_of_interest_id` ON `${TABLE_NAME}` (`location_of_interest_id`)" + }, + { + "name": "index_submission_mutation_submission_id", + "unique": false, + "columnNames": [ + "submission_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_submission_mutation_submission_id` ON `${TABLE_NAME}` (`submission_id`)" + } + ], + "foreignKeys": [ + { + "table": "location_of_interest", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "location_of_interest_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "submission", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submission_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "offline_area", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `state` INTEGER NOT NULL, `north` REAL NOT NULL, `south` REAL NOT NULL, `east` REAL NOT NULL, `west` REAL NOT NULL, `min_zoom` INTEGER NOT NULL, `max_zoom` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "north", + "columnName": "north", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "south", + "columnName": "south", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "east", + "columnName": "east", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "west", + "columnName": "west", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "minZoom", + "columnName": "min_zoom", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxZoom", + "columnName": "max_zoom", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "user", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `email` TEXT NOT NULL, `display_name` TEXT NOT NULL, `photo_url` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "photoUrl", + "columnName": "photo_url", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "condition", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`parent_task_id` TEXT NOT NULL, `match_type` INTEGER NOT NULL, PRIMARY KEY(`parent_task_id`), FOREIGN KEY(`parent_task_id`) REFERENCES `task`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "parentTaskId", + "columnName": "parent_task_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "matchType", + "columnName": "match_type", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "parent_task_id" + ] + }, + "indices": [ + { + "name": "index_condition_parent_task_id", + "unique": false, + "columnNames": [ + "parent_task_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_condition_parent_task_id` ON `${TABLE_NAME}` (`parent_task_id`)" + } + ], + "foreignKeys": [ + { + "table": "task", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "parent_task_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "expression", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`parent_task_id` TEXT NOT NULL, `task_id` TEXT NOT NULL, `expression_type` INTEGER NOT NULL, `option_ids` TEXT, PRIMARY KEY(`parent_task_id`), FOREIGN KEY(`parent_task_id`) REFERENCES `condition`(`parent_task_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "parentTaskId", + "columnName": "parent_task_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "taskId", + "columnName": "task_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "expressionType", + "columnName": "expression_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "optionIds", + "columnName": "option_ids", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "parent_task_id" + ] + }, + "indices": [ + { + "name": "index_expression_parent_task_id", + "unique": false, + "columnNames": [ + "parent_task_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_expression_parent_task_id` ON `${TABLE_NAME}` (`parent_task_id`)" + } + ], + "foreignKeys": [ + { + "table": "condition", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "parent_task_id" + ], + "referencedColumns": [ + "parent_task_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, '9269a153abd81404333bc618b8ecf6bd')" + ] + } +} \ No newline at end of file diff --git a/ground/src/main/java/com/google/android/ground/Config.kt b/ground/src/main/java/com/google/android/ground/Config.kt index 860be54621..c2ccefa522 100644 --- a/ground/src/main/java/com/google/android/ground/Config.kt +++ b/ground/src/main/java/com/google/android/ground/Config.kt @@ -25,7 +25,7 @@ object Config { const val SHARED_PREFS_MODE = Context.MODE_PRIVATE // Local db settings. - const val DB_VERSION = 120 + const val DB_VERSION = 121 const val DB_NAME = "ground.db" // Firebase Cloud Firestore settings. diff --git a/ground/src/main/java/com/google/android/ground/model/submission/DraftSubmission.kt b/ground/src/main/java/com/google/android/ground/model/submission/DraftSubmission.kt index 9c1e7b5fab..45a1ed1311 100644 --- a/ground/src/main/java/com/google/android/ground/model/submission/DraftSubmission.kt +++ b/ground/src/main/java/com/google/android/ground/model/submission/DraftSubmission.kt @@ -23,4 +23,5 @@ data class DraftSubmission( val loiName: String?, val surveyId: String, val deltas: List, + val currentTaskId: String?, ) diff --git a/ground/src/main/java/com/google/android/ground/persistence/local/room/LocalDatabase.kt b/ground/src/main/java/com/google/android/ground/persistence/local/room/LocalDatabase.kt index cff284a137..a2efc49471 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/local/room/LocalDatabase.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/local/room/LocalDatabase.kt @@ -15,6 +15,7 @@ */ package com.google.android.ground.persistence.local.room +import androidx.room.AutoMigration import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters @@ -89,6 +90,7 @@ import com.google.android.ground.persistence.local.room.fields.TileSetEntityStat ], version = Config.DB_VERSION, exportSchema = true, + autoMigrations = [AutoMigration(from = 120, to = 121)], ) @TypeConverters( TaskEntityType::class, diff --git a/ground/src/main/java/com/google/android/ground/persistence/local/room/converter/ConverterExt.kt b/ground/src/main/java/com/google/android/ground/persistence/local/room/converter/ConverterExt.kt index c063be1fc1..9f5704f9d5 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/local/room/converter/ConverterExt.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/local/room/converter/ConverterExt.kt @@ -487,6 +487,7 @@ fun DraftSubmissionEntity.toModelObject(survey: Survey): DraftSubmission { loiName = loiName, surveyId = surveyId, deltas = SubmissionDeltasConverter.fromString(job, deltas), + currentTaskId = currentTaskId, ) } @@ -498,4 +499,5 @@ fun DraftSubmission.toLocalDataStoreObject() = loiName = loiName, surveyId = surveyId, deltas = SubmissionDeltasConverter.toString(deltas), + currentTaskId = currentTaskId, ) diff --git a/ground/src/main/java/com/google/android/ground/persistence/local/room/entity/DraftSubmissionEntity.kt b/ground/src/main/java/com/google/android/ground/persistence/local/room/entity/DraftSubmissionEntity.kt index ad03c4b5f0..597e0a318c 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/local/room/entity/DraftSubmissionEntity.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/local/room/entity/DraftSubmissionEntity.kt @@ -30,4 +30,5 @@ data class DraftSubmissionEntity( @ColumnInfo(name = "survey_id") val surveyId: String, @ColumnInfo(name = "deltas") val deltas: String?, @ColumnInfo(name = "loi_name") val loiName: String?, + @ColumnInfo(name = "current_task_id") val currentTaskId: String?, ) diff --git a/ground/src/main/java/com/google/android/ground/repository/SubmissionRepository.kt b/ground/src/main/java/com/google/android/ground/repository/SubmissionRepository.kt index 366ac7a198..464ee0d06a 100644 --- a/ground/src/main/java/com/google/android/ground/repository/SubmissionRepository.kt +++ b/ground/src/main/java/com/google/android/ground/repository/SubmissionRepository.kt @@ -87,9 +87,10 @@ constructor( surveyId: String, deltas: List, loiName: String?, + currentTaskId: String, ) { val newId = uuidGenerator.generateUuid() - val draft = DraftSubmission(newId, jobId, loiId, loiName, surveyId, deltas) + val draft = DraftSubmission(newId, jobId, loiId, loiName, surveyId, deltas, currentTaskId) localSubmissionStore.saveDraftSubmission(draftSubmission = draft) localValueStore.draftSubmissionId = newId } diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionFragment.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionFragment.kt index 2c0fb5c877..bf3f33838d 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionFragment.kt @@ -151,7 +151,7 @@ class DataCollectionFragment : AbstractFragment(), BackPressListener { if (currentAdapter == null || currentAdapter.tasks != tasks) { viewPager.adapter = viewPagerAdapterFactory.create(this, tasks) } - updateProgressBar(taskPosition, false) + viewPager.doOnLayout { onTaskChanged(taskPosition) } } private fun onTaskChanged(taskPosition: TaskPosition) { diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionViewModel.kt index 48486e92a8..62edece914 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionViewModel.kt @@ -198,8 +198,6 @@ internal constructor( /** Moves back to the previous task in the sequence if the current value is valid or empty. */ suspend fun onPreviousClicked(taskViewModel: AbstractTaskViewModel) { - check(getPositionInTaskSequence().first != 0) - val task = taskViewModel.task val taskValue = taskViewModel.taskTaskData.firstOrNull() @@ -287,6 +285,7 @@ internal constructor( surveyId = surveyId, deltas = getDeltas(), loiName = customLoiName, + currentTaskId = currentTaskId.value, ) } } diff --git a/ground/src/main/java/com/google/android/ground/ui/home/HomeScreenFragment.kt b/ground/src/main/java/com/google/android/ground/ui/home/HomeScreenFragment.kt index c14ff8fcd9..0212e5a1f8 100644 --- a/ground/src/main/java/com/google/android/ground/ui/home/HomeScreenFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/home/HomeScreenFragment.kt @@ -115,6 +115,7 @@ class HomeScreenFragment : draft.jobId, true, SubmissionDeltasConverter.toString(draft.deltas), + draft.currentTaskId ?: "", ) ) diff --git a/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerFragment.kt b/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerFragment.kt index ec67ae07a4..c9bbf82fdf 100644 --- a/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerFragment.kt @@ -304,6 +304,7 @@ class HomeScreenMapContainerFragment : AbstractMapContainerFragment() { cardUiData.loi.job.id, false, null, + "", ) ) is MapCardUiData.AddLoiCardUiData -> @@ -315,6 +316,7 @@ class HomeScreenMapContainerFragment : AbstractMapContainerFragment() { cardUiData.job.id, false, null, + "", ) ) } diff --git a/ground/src/main/res/navigation/nav_graph.xml b/ground/src/main/res/navigation/nav_graph.xml index c11f5e671d..4f6895a9a8 100644 --- a/ground/src/main/res/navigation/nav_graph.xml +++ b/ground/src/main/res/navigation/nav_graph.xml @@ -106,6 +106,10 @@ android:name="draftValues" type="string" app:nullable="true" /> + + ) { + private suspend fun assertDraftSaved(valueDeltas: List, currentTaskId: String) { val draftId = submissionRepository.getDraftSubmissionsId() assertThat(draftId).isNotEmpty() @@ -288,6 +289,7 @@ class DataCollectionFragmentTest : BaseHiltTest() { loiName = LOCATION_OF_INTEREST_NAME, surveyId = SURVEY.id, deltas = valueDeltas, + currentTaskId = currentTaskId, ) ) } @@ -314,6 +316,7 @@ class DataCollectionFragmentTest : BaseHiltTest() { JOB.id, false, null, + "", ) .build() .toBundle()