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/MainActivity.kt b/ground/src/main/java/com/google/android/ground/MainActivity.kt index 7c600794cf..d085ad4460 100644 --- a/ground/src/main/java/com/google/android/ground/MainActivity.kt +++ b/ground/src/main/java/com/google/android/ground/MainActivity.kt @@ -27,17 +27,13 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.platform.ComposeView import androidx.core.view.WindowInsetsCompat import androidx.lifecycle.lifecycleScope +import androidx.navigation.NavDirections import androidx.navigation.fragment.NavHostFragment import com.google.android.ground.databinding.MainActBinding import com.google.android.ground.repository.UserRepository import com.google.android.ground.system.ActivityCallback import com.google.android.ground.system.ActivityStreams import com.google.android.ground.ui.common.BackPressListener -import com.google.android.ground.ui.common.FinishApp -import com.google.android.ground.ui.common.NavigateTo -import com.google.android.ground.ui.common.NavigateUp -import com.google.android.ground.ui.common.NavigationRequest -import com.google.android.ground.ui.common.Navigator import com.google.android.ground.ui.common.ViewModelFactory import com.google.android.ground.ui.common.modalSpinner import com.google.android.ground.ui.home.HomeScreenFragmentDirections @@ -57,7 +53,6 @@ import timber.log.Timber class MainActivity : AbstractActivity() { @Inject lateinit var activityStreams: ActivityStreams @Inject lateinit var viewModelFactory: ViewModelFactory - @Inject lateinit var navigator: Navigator @Inject lateinit var userRepository: UserRepository private lateinit var viewModel: MainViewModel @@ -79,8 +74,6 @@ class MainActivity : AbstractActivity() { } } - lifecycleScope.launch { navigator.getNavigateRequests().collect { onNavigate(it) } } - val binding = MainActBinding.inflate(layoutInflater) setContentView(binding.root) @@ -100,16 +93,16 @@ class MainActivity : AbstractActivity() { showPermissionDeniedDialog(viewGroup) } MainUiState.OnUserSignedOut -> { - navigator.navigate(SignInFragmentDirections.showSignInScreen()) + navigateTo(SignInFragmentDirections.showSignInScreen()) } MainUiState.TosNotAccepted -> { - navigator.navigate(SignInFragmentDirections.showTermsOfService(false)) + navigateTo(SignInFragmentDirections.showTermsOfService(false)) } MainUiState.NoActiveSurvey -> { - navigator.navigate(SurveySelectorFragmentDirections.showSurveySelectorScreen(true)) + navigateTo(SurveySelectorFragmentDirections.showSurveySelectorScreen(true)) } MainUiState.ShowHomeScreen -> { - navigator.navigate(HomeScreenFragmentDirections.showHomeScreen()) + navigateTo(HomeScreenFragmentDirections.showHomeScreen()) } MainUiState.OnUserSigningIn -> { onSignInProgress(true) @@ -137,7 +130,7 @@ class MainActivity : AbstractActivity() { }, onCloseApp = { showDialog = false - navigator.finishApp() + finish() }, ) } @@ -152,14 +145,6 @@ class MainActivity : AbstractActivity() { viewModel.windowInsets.postValue(insets) } - private fun onNavigate(navRequest: NavigationRequest) { - when (navRequest) { - is NavigateTo -> navHostFragment.navController.navigate(navRequest.directions) - is NavigateUp -> navigateUp() - is FinishApp -> finish() - } - } - /** * The Android permissions API requires this callback to live in an Activity; here we dispatch the * result back to the PermissionManager for handling. @@ -225,4 +210,8 @@ class MainActivity : AbstractActivity() { signInProgressDialog = null } } + + private fun navigateTo(directions: NavDirections) { + navHostFragment.navController.navigate(directions) + } } 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/common/NavigationRequest.kt b/ground/src/main/java/com/google/android/ground/ui/common/NavigationRequest.kt deleted file mode 100644 index 86ca870c59..0000000000 --- a/ground/src/main/java/com/google/android/ground/ui/common/NavigationRequest.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.ground.ui.common - -import androidx.navigation.NavDirections - -sealed interface NavigationRequest - -data class NavigateTo(val directions: NavDirections) : NavigationRequest - -data object NavigateUp : NavigationRequest - -data object FinishApp : NavigationRequest diff --git a/ground/src/main/java/com/google/android/ground/ui/common/Navigator.kt b/ground/src/main/java/com/google/android/ground/ui/common/Navigator.kt deleted file mode 100644 index 87375956ba..0000000000 --- a/ground/src/main/java/com/google/android/ground/ui/common/Navigator.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2018 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.ground.ui.common - -import androidx.navigation.NavDirections -import com.google.android.ground.coroutines.MainDispatcher -import com.google.android.ground.coroutines.MainScope -import javax.inject.Inject -import javax.inject.Singleton -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.shareIn -import kotlinx.coroutines.launch - -/** - * Responsible for abstracting navigation from fragment to fragment. Exposes various actions to - * ViewModels that cause a NavDirections to be emitted to the observer (in this case, the - * [com.google.android.ground.MainActivity], which is expected to pass it to the current - * [androidx.navigation.NavController]. - */ -@Singleton -class Navigator -@Inject -constructor( - @MainDispatcher private val dispatcher: CoroutineDispatcher, - @MainScope private val coroutineScope: CoroutineScope, -) { - private val _navigateRequests = MutableSharedFlow() - - /** Stream of navigation requests for fulfillment by the view layer. */ - fun getNavigateRequests(): SharedFlow = - _navigateRequests.shareIn(coroutineScope, SharingStarted.Lazily, replay = 0) - - /** Navigates up one level on the back stack. */ - fun navigateUp() { - requestNavigation(NavigateUp) - } - - fun navigate(directions: NavDirections) { - requestNavigation(NavigateTo(directions)) - } - - fun finishApp() { - requestNavigation(FinishApp) - } - - private fun requestNavigation(request: NavigationRequest) { - CoroutineScope(dispatcher).launch { _navigateRequests.emit(request) } - } -} 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 f71e0f5fbd..9944a85234 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 @@ -206,8 +206,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() @@ -295,6 +293,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/java/com/google/android/ground/ui/offlineareas/OfflineAreasFragment.kt b/ground/src/main/java/com/google/android/ground/ui/offlineareas/OfflineAreasFragment.kt index 711c2366f5..da7adbf769 100644 --- a/ground/src/main/java/com/google/android/ground/ui/offlineareas/OfflineAreasFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/offlineareas/OfflineAreasFragment.kt @@ -29,10 +29,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController +import com.google.android.ground.R import com.google.android.ground.databinding.OfflineAreasFragBinding import com.google.android.ground.ui.common.AbstractFragment import com.google.android.ground.ui.theme.AppTheme import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch /** @@ -69,8 +71,11 @@ class OfflineAreasFragment : AbstractFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) lifecycleScope.launch { - viewModel.navigateToOfflineAreaSelector.collect { - findNavController().navigate(OfflineAreasFragmentDirections.showOfflineAreaSelector()) + viewModel.navigateToOfflineAreaSelector.collectLatest { + val navController = findNavController() + if (navController.currentDestination?.id == R.id.offline_areas_fragment) { + navController.navigate(OfflineAreasFragmentDirections.showOfflineAreaSelector()) + } } } } diff --git a/ground/src/main/java/com/google/android/ground/ui/offlineareas/OfflineAreasViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/offlineareas/OfflineAreasViewModel.kt index 5254bbbea5..e7fe017073 100644 --- a/ground/src/main/java/com/google/android/ground/ui/offlineareas/OfflineAreasViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/offlineareas/OfflineAreasViewModel.kt @@ -49,7 +49,7 @@ internal constructor(private val offlineAreaRepository: OfflineAreaRepository) : val showNoAreasMessage: LiveData val showProgressSpinner: LiveData - private val _navigateToOfflineAreaSelector = MutableSharedFlow(replay = 0) + private val _navigateToOfflineAreaSelector = MutableSharedFlow(replay = 1) val navigateToOfflineAreaSelector = _navigateToOfflineAreaSelector.asSharedFlow() init { 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" /> + + Sélectionnez une zone à télécharger Sélectionnez une carte de base pour l’utilisation hors ligne Télécharger - Télécharger + Caméra Prévisualisation Paramètres Zone téléchargée @@ -66,10 +66,11 @@ Date l’heure - État de synchronisation des données + État de synchronisation - Ajouter un point + Ajouter point Finir + Carte routière Satellite Relief @@ -89,7 +90,7 @@ Ce boulot n’a plus de tâches à effectuer Cette enquête est mal configurée (aucune condition de partage des données n’est disponible). Veuillez contacter l’organisateur de l’enquête. Impossible de collecter des données car l’utilisateur n’a que des droits de visualisation. - Imagerie cartographique hors ligne + Cartes hors ligne Aucune imagerie téléchargée Cacher ou afficher l’imagerie cartographique téléchargée Couches @@ -124,7 +125,7 @@ Veuillez autoriser l’appareil photo de cette application à soumettre des photos. Zone incomplète Initialisation… - Déplacez la carte et touchez « Ajouter un point » pour ajouter des points autour de la zone souhaitée. + Déplacez la carte et touchez « Ajouter point » pour ajouter des points autour de la zone souhaitée. Fermer Accord de partage de données Localisation sur la carte : @@ -150,7 +151,7 @@ À propos Conditions générales d’utilisation Ground est un projet communautaire open-source développé par Google et la FAO dans le cadre du « Forest Data Partnership » avec l’aide de SIG, Ecam et des contributeurs de la communauté open-source. \n\nIl est sous licence Apache License, Version 2.0. \n\nVoir autres Licenses - Echec + Échec Synchronisé Synchronisation Téléchargement en attente diff --git a/ground/src/test/java/com/google/android/ground/NavigationTestHelper.kt b/ground/src/test/java/com/google/android/ground/NavigationTestHelper.kt deleted file mode 100644 index 8691057257..0000000000 --- a/ground/src/test/java/com/google/android/ground/NavigationTestHelper.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.ground - -import androidx.navigation.NavDirections -import app.cash.turbine.test -import com.google.android.ground.ui.common.NavigateTo -import com.google.android.ground.ui.common.NavigationRequest -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.onSubscription -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.advanceUntilIdle - -/** - * Asserts that the given [expectedNavDirections] nav direction is emitted by [testSharedFlow] when - * the given block [runOnSubscription] is invoked. - * - * @param testSharedFlow SharedFlow under test - * @param expectedNavDirections NavDirections to validate after the flow has been subscribed - * @param runOnSubscription Runs the block when the given flow is subscribed - */ -suspend fun TestScope.testNavigateTo( - testSharedFlow: SharedFlow, - expectedNavDirections: NavDirections, - runOnSubscription: () -> Unit, -) { - testNavigateTo( - testSharedFlow, - { navDirections -> assertThat(navDirections).isEqualTo(expectedNavDirections) }, - runOnSubscription, - ) -} - -suspend fun TestScope.testNavigateTo( - testSharedFlow: SharedFlow, - validate: (NavDirections) -> Unit, - runOnSubscription: () -> Unit, -) { - testNavigate( - testSharedFlow, - { navigationRequest -> - assertThat(navigationRequest).isInstanceOf(NavigateTo::class.java) - validate((navigationRequest as NavigateTo).directions) - }, - runOnSubscription, - ) -} - -@OptIn(ExperimentalCoroutinesApi::class) -suspend fun TestScope.testNavigate( - testSharedFlow: SharedFlow, - validate: (NavigationRequest) -> Unit, - runOnSubscription: () -> Unit, -) { - testSharedFlow - .onSubscription { - runOnSubscription() - advanceUntilIdle() - } - .test { validate(expectMostRecentItem()) } -} - -@OptIn(ExperimentalCoroutinesApi::class) -suspend fun TestScope.testNoNavigation( - testSharedFlow: SharedFlow, - runOnSubscription: () -> Unit, -) { - testSharedFlow - .onSubscription { - runOnSubscription() - advanceUntilIdle() - } - .test { expectNoEvents() } -} - -/** - * Verifies that the given [NavDirections] is emitted if [expectedNavDirections] is provided, - * otherwise asserts that [NavDirections] should be emitted. - */ -suspend fun TestScope.testMaybeNavigateTo( - testSharedFlow: SharedFlow, - expectedNavDirections: NavDirections?, - runOnSubscription: () -> Unit, -) { - if (expectedNavDirections == null) { - testNoNavigation(testSharedFlow, runOnSubscription) - } else { - testNavigateTo(testSharedFlow, expectedNavDirections, runOnSubscription) - } -} diff --git a/ground/src/test/java/com/google/android/ground/ui/common/NavigatorTest.kt b/ground/src/test/java/com/google/android/ground/ui/common/NavigatorTest.kt deleted file mode 100644 index 190f6bcb53..0000000000 --- a/ground/src/test/java/com/google/android/ground/ui/common/NavigatorTest.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.ground.ui.common - -import com.google.android.ground.BaseHiltTest -import com.google.android.ground.testNavigate -import com.google.android.ground.testNavigateTo -import com.google.android.ground.ui.signin.SignInFragmentDirections -import com.google.common.truth.Truth.assertThat -import dagger.hilt.android.testing.HiltAndroidTest -import javax.inject.Inject -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -@HiltAndroidTest -@RunWith(RobolectricTestRunner::class) -class NavigatorTest : BaseHiltTest() { - - @Inject lateinit var navigator: Navigator - - @Test - fun `navigate up`() = runWithTestDispatcher { - testNavigate(navigator.getNavigateRequests(), { assertThat(it).isEqualTo(NavigateUp) }) { - navigator.navigateUp() - } - } - - @Test - fun `finish app`() = runWithTestDispatcher { - testNavigate(navigator.getNavigateRequests(), { assertThat(it).isEqualTo(FinishApp) }) { - navigator.finishApp() - } - } - - @Test - fun `navigate to`() = runWithTestDispatcher { - testNavigateTo(navigator.getNavigateRequests(), SignInFragmentDirections.showSignInScreen()) { - navigator.navigate(SignInFragmentDirections.showSignInScreen()) - } - } -} diff --git a/ground/src/test/java/com/google/android/ground/ui/datacollection/DataCollectionFragmentTest.kt b/ground/src/test/java/com/google/android/ground/ui/datacollection/DataCollectionFragmentTest.kt index 4abc5f1721..13ac2138cd 100644 --- a/ground/src/test/java/com/google/android/ground/ui/datacollection/DataCollectionFragmentTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/datacollection/DataCollectionFragmentTest.kt @@ -119,7 +119,7 @@ class DataCollectionFragmentTest : BaseHiltTest() { fun `Next click saves draft`() = runWithTestDispatcher { runner().inputText(TASK_1_RESPONSE).clickNextButton() - assertDraftSaved(listOf(TASK_1_VALUE_DELTA)) + assertDraftSaved(listOf(TASK_1_VALUE_DELTA), currentTaskId = TASK_ID_2) } @Test @@ -130,7 +130,7 @@ class DataCollectionFragmentTest : BaseHiltTest() { .selectMultipleChoiceOption(TASK_2_OPTION_LABEL) .clickPreviousButton() - assertDraftSaved(listOf(TASK_1_VALUE_DELTA, TASK_2_VALUE_DELTA)) + assertDraftSaved(listOf(TASK_1_VALUE_DELTA, TASK_2_VALUE_DELTA), currentTaskId = TASK_ID_1) } @Test @@ -159,6 +159,7 @@ class DataCollectionFragmentTest : BaseHiltTest() { JOB.id, true, SubmissionDeltasConverter.toString(expectedDeltas), + "", ) .build() .toBundle() @@ -273,7 +274,7 @@ class DataCollectionFragmentTest : BaseHiltTest() { ) } - private suspend fun assertDraftSaved(valueDeltas: List) { + 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() diff --git a/ground/src/test/java/com/google/android/ground/ui/surveyselector/SurveySelectorFragmentTest.kt b/ground/src/test/java/com/google/android/ground/ui/surveyselector/SurveySelectorFragmentTest.kt index 36e3695911..5f54f54d3a 100644 --- a/ground/src/test/java/com/google/android/ground/ui/surveyselector/SurveySelectorFragmentTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/surveyselector/SurveySelectorFragmentTest.kt @@ -34,8 +34,6 @@ import com.google.android.ground.domain.usecases.survey.ActivateSurveyUseCase import com.google.android.ground.model.SurveyListItem import com.google.android.ground.repository.SurveyRepository import com.google.android.ground.repository.UserRepository -import com.google.android.ground.ui.common.Navigator -import com.google.android.ground.ui.home.HomeScreenFragmentDirections import com.google.common.truth.Truth.assertThat import com.sharedtest.FakeData import com.sharedtest.system.auth.FakeAuthenticationManager @@ -63,7 +61,6 @@ import org.robolectric.shadows.ShadowToast @RunWith(RobolectricTestRunner::class) class SurveySelectorFragmentTest : BaseHiltTest() { - @BindValue @Mock lateinit var navigator: Navigator @BindValue @Mock lateinit var surveyRepository: SurveyRepository @BindValue @Mock lateinit var userRepository: UserRepository @BindValue @Mock lateinit var activateSurvey: ActivateSurveyUseCase @@ -157,7 +154,13 @@ class SurveySelectorFragmentTest : BaseHiltTest() { setSurveyList(listOf(TEST_SURVEY_1, TEST_SURVEY_2)) setLocalSurveys(listOf()) - setUpFragment() + + launchFragmentWithNavController( + fragmentArgs = bundleOf(Pair("shouldExitApp", false)), + destId = R.id.surveySelectorFragment, + navControllerCallback = { navController = it }, + ) + advanceUntilIdle() // Click second item onView(withId(R.id.recycler_view)) @@ -167,7 +170,7 @@ class SurveySelectorFragmentTest : BaseHiltTest() { // Assert survey is activated. verify(activateSurvey).invoke(TEST_SURVEY_2.id) // Assert that navigation to home screen was not requested - verify(navigator, times(0)).navigate(HomeScreenFragmentDirections.showHomeScreen()) + assertThat(navController.currentDestination?.id).isEqualTo(R.id.surveySelectorFragment) // Error toast message assertThat(ShadowToast.shownToastCount()).isEqualTo(1) } diff --git a/ground/src/test/java/com/google/android/ground/ui/tos/TermsOfServiceFragmentTest.kt b/ground/src/test/java/com/google/android/ground/ui/tos/TermsOfServiceFragmentTest.kt index e4710fbd1a..82e04c7dac 100644 --- a/ground/src/test/java/com/google/android/ground/ui/tos/TermsOfServiceFragmentTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/tos/TermsOfServiceFragmentTest.kt @@ -31,7 +31,6 @@ import com.google.android.ground.launchFragmentInHiltContainer import com.google.android.ground.launchFragmentWithNavController import com.google.android.ground.model.TermsOfService import com.google.android.ground.repository.TermsOfServiceRepository -import com.google.android.ground.ui.common.Navigator import com.google.common.truth.Truth.assertThat import com.sharedtest.persistence.remote.FakeRemoteDataStore import dagger.hilt.android.testing.HiltAndroidTest @@ -50,7 +49,6 @@ import org.robolectric.RobolectricTestRunner class TermsOfServiceFragmentTest : BaseHiltTest() { @Inject lateinit var fakeRemoteDataStore: FakeRemoteDataStore - @Inject lateinit var navigator: Navigator @Inject lateinit var termsOfServiceRepository: TermsOfServiceRepository @Inject lateinit var viewModel: TermsOfServiceViewModel private lateinit var navController: NavController