diff --git a/app/build.gradle b/app/build.gradle index baabb18d2..e863910ef 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -180,3 +180,7 @@ private def setupFirebaseConfigFields(buildType) { buildType.manifestPlaceholders = [fcmEnabled: firebaseEnabled && cloudMessagingEnabled] } + +ksp { + arg("room.schemaLocation", "$projectDir/schemas") +} diff --git a/app/schemas/org.openedx.app.room.AppDatabase/1.json b/app/schemas/org.openedx.app.room.AppDatabase/1.json new file mode 100644 index 000000000..c249fa741 --- /dev/null +++ b/app/schemas/org.openedx.app.room.AppDatabase/1.json @@ -0,0 +1,772 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "bcac519e74e751a75f3e6fa5d39ac5a3", + "entities": [ + { + "tableName": "course_discovery_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `blocksUrl` TEXT NOT NULL, `courseId` TEXT NOT NULL, `effort` TEXT NOT NULL, `enrollmentStart` TEXT NOT NULL, `enrollmentEnd` TEXT NOT NULL, `hidden` INTEGER NOT NULL, `invitationOnly` INTEGER NOT NULL, `mobileAvailable` INTEGER NOT NULL, `name` TEXT NOT NULL, `number` TEXT NOT NULL, `org` TEXT NOT NULL, `pacing` TEXT NOT NULL, `shortDescription` TEXT NOT NULL, `start` TEXT NOT NULL, `end` TEXT NOT NULL, `startDisplay` TEXT NOT NULL, `startType` TEXT NOT NULL, `overview` TEXT NOT NULL, `isEnrolled` INTEGER NOT NULL, `bannerImage` TEXT, `courseImage` TEXT, `courseVideo` TEXT, `image` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "blocksUrl", + "columnName": "blocksUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "effort", + "columnName": "effort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enrollmentStart", + "columnName": "enrollmentStart", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enrollmentEnd", + "columnName": "enrollmentEnd", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "invitationOnly", + "columnName": "invitationOnly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mobileAvailable", + "columnName": "mobileAvailable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "org", + "columnName": "org", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pacing", + "columnName": "pacing", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortDescription", + "columnName": "shortDescription", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "startDisplay", + "columnName": "startDisplay", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "startType", + "columnName": "startType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "overview", + "columnName": "overview", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isEnrolled", + "columnName": "isEnrolled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "media.bannerImage", + "columnName": "bannerImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "media.courseImage", + "columnName": "courseImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "media.courseVideo", + "columnName": "courseVideo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "media.image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "course_enrolled_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` TEXT NOT NULL, `auditAccessExpires` TEXT NOT NULL, `created` TEXT NOT NULL, `mode` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `id` TEXT NOT NULL, `name` TEXT NOT NULL, `number` TEXT NOT NULL, `org` TEXT NOT NULL, `start` TEXT NOT NULL, `startDisplay` TEXT NOT NULL, `startType` TEXT NOT NULL, `end` TEXT NOT NULL, `dynamicUpgradeDeadline` TEXT NOT NULL, `subscriptionId` TEXT NOT NULL, `course_image_link` TEXT NOT NULL, `courseAbout` TEXT NOT NULL, `courseUpdates` TEXT NOT NULL, `courseHandouts` TEXT NOT NULL, `discussionUrl` TEXT NOT NULL, `videoOutline` TEXT NOT NULL, `isSelfPaced` INTEGER NOT NULL, `hasAccess` INTEGER, `errorCode` TEXT, `developerMessage` TEXT, `userMessage` TEXT, `additionalContextUserMessage` TEXT, `userFragment` TEXT, `bannerImage` TEXT, `courseImage` TEXT, `courseVideo` TEXT, `image` TEXT, `facebook` TEXT NOT NULL, `twitter` TEXT NOT NULL, `certificateURL` TEXT, `assignments_completed` INTEGER NOT NULL, `total_assignments_count` INTEGER NOT NULL, `lastVisitedModuleId` TEXT, `lastVisitedModulePath` TEXT, `lastVisitedBlockId` TEXT, `lastVisitedUnitDisplayName` TEXT, `futureAssignments` TEXT, `pastAssignments` TEXT, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "auditAccessExpires", + "columnName": "auditAccessExpires", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mode", + "columnName": "mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "course.id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.number", + "columnName": "number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.org", + "columnName": "org", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.start", + "columnName": "start", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.startDisplay", + "columnName": "startDisplay", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.startType", + "columnName": "startType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.end", + "columnName": "end", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.dynamicUpgradeDeadline", + "columnName": "dynamicUpgradeDeadline", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.subscriptionId", + "columnName": "subscriptionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.courseImage", + "columnName": "course_image_link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.courseAbout", + "columnName": "courseAbout", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.courseUpdates", + "columnName": "courseUpdates", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.courseHandouts", + "columnName": "courseHandouts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.discussionUrl", + "columnName": "discussionUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.videoOutline", + "columnName": "videoOutline", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.isSelfPaced", + "columnName": "isSelfPaced", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "course.coursewareAccess.hasAccess", + "columnName": "hasAccess", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "course.coursewareAccess.errorCode", + "columnName": "errorCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "course.coursewareAccess.developerMessage", + "columnName": "developerMessage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "course.coursewareAccess.userMessage", + "columnName": "userMessage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "course.coursewareAccess.additionalContextUserMessage", + "columnName": "additionalContextUserMessage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "course.coursewareAccess.userFragment", + "columnName": "userFragment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "course.media.bannerImage", + "columnName": "bannerImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "course.media.courseImage", + "columnName": "courseImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "course.media.courseVideo", + "columnName": "courseVideo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "course.media.image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "course.courseSharingUtmParameters.facebook", + "columnName": "facebook", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.courseSharingUtmParameters.twitter", + "columnName": "twitter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificate.certificateURL", + "columnName": "certificateURL", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "progress.assignmentsCompleted", + "columnName": "assignments_completed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progress.totalAssignmentsCount", + "columnName": "total_assignments_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseStatus.lastVisitedModuleId", + "columnName": "lastVisitedModuleId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseStatus.lastVisitedModulePath", + "columnName": "lastVisitedModulePath", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseStatus.lastVisitedBlockId", + "columnName": "lastVisitedBlockId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseStatus.lastVisitedUnitDisplayName", + "columnName": "lastVisitedUnitDisplayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseAssignments.futureAssignments", + "columnName": "futureAssignments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseAssignments.pastAssignments", + "columnName": "pastAssignments", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "course_structure_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`root` TEXT NOT NULL, `id` TEXT NOT NULL, `blocks` TEXT NOT NULL, `name` TEXT NOT NULL, `number` TEXT NOT NULL, `org` TEXT NOT NULL, `start` TEXT, `startDisplay` TEXT NOT NULL, `startType` TEXT NOT NULL, `end` TEXT, `isSelfPaced` INTEGER NOT NULL, `hasAccess` INTEGER, `errorCode` TEXT, `developerMessage` TEXT, `userMessage` TEXT, `additionalContextUserMessage` TEXT, `userFragment` TEXT, `bannerImage` TEXT, `courseImage` TEXT, `courseVideo` TEXT, `image` TEXT, `certificateURL` TEXT, `assignments_completed` INTEGER NOT NULL, `total_assignments_count` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "root", + "columnName": "root", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "blocks", + "columnName": "blocks", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "org", + "columnName": "org", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startDisplay", + "columnName": "startDisplay", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "startType", + "columnName": "startType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isSelfPaced", + "columnName": "isSelfPaced", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "coursewareAccess.hasAccess", + "columnName": "hasAccess", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "coursewareAccess.errorCode", + "columnName": "errorCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coursewareAccess.developerMessage", + "columnName": "developerMessage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coursewareAccess.userMessage", + "columnName": "userMessage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coursewareAccess.additionalContextUserMessage", + "columnName": "additionalContextUserMessage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coursewareAccess.userFragment", + "columnName": "userFragment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "media.bannerImage", + "columnName": "bannerImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "media.courseImage", + "columnName": "courseImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "media.courseVideo", + "columnName": "courseVideo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "media.image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "certificate.certificateURL", + "columnName": "certificateURL", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "progress.assignmentsCompleted", + "columnName": "assignments_completed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progress.totalAssignmentsCount", + "columnName": "total_assignments_count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "download_model", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `courseId` TEXT NOT NULL, `size` INTEGER NOT NULL, `path` TEXT NOT NULL, `url` TEXT NOT NULL, `type` TEXT NOT NULL, `downloadedState` TEXT NOT NULL, `lastModified` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "downloadedState", + "columnName": "downloadedState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastModified", + "columnName": "lastModified", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "offline_x_block_progress_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `courseId` TEXT NOT NULL, `url` TEXT NOT NULL, `type` TEXT NOT NULL, `data` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "blockId", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "jsonProgress.url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "jsonProgress.type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "jsonProgress.data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "course_calendar_event_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`event_id` INTEGER NOT NULL, `course_id` TEXT NOT NULL, PRIMARY KEY(`event_id`))", + "fields": [ + { + "fieldPath": "eventId", + "columnName": "event_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "course_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "event_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "course_calendar_state_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`course_id` TEXT NOT NULL, `checksum` INTEGER NOT NULL, `is_course_sync_enabled` INTEGER NOT NULL, PRIMARY KEY(`course_id`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "course_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "checksum", + "columnName": "checksum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCourseSyncEnabled", + "columnName": "is_course_sync_enabled", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "course_id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "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, 'bcac519e74e751a75f3e6fa5d39ac5a3')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/org.openedx.app.room.AppDatabase/2.json b/app/schemas/org.openedx.app.room.AppDatabase/2.json new file mode 100644 index 000000000..002abc547 --- /dev/null +++ b/app/schemas/org.openedx.app.room.AppDatabase/2.json @@ -0,0 +1,978 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "ed545aec6739ec7692c4bb72179331c4", + "entities": [ + { + "tableName": "course_discovery_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `blocksUrl` TEXT NOT NULL, `courseId` TEXT NOT NULL, `effort` TEXT NOT NULL, `enrollmentStart` TEXT NOT NULL, `enrollmentEnd` TEXT NOT NULL, `hidden` INTEGER NOT NULL, `invitationOnly` INTEGER NOT NULL, `mobileAvailable` INTEGER NOT NULL, `name` TEXT NOT NULL, `number` TEXT NOT NULL, `org` TEXT NOT NULL, `pacing` TEXT NOT NULL, `shortDescription` TEXT NOT NULL, `start` TEXT NOT NULL, `end` TEXT NOT NULL, `startDisplay` TEXT NOT NULL, `startType` TEXT NOT NULL, `overview` TEXT NOT NULL, `isEnrolled` INTEGER NOT NULL, `bannerImage` TEXT, `courseImage` TEXT, `courseVideo` TEXT, `image` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "blocksUrl", + "columnName": "blocksUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "effort", + "columnName": "effort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enrollmentStart", + "columnName": "enrollmentStart", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enrollmentEnd", + "columnName": "enrollmentEnd", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "invitationOnly", + "columnName": "invitationOnly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mobileAvailable", + "columnName": "mobileAvailable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "org", + "columnName": "org", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pacing", + "columnName": "pacing", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortDescription", + "columnName": "shortDescription", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "startDisplay", + "columnName": "startDisplay", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "startType", + "columnName": "startType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "overview", + "columnName": "overview", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isEnrolled", + "columnName": "isEnrolled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "media.bannerImage", + "columnName": "bannerImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "media.courseImage", + "columnName": "courseImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "media.courseVideo", + "columnName": "courseVideo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "media.image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "course_enrolled_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` TEXT NOT NULL, `auditAccessExpires` TEXT NOT NULL, `created` TEXT NOT NULL, `mode` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `id` TEXT NOT NULL, `name` TEXT NOT NULL, `number` TEXT NOT NULL, `org` TEXT NOT NULL, `start` TEXT NOT NULL, `startDisplay` TEXT NOT NULL, `startType` TEXT NOT NULL, `end` TEXT NOT NULL, `dynamicUpgradeDeadline` TEXT NOT NULL, `subscriptionId` TEXT NOT NULL, `course_image_link` TEXT NOT NULL, `courseAbout` TEXT NOT NULL, `courseUpdates` TEXT NOT NULL, `courseHandouts` TEXT NOT NULL, `discussionUrl` TEXT NOT NULL, `videoOutline` TEXT NOT NULL, `isSelfPaced` INTEGER NOT NULL, `hasAccess` INTEGER, `errorCode` TEXT, `developerMessage` TEXT, `userMessage` TEXT, `additionalContextUserMessage` TEXT, `userFragment` TEXT, `bannerImage` TEXT, `courseImage` TEXT, `courseVideo` TEXT, `image` TEXT, `facebook` TEXT NOT NULL, `twitter` TEXT NOT NULL, `certificateURL` TEXT, `assignments_completed` INTEGER NOT NULL, `total_assignments_count` INTEGER NOT NULL, `lastVisitedModuleId` TEXT, `lastVisitedModulePath` TEXT, `lastVisitedBlockId` TEXT, `lastVisitedUnitDisplayName` TEXT, `futureAssignments` TEXT, `pastAssignments` TEXT, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "auditAccessExpires", + "columnName": "auditAccessExpires", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mode", + "columnName": "mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "course.id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.number", + "columnName": "number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.org", + "columnName": "org", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.start", + "columnName": "start", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.startDisplay", + "columnName": "startDisplay", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.startType", + "columnName": "startType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.end", + "columnName": "end", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.dynamicUpgradeDeadline", + "columnName": "dynamicUpgradeDeadline", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.subscriptionId", + "columnName": "subscriptionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.courseImage", + "columnName": "course_image_link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.courseAbout", + "columnName": "courseAbout", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.courseUpdates", + "columnName": "courseUpdates", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.courseHandouts", + "columnName": "courseHandouts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.discussionUrl", + "columnName": "discussionUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.videoOutline", + "columnName": "videoOutline", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.isSelfPaced", + "columnName": "isSelfPaced", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "course.coursewareAccess.hasAccess", + "columnName": "hasAccess", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "course.coursewareAccess.errorCode", + "columnName": "errorCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "course.coursewareAccess.developerMessage", + "columnName": "developerMessage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "course.coursewareAccess.userMessage", + "columnName": "userMessage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "course.coursewareAccess.additionalContextUserMessage", + "columnName": "additionalContextUserMessage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "course.coursewareAccess.userFragment", + "columnName": "userFragment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "course.media.bannerImage", + "columnName": "bannerImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "course.media.courseImage", + "columnName": "courseImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "course.media.courseVideo", + "columnName": "courseVideo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "course.media.image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "course.courseSharingUtmParameters.facebook", + "columnName": "facebook", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "course.courseSharingUtmParameters.twitter", + "columnName": "twitter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificate.certificateURL", + "columnName": "certificateURL", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "progress.assignmentsCompleted", + "columnName": "assignments_completed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progress.totalAssignmentsCount", + "columnName": "total_assignments_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseStatus.lastVisitedModuleId", + "columnName": "lastVisitedModuleId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseStatus.lastVisitedModulePath", + "columnName": "lastVisitedModulePath", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseStatus.lastVisitedBlockId", + "columnName": "lastVisitedBlockId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseStatus.lastVisitedUnitDisplayName", + "columnName": "lastVisitedUnitDisplayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseAssignments.futureAssignments", + "columnName": "futureAssignments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseAssignments.pastAssignments", + "columnName": "pastAssignments", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "course_structure_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`root` TEXT NOT NULL, `id` TEXT NOT NULL, `blocks` TEXT NOT NULL, `name` TEXT NOT NULL, `number` TEXT NOT NULL, `org` TEXT NOT NULL, `start` TEXT, `startDisplay` TEXT NOT NULL, `startType` TEXT NOT NULL, `end` TEXT, `isSelfPaced` INTEGER NOT NULL, `hasAccess` INTEGER, `errorCode` TEXT, `developerMessage` TEXT, `userMessage` TEXT, `additionalContextUserMessage` TEXT, `userFragment` TEXT, `bannerImage` TEXT, `courseImage` TEXT, `courseVideo` TEXT, `image` TEXT, `certificateURL` TEXT, `assignments_completed` INTEGER NOT NULL, `total_assignments_count` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "root", + "columnName": "root", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "blocks", + "columnName": "blocks", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "org", + "columnName": "org", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startDisplay", + "columnName": "startDisplay", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "startType", + "columnName": "startType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isSelfPaced", + "columnName": "isSelfPaced", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "coursewareAccess.hasAccess", + "columnName": "hasAccess", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "coursewareAccess.errorCode", + "columnName": "errorCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coursewareAccess.developerMessage", + "columnName": "developerMessage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coursewareAccess.userMessage", + "columnName": "userMessage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coursewareAccess.additionalContextUserMessage", + "columnName": "additionalContextUserMessage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coursewareAccess.userFragment", + "columnName": "userFragment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "media.bannerImage", + "columnName": "bannerImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "media.courseImage", + "columnName": "courseImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "media.courseVideo", + "columnName": "courseVideo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "media.image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "certificate.certificateURL", + "columnName": "certificateURL", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "progress.assignmentsCompleted", + "columnName": "assignments_completed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progress.totalAssignmentsCount", + "columnName": "total_assignments_count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "download_model", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `courseId` TEXT NOT NULL, `size` INTEGER NOT NULL, `path` TEXT NOT NULL, `url` TEXT NOT NULL, `type` TEXT NOT NULL, `downloadedState` TEXT NOT NULL, `lastModified` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "downloadedState", + "columnName": "downloadedState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastModified", + "columnName": "lastModified", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "offline_x_block_progress_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `courseId` TEXT NOT NULL, `url` TEXT NOT NULL, `type` TEXT NOT NULL, `data` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "blockId", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "jsonProgress.url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "jsonProgress.type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "jsonProgress.data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "course_calendar_event_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`event_id` INTEGER NOT NULL, `course_id` TEXT NOT NULL, PRIMARY KEY(`event_id`))", + "fields": [ + { + "fieldPath": "eventId", + "columnName": "event_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "course_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "event_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "course_calendar_state_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`course_id` TEXT NOT NULL, `checksum` INTEGER NOT NULL, `is_course_sync_enabled` INTEGER NOT NULL, PRIMARY KEY(`course_id`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "course_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "checksum", + "columnName": "checksum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCourseSyncEnabled", + "columnName": "is_course_sync_enabled", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "course_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "course_enrollment_details_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `courseUpdates` TEXT NOT NULL, `courseHandouts` TEXT NOT NULL, `discussionUrl` TEXT NOT NULL, `hasUnmetPrerequisites` INTEGER NOT NULL, `isTooEarly` INTEGER NOT NULL, `isStaff` INTEGER NOT NULL, `auditAccessExpires` TEXT, `hasAccess` INTEGER, `errorCode` TEXT, `developerMessage` TEXT, `userMessage` TEXT, `additionalContextUserMessage` TEXT, `userFragment` TEXT, `certificateURL` TEXT, `created` TEXT, `mode` TEXT, `isActive` INTEGER NOT NULL, `upgradeDeadline` TEXT, `name` TEXT NOT NULL, `number` TEXT NOT NULL, `org` TEXT NOT NULL, `startDisplay` TEXT NOT NULL, `startType` TEXT NOT NULL, `isSelfPaced` INTEGER NOT NULL, `courseAbout` TEXT NOT NULL, `bannerImage` TEXT, `courseImage` TEXT, `courseVideo` TEXT, `image` TEXT, `facebook` TEXT NOT NULL, `twitter` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseUpdates", + "columnName": "courseUpdates", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseHandouts", + "columnName": "courseHandouts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "discussionUrl", + "columnName": "discussionUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseAccessDetails.hasUnmetPrerequisites", + "columnName": "hasUnmetPrerequisites", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseAccessDetails.isTooEarly", + "columnName": "isTooEarly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseAccessDetails.isStaff", + "columnName": "isStaff", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseAccessDetails.auditAccessExpires", + "columnName": "auditAccessExpires", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseAccessDetails.coursewareAccess.hasAccess", + "columnName": "hasAccess", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "courseAccessDetails.coursewareAccess.errorCode", + "columnName": "errorCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseAccessDetails.coursewareAccess.developerMessage", + "columnName": "developerMessage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseAccessDetails.coursewareAccess.userMessage", + "columnName": "userMessage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseAccessDetails.coursewareAccess.additionalContextUserMessage", + "columnName": "additionalContextUserMessage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseAccessDetails.coursewareAccess.userFragment", + "columnName": "userFragment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "certificate.certificateURL", + "columnName": "certificateURL", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enrollmentDetails.created", + "columnName": "created", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enrollmentDetails.mode", + "columnName": "mode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enrollmentDetails.isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enrollmentDetails.upgradeDeadline", + "columnName": "upgradeDeadline", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseInfoOverview.name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseInfoOverview.number", + "columnName": "number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseInfoOverview.org", + "columnName": "org", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseInfoOverview.startDisplay", + "columnName": "startDisplay", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseInfoOverview.startType", + "columnName": "startType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseInfoOverview.isSelfPaced", + "columnName": "isSelfPaced", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseInfoOverview.courseAbout", + "columnName": "courseAbout", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseInfoOverview.media.bannerImage", + "columnName": "bannerImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseInfoOverview.media.courseImage", + "columnName": "courseImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseInfoOverview.media.courseVideo", + "columnName": "courseVideo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseInfoOverview.media.image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseInfoOverview.courseSharingUtmParameters.facebook", + "columnName": "facebook", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseInfoOverview.courseSharingUtmParameters.twitter", + "columnName": "twitter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "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, 'ed545aec6739ec7692c4bb72179331c4')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/org/openedx/app/room/AppDatabase.kt b/app/src/main/java/org/openedx/app/room/AppDatabase.kt index 6aa46ed1f..eec5b1811 100644 --- a/app/src/main/java/org/openedx/app/room/AppDatabase.kt +++ b/app/src/main/java/org/openedx/app/room/AppDatabase.kt @@ -1,10 +1,12 @@ package org.openedx.app.room +import androidx.room.AutoMigration import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters import org.openedx.core.data.model.room.CourseCalendarEventEntity import org.openedx.core.data.model.room.CourseCalendarStateEntity +import org.openedx.core.data.model.room.CourseEnrollmentDetailsEntity import org.openedx.core.data.model.room.CourseStructureEntity import org.openedx.core.data.model.room.OfflineXBlockProgress import org.openedx.core.data.model.room.discovery.EnrolledCourseEntity @@ -18,7 +20,7 @@ import org.openedx.discovery.data.converter.DiscoveryConverter import org.openedx.discovery.data.model.room.CourseEntity import org.openedx.discovery.data.storage.DiscoveryDao -const val DATABASE_VERSION = 1 +const val DATABASE_VERSION = 2 const val DATABASE_NAME = "OpenEdX_db" @Database( @@ -29,10 +31,13 @@ const val DATABASE_NAME = "OpenEdX_db" DownloadModelEntity::class, OfflineXBlockProgress::class, CourseCalendarEventEntity::class, - CourseCalendarStateEntity::class + CourseCalendarStateEntity::class, + CourseEnrollmentDetailsEntity::class ], - version = DATABASE_VERSION, - exportSchema = false + autoMigrations = [ + AutoMigration(1, DATABASE_VERSION) + ], + version = DATABASE_VERSION ) @TypeConverters(DiscoveryConverter::class, CourseConverter::class) abstract class AppDatabase : RoomDatabase() { diff --git a/app/src/main/java/org/openedx/app/room/DatabaseManager.kt b/app/src/main/java/org/openedx/app/room/DatabaseManager.kt index 5d5415854..bcc123763 100644 --- a/app/src/main/java/org/openedx/app/room/DatabaseManager.kt +++ b/app/src/main/java/org/openedx/app/room/DatabaseManager.kt @@ -18,6 +18,7 @@ class DatabaseManager( override fun clearTables() { CoroutineScope(Dispatchers.IO).launch { courseDao.clearCachedData() + courseDao.clearEnrollmentCachedData() dashboardDao.clearCachedData() downloadDao.clearOfflineProgress() discoveryDao.clearCachedData() diff --git a/core/src/main/java/org/openedx/core/data/model/room/CourseEnrollmentDetailsEntity.kt b/core/src/main/java/org/openedx/core/data/model/room/CourseEnrollmentDetailsEntity.kt new file mode 100644 index 000000000..cc5d55278 --- /dev/null +++ b/core/src/main/java/org/openedx/core/data/model/room/CourseEnrollmentDetailsEntity.kt @@ -0,0 +1,84 @@ +package org.openedx.core.data.model.room + +import androidx.room.ColumnInfo +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.PrimaryKey +import org.openedx.core.data.model.room.discovery.CertificateDb +import org.openedx.core.data.model.room.discovery.CourseAccessDetailsDb +import org.openedx.core.data.model.room.discovery.CourseSharingUtmParametersDb +import org.openedx.core.data.model.room.discovery.EnrollmentDetailsDB +import org.openedx.core.domain.model.CourseEnrollmentDetails +import org.openedx.core.domain.model.CourseInfoOverview +import java.util.Date + +@Entity(tableName = "course_enrollment_details_table") +data class CourseEnrollmentDetailsEntity( + @PrimaryKey + @ColumnInfo("id") + val id: String, + @ColumnInfo("courseUpdates") + val courseUpdates: String, + @ColumnInfo("courseHandouts") + val courseHandouts: String, + @ColumnInfo("discussionUrl") + val discussionUrl: String, + @Embedded + val courseAccessDetails: CourseAccessDetailsDb, + @Embedded + val certificate: CertificateDb?, + @Embedded + val enrollmentDetails: EnrollmentDetailsDB, + @Embedded + val courseInfoOverview: CourseInfoOverviewDb +) { + fun mapToDomain() = CourseEnrollmentDetails( + id = id, + courseUpdates = courseUpdates, + courseHandouts = courseHandouts, + discussionUrl = discussionUrl, + courseAccessDetails = courseAccessDetails.mapToDomain(), + certificate = certificate?.mapToDomain(), + enrollmentDetails = enrollmentDetails.mapToDomain(), + courseInfoOverview = courseInfoOverview.mapToDomain() + ) +} + +data class CourseInfoOverviewDb( + @ColumnInfo("name") + val name: String, + @ColumnInfo("number") + val number: String, + @ColumnInfo("org") + val org: String, + @Embedded + val start: Date?, + @ColumnInfo("startDisplay") + val startDisplay: String, + @ColumnInfo("startType") + val startType: String, + @Embedded + val end: Date?, + @ColumnInfo("isSelfPaced") + val isSelfPaced: Boolean, + @Embedded + var media: MediaDb?, + @Embedded + val courseSharingUtmParameters: CourseSharingUtmParametersDb, + @ColumnInfo("courseAbout") + val courseAbout: String, +) { + fun mapToDomain() = CourseInfoOverview( + name = name, + number = number, + org = org, + start = start, + startDisplay = startDisplay, + startType = startType, + end = end, + isSelfPaced = isSelfPaced, + media = media?.mapToDomain(), + courseSharingUtmParameters = courseSharingUtmParameters.mapToDomain(), + courseAbout = courseAbout + ) +} diff --git a/core/src/main/java/org/openedx/core/domain/model/Certificate.kt b/core/src/main/java/org/openedx/core/domain/model/Certificate.kt index 054b75511..62fb51b50 100644 --- a/core/src/main/java/org/openedx/core/domain/model/Certificate.kt +++ b/core/src/main/java/org/openedx/core/domain/model/Certificate.kt @@ -2,10 +2,13 @@ package org.openedx.core.domain.model import android.os.Parcelable import kotlinx.parcelize.Parcelize +import org.openedx.core.data.model.room.discovery.CertificateDb @Parcelize data class Certificate( val certificateURL: String? ) : Parcelable { fun isCertificateEarned() = certificateURL?.isNotEmpty() == true + + fun mapToRoomEntity() = CertificateDb(certificateURL) } diff --git a/core/src/main/java/org/openedx/core/domain/model/CourseAccessDetails.kt b/core/src/main/java/org/openedx/core/domain/model/CourseAccessDetails.kt index fac674e66..2c95865e9 100644 --- a/core/src/main/java/org/openedx/core/domain/model/CourseAccessDetails.kt +++ b/core/src/main/java/org/openedx/core/domain/model/CourseAccessDetails.kt @@ -1,7 +1,9 @@ package org.openedx.core.domain.model import android.os.Parcelable +import com.google.gson.internal.bind.util.ISO8601Utils import kotlinx.parcelize.Parcelize +import org.openedx.core.data.model.room.discovery.CourseAccessDetailsDb import java.util.Date @Parcelize @@ -11,4 +13,14 @@ data class CourseAccessDetails( val isStaff: Boolean, val auditAccessExpires: Date?, val coursewareAccess: CoursewareAccess?, -) : Parcelable +) : Parcelable { + + fun mapToRoomEntity(): CourseAccessDetailsDb = + CourseAccessDetailsDb( + hasUnmetPrerequisites = hasUnmetPrerequisites, + isTooEarly = isTooEarly, + isStaff = isStaff, + auditAccessExpires = auditAccessExpires?.let { ISO8601Utils.format(it) }, + coursewareAccess = coursewareAccess?.mapToEntity() + ) +} diff --git a/core/src/main/java/org/openedx/core/domain/model/CourseEnrollmentDetails.kt b/core/src/main/java/org/openedx/core/domain/model/CourseEnrollmentDetails.kt index 5c61fee60..ec961dfcd 100644 --- a/core/src/main/java/org/openedx/core/domain/model/CourseEnrollmentDetails.kt +++ b/core/src/main/java/org/openedx/core/domain/model/CourseEnrollmentDetails.kt @@ -2,6 +2,7 @@ package org.openedx.core.domain.model import android.os.Parcelable import kotlinx.parcelize.Parcelize +import org.openedx.core.data.model.room.CourseEnrollmentDetailsEntity import org.openedx.core.extension.isNotNull import java.util.Date @@ -23,6 +24,17 @@ data class CourseEnrollmentDetails( val isAuditAccessExpired: Boolean get() = courseAccessDetails.auditAccessExpires.isNotNull() && Date().after(courseAccessDetails.auditAccessExpires) + + fun mapToEntity() = CourseEnrollmentDetailsEntity( + id = id, + courseUpdates = courseUpdates, + courseHandouts = courseHandouts, + discussionUrl = discussionUrl, + courseAccessDetails = courseAccessDetails.mapToRoomEntity(), + certificate = certificate?.mapToRoomEntity(), + enrollmentDetails = enrollmentDetails.mapToEntity(), + courseInfoOverview = courseInfoOverview.mapToEntity() + ) } enum class CourseAccessError { diff --git a/core/src/main/java/org/openedx/core/domain/model/CourseInfoOverview.kt b/core/src/main/java/org/openedx/core/domain/model/CourseInfoOverview.kt index 4d02f10b9..6895522f5 100644 --- a/core/src/main/java/org/openedx/core/domain/model/CourseInfoOverview.kt +++ b/core/src/main/java/org/openedx/core/domain/model/CourseInfoOverview.kt @@ -2,6 +2,7 @@ package org.openedx.core.domain.model import android.os.Parcelable import kotlinx.parcelize.Parcelize +import org.openedx.core.data.model.room.CourseInfoOverviewDb import java.util.Date @Parcelize @@ -10,7 +11,7 @@ data class CourseInfoOverview( val number: String, val org: String, val start: Date?, - val startDisplay: String, + val startDisplay: String?, val startType: String, val end: Date?, val isSelfPaced: Boolean, @@ -20,4 +21,18 @@ data class CourseInfoOverview( ) : Parcelable { val isStarted: Boolean get() = start?.before(Date()) ?: false + + fun mapToEntity() = CourseInfoOverviewDb( + name = name, + number = number, + org = org, + start = start, + startDisplay = startDisplay ?: "", + startType = startType, + end = end, + isSelfPaced = isSelfPaced, + media = media?.mapToEntity(), + courseSharingUtmParameters = courseSharingUtmParameters.mapToEntity(), + courseAbout = courseAbout + ) } diff --git a/core/src/main/java/org/openedx/core/domain/model/CourseSharingUtmParameters.kt b/core/src/main/java/org/openedx/core/domain/model/CourseSharingUtmParameters.kt index 186ef85fd..1d27361a3 100644 --- a/core/src/main/java/org/openedx/core/domain/model/CourseSharingUtmParameters.kt +++ b/core/src/main/java/org/openedx/core/domain/model/CourseSharingUtmParameters.kt @@ -2,9 +2,16 @@ package org.openedx.core.domain.model import android.os.Parcelable import kotlinx.parcelize.Parcelize +import org.openedx.core.data.model.room.discovery.CourseSharingUtmParametersDb @Parcelize data class CourseSharingUtmParameters( val facebook: String, val twitter: String -) : Parcelable +) : Parcelable { + + fun mapToEntity() = CourseSharingUtmParametersDb( + facebook = facebook, + twitter = twitter + ) +} diff --git a/core/src/main/java/org/openedx/core/domain/model/CoursewareAccess.kt b/core/src/main/java/org/openedx/core/domain/model/CoursewareAccess.kt index 5dd48d94e..9f0fd60e6 100644 --- a/core/src/main/java/org/openedx/core/domain/model/CoursewareAccess.kt +++ b/core/src/main/java/org/openedx/core/domain/model/CoursewareAccess.kt @@ -2,6 +2,7 @@ package org.openedx.core.domain.model import android.os.Parcelable import kotlinx.parcelize.Parcelize +import org.openedx.core.data.model.room.discovery.CoursewareAccessDb @Parcelize data class CoursewareAccess( @@ -11,4 +12,14 @@ data class CoursewareAccess( val userMessage: String, val additionalContextUserMessage: String, val userFragment: String -) : Parcelable +) : Parcelable { + + fun mapToEntity() = CoursewareAccessDb( + hasAccess = hasAccess, + errorCode = errorCode, + developerMessage = developerMessage, + userMessage = userMessage, + additionalContextUserMessage = additionalContextUserMessage, + userFragment = userFragment + ) +} diff --git a/core/src/main/java/org/openedx/core/domain/model/EnrollmentDetails.kt b/core/src/main/java/org/openedx/core/domain/model/EnrollmentDetails.kt index c9d39ec35..b880f3948 100644 --- a/core/src/main/java/org/openedx/core/domain/model/EnrollmentDetails.kt +++ b/core/src/main/java/org/openedx/core/domain/model/EnrollmentDetails.kt @@ -1,7 +1,9 @@ package org.openedx.core.domain.model import android.os.Parcelable +import com.google.gson.internal.bind.util.ISO8601Utils import kotlinx.parcelize.Parcelize +import org.openedx.core.data.model.room.discovery.EnrollmentDetailsDB import java.util.Date @Parcelize @@ -10,4 +12,12 @@ data class EnrollmentDetails( val mode: String?, val isActive: Boolean, val upgradeDeadline: Date?, -) : Parcelable +) : Parcelable { + + fun mapToEntity() = EnrollmentDetailsDB( + created = created?.let { ISO8601Utils.format(it) }, + mode = mode, + isActive = isActive, + upgradeDeadline = upgradeDeadline?.let { ISO8601Utils.format(it) } + ) +} diff --git a/core/src/main/java/org/openedx/core/domain/model/Media.kt b/core/src/main/java/org/openedx/core/domain/model/Media.kt index 51fa6dda5..572fcbdae 100644 --- a/core/src/main/java/org/openedx/core/domain/model/Media.kt +++ b/core/src/main/java/org/openedx/core/domain/model/Media.kt @@ -2,6 +2,11 @@ package org.openedx.core.domain.model import android.os.Parcelable import kotlinx.parcelize.Parcelize +import org.openedx.core.data.model.room.BannerImageDb +import org.openedx.core.data.model.room.CourseImageDb +import org.openedx.core.data.model.room.CourseVideoDb +import org.openedx.core.data.model.room.ImageDb +import org.openedx.core.data.model.room.MediaDb @Parcelize data class Media( @@ -9,28 +14,48 @@ data class Media( val courseImage: CourseImage? = null, val courseVideo: CourseVideo? = null, val image: Image? = null -) : Parcelable +) : Parcelable { + + fun mapToEntity() = MediaDb( + bannerImage = bannerImage?.mapToEntity(), + courseImage = courseImage?.mapToEntity(), + courseVideo = courseVideo?.mapToEntity(), + image = image?.mapToEntity() + ) +} @Parcelize data class Image( val large: String, val raw: String, val small: String -) : Parcelable +) : Parcelable { + + fun mapToEntity() = ImageDb(large, raw, small) +} @Parcelize data class CourseVideo( val uri: String -) : Parcelable +) : Parcelable { + + fun mapToEntity() = CourseVideoDb(uri) +} @Parcelize data class CourseImage( val uri: String, val name: String -) : Parcelable +) : Parcelable { + + fun mapToEntity() = CourseImageDb(uri, name) +} @Parcelize data class BannerImage( val uri: String, val uriAbsolute: String -) : Parcelable +) : Parcelable { + + fun mapToEntity() = BannerImageDb(uri, uriAbsolute) +} diff --git a/core/src/main/java/org/openedx/core/extension/CoroutineExt.kt b/core/src/main/java/org/openedx/core/extension/CoroutineExt.kt new file mode 100644 index 000000000..5a29ef9f5 --- /dev/null +++ b/core/src/main/java/org/openedx/core/extension/CoroutineExt.kt @@ -0,0 +1,14 @@ +package org.openedx.core.extension + +import kotlinx.coroutines.channels.ProducerScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.channelFlow +import kotlin.experimental.ExperimentalTypeInference + +@OptIn(ExperimentalTypeInference::class) +inline fun channelFlowWithAwait( + @BuilderInference crossinline block: suspend ProducerScope.() -> Unit +) = channelFlow { + block(this) + awaitClose() +} diff --git a/course/src/main/java/org/openedx/course/data/repository/CourseRepository.kt b/course/src/main/java/org/openedx/course/data/repository/CourseRepository.kt index d9034e4ef..bc508821d 100644 --- a/course/src/main/java/org/openedx/course/data/repository/CourseRepository.kt +++ b/course/src/main/java/org/openedx/course/data/repository/CourseRepository.kt @@ -1,5 +1,6 @@ package org.openedx.course.data.repository +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import okhttp3.MultipartBody import org.openedx.core.ApiConstants @@ -9,15 +10,19 @@ import org.openedx.core.data.model.room.OfflineXBlockProgress import org.openedx.core.data.model.room.XBlockProgressData import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.CourseComponentStatus +import org.openedx.core.domain.model.CourseDatesBannerInfo +import org.openedx.core.domain.model.CourseDatesResult import org.openedx.core.domain.model.CourseEnrollmentDetails import org.openedx.core.domain.model.CourseStructure import org.openedx.core.exception.NoCachedDataException +import org.openedx.core.extension.channelFlowWithAwait import org.openedx.core.module.db.DownloadDao import org.openedx.core.system.connection.NetworkConnection import org.openedx.course.data.storage.CourseDao import java.net.URLDecoder import java.nio.charset.StandardCharsets +@Suppress("TooManyFunctions") class CourseRepository( private val api: CourseApi, private val courseDao: CourseDao, @@ -25,7 +30,10 @@ class CourseRepository( private val preferencesManager: CorePreferences, private val networkConnection: NetworkConnection, ) { - private var courseStructure = mutableMapOf() + private val courseStructure = mutableMapOf() + + private val courseStatusMap = mutableMapOf() + private val courseDatesMap = mutableMapOf() suspend fun removeDownloadModel(id: String) { downloadDao.removeDownloadModel(id) @@ -37,6 +45,35 @@ class CourseRepository( suspend fun getAllDownloadModels() = downloadDao.readAllData().map { it.mapToDomain() } + suspend fun getCourseStructureFlow(courseId: String, forceRefresh: Boolean = true): Flow = + channelFlowWithAwait { + var hasCourseStructure = false + val cachedCourseStructure = courseStructure[courseId] ?: ( + courseDao.getCourseStructureById(courseId)?.mapToDomain() + ) + if (cachedCourseStructure != null) { + hasCourseStructure = true + trySend(cachedCourseStructure) + } + val fetchRemoteCourse = !hasCourseStructure || forceRefresh + if (networkConnection.isOnline() && fetchRemoteCourse) { + val response = api.getCourseStructure( + "stale-if-error=0", + "v4", + preferencesManager.user?.username, + courseId + ) + courseDao.insertCourseStructureEntity(response.mapToRoomEntity()) + val courseDomainModel = response.mapToDomain() + courseStructure[courseId] = courseDomainModel + trySend(courseDomainModel) + hasCourseStructure = true + } + if (!hasCourseStructure) { + throw NoCachedDataException() + } + } + suspend fun getCourseStructureFromCache(courseId: String): CourseStructure { val cachedCourseStructure = courseDao.getCourseStructureById(courseId) if (cachedCourseStructure != null) { @@ -70,10 +107,41 @@ class CourseRepository( return courseStructure[courseId]!! } + suspend fun getEnrollmentDetailsFlow(courseId: String): Flow = + channelFlowWithAwait { + getCourseEnrollmentDetailsFromCache(courseId)?.let { + trySend(it) + } + val details = getEnrollmentDetails(courseId) + courseDao.insertCourseEnrollmentDetailsEntity(details.mapToEntity()) + trySend(details) + } + + private suspend fun getCourseEnrollmentDetailsFromCache(courseId: String): CourseEnrollmentDetails? { + return courseDao.getCourseEnrollmentDetailsById(id = courseId) + ?.mapToDomain() + } + suspend fun getEnrollmentDetails(courseId: String): CourseEnrollmentDetails { return api.getEnrollmentDetails(courseId = courseId).mapToDomain() } + suspend fun getCourseStatusFlow(courseId: String): Flow = + channelFlowWithAwait { + val localStatus = courseStatusMap[courseId] + localStatus?.let { trySend(it) } + + if (networkConnection.isOnline()) { + val username = preferencesManager.user?.username ?: "" + val status = api.getCourseStatus(username, courseId).mapToDomain() + courseStatusMap[courseId] = status + trySend(status) + } else { + val status = localStatus ?: CourseComponentStatus("") + trySend(status) + } + } + suspend fun getCourseStatus(courseId: String): CourseComponentStatus { val username = preferencesManager.user?.username ?: "" return api.getCourseStatus(username, courseId).mapToDomain() @@ -89,6 +157,30 @@ class CourseRepository( return api.markBlocksCompletion(blocksCompletionBody) } + suspend fun getCourseDatesFlow(courseId: String): Flow = + channelFlowWithAwait { + val localDates = courseDatesMap[courseId] + localDates?.let { trySend(it) } + + if (networkConnection.isOnline()) { + val datesResult = api.getCourseDates(courseId).getCourseDatesResult() + courseDatesMap[courseId] = datesResult + trySend(datesResult) + } else { + val datesResult = localDates ?: CourseDatesResult( + datesSection = linkedMapOf(), + courseBanner = CourseDatesBannerInfo( + missedDeadlines = false, + missedGatedContent = false, + verifiedUpgradeLink = "", + contentTypeGatingEnabled = false, + hasEnded = false + ) + ) + trySend(datesResult) + } + } + suspend fun getCourseDates(courseId: String) = api.getCourseDates(courseId).getCourseDatesResult() diff --git a/course/src/main/java/org/openedx/course/data/storage/CourseDao.kt b/course/src/main/java/org/openedx/course/data/storage/CourseDao.kt index 63bd1c4d9..8c2d94f03 100644 --- a/course/src/main/java/org/openedx/course/data/storage/CourseDao.kt +++ b/course/src/main/java/org/openedx/course/data/storage/CourseDao.kt @@ -4,6 +4,7 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import org.openedx.core.data.model.room.CourseEnrollmentDetailsEntity import org.openedx.core.data.model.room.CourseStructureEntity @Dao @@ -17,4 +18,13 @@ interface CourseDao { @Query("DELETE FROM course_structure_table") suspend fun clearCachedData() + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertCourseEnrollmentDetailsEntity(vararg courseEnrollmentDetailsEntity: CourseEnrollmentDetailsEntity) + + @Query("SELECT * FROM course_enrollment_details_table WHERE id=:id") + suspend fun getCourseEnrollmentDetailsById(id: String): CourseEnrollmentDetailsEntity? + + @Query("DELETE FROM course_enrollment_details_table") + suspend fun clearEnrollmentCachedData() } diff --git a/course/src/main/java/org/openedx/course/domain/interactor/CourseInteractor.kt b/course/src/main/java/org/openedx/course/domain/interactor/CourseInteractor.kt index fdbcdd204..4678c9115 100644 --- a/course/src/main/java/org/openedx/course/domain/interactor/CourseInteractor.kt +++ b/course/src/main/java/org/openedx/course/domain/interactor/CourseInteractor.kt @@ -1,15 +1,24 @@ package org.openedx.course.domain.interactor +import kotlinx.coroutines.flow.Flow import org.openedx.core.BlockType import org.openedx.core.domain.model.Block import org.openedx.core.domain.model.CourseEnrollmentDetails import org.openedx.core.domain.model.CourseStructure import org.openedx.course.data.repository.CourseRepository +@Suppress("TooManyFunctions") class CourseInteractor( private val repository: CourseRepository ) { + suspend fun getCourseStructureFlow( + courseId: String, + forceRefresh: Boolean = true + ): Flow { + return repository.getCourseStructureFlow(courseId, forceRefresh) + } + suspend fun getCourseStructure( courseId: String, isNeedRefresh: Boolean = false @@ -21,6 +30,10 @@ class CourseInteractor( return repository.getCourseStructureFromCache(courseId) } + suspend fun getEnrollmentDetailsFlow(courseId: String): Flow { + return repository.getEnrollmentDetailsFlow(courseId) + } + suspend fun getEnrollmentDetails(courseId: String): CourseEnrollmentDetails { return repository.getEnrollmentDetails(courseId = courseId) } @@ -68,8 +81,12 @@ class CourseInteractor( } } + suspend fun getCourseStatusFlow(courseId: String) = repository.getCourseStatusFlow(courseId) + suspend fun getCourseStatus(courseId: String) = repository.getCourseStatus(courseId) + suspend fun getCourseDatesFlow(courseId: String) = repository.getCourseDatesFlow(courseId) + suspend fun getCourseDates(courseId: String) = repository.getCourseDates(courseId) suspend fun resetCourseDates(courseId: String) = repository.resetCourseDates(courseId) diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt index ac1cb591e..0e7288423 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt @@ -6,7 +6,6 @@ import android.os.Build import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -14,9 +13,10 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.supervisorScope import org.openedx.core.config.Config import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.CourseAccessError @@ -170,47 +170,27 @@ class CourseContainerViewModel( _showProgress.value = true viewModelScope.launch { - try { - val (courseStructure, courseEnrollmentDetails) = fetchCourseData(courseId) - _showProgress.value = false - when { - courseEnrollmentDetails != null -> { - handleCourseEnrollment(courseEnrollmentDetails) - } - - courseStructure != null -> { - handleCourseStructureOnly(courseStructure) - } - - else -> { - _courseAccessStatus.value = CourseAccessError.UNKNOWN - } + val courseStructureFlow = interactor.getCourseStructureFlow(courseId) + .catch { e -> + handleFetchError(e) + emit(null) } - } catch (e: Exception) { - e.printStackTrace() + val courseDetailsFlow = interactor.getEnrollmentDetailsFlow(courseId) + .catch { emit(null) } + courseStructureFlow.combine(courseDetailsFlow) { courseStructure, courseEnrollmentDetails -> + courseStructure to courseEnrollmentDetails + }.catch { e -> handleFetchError(e) - _showProgress.value = false + }.collect { (courseStructure, courseEnrollmentDetails) -> + when { + courseEnrollmentDetails != null -> handleCourseEnrollment(courseEnrollmentDetails) + courseStructure != null -> handleCourseStructureOnly(courseStructure) + else -> _courseAccessStatus.value = CourseAccessError.UNKNOWN + } } } } - private suspend fun fetchCourseData( - courseId: String - ): Pair = supervisorScope { - val deferredCourse = async { - runCatching { - interactor.getCourseStructure(courseId, isNeedRefresh = true) - }.getOrNull() - } - val deferredEnrollment = async { - runCatching { - interactor.getEnrollmentDetails(courseId) - }.getOrNull() - } - - Pair(deferredCourse.await(), deferredEnrollment.await()) - } - /** * Handles the scenario where [CourseEnrollmentDetails] is successfully fetched. */ @@ -262,15 +242,17 @@ class CourseContainerViewModel( _dataReady.value = true } - private fun handleFetchError(e: Exception) { + private fun handleFetchError(e: Throwable) { + e.printStackTrace() if (isNetworkRelatedError(e)) { _errorMessage.value = resourceManager.getString(CoreR.string.core_error_no_connection) } else { _courseAccessStatus.value = CourseAccessError.UNKNOWN } + _showProgress.value = false } - private fun isNetworkRelatedError(e: Exception): Boolean { + private fun isNetworkRelatedError(e: Throwable): Boolean { return e.isInternetError() || e is NoCachedDataException } diff --git a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt index 4b373b05f..916213026 100644 --- a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt @@ -8,6 +8,8 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import org.openedx.core.BlockType import org.openedx.core.R @@ -17,7 +19,6 @@ import org.openedx.core.domain.model.Block import org.openedx.core.domain.model.CourseComponentStatus import org.openedx.core.domain.model.CourseDateBlock import org.openedx.core.domain.model.CourseDatesBannerInfo -import org.openedx.core.domain.model.CourseDatesResult import org.openedx.core.domain.model.CourseStructure import org.openedx.core.extension.getSequentialBlocks import org.openedx.core.extension.getVerticalBlocks @@ -183,48 +184,31 @@ class CourseOutlineViewModel( private fun getCourseDataInternal() { viewModelScope.launch { - try { - val courseStructure = interactor.getCourseStructure(courseId) + val courseStructureFlow = interactor.getCourseStructureFlow(courseId, false) + .catch { emit(null) } + val courseStatusFlow = interactor.getCourseStatusFlow(courseId) + val courseDatesFlow = interactor.getCourseDatesFlow(courseId) + combine( + courseStructureFlow, + courseStatusFlow, + courseDatesFlow + ) { courseStructure, courseStatus, courseDatesResult -> + Triple(courseStructure, courseStatus, courseDatesResult) + }.catch { e -> + handleCourseDataError(e) + }.collect { (courseStructure, courseStatus, courseDates) -> + if (courseStructure == null) return@collect val blocks = courseStructure.blockData - val courseStatus = fetchCourseStatus() - val courseDatesResult = fetchCourseDates() - val datesBannerInfo = courseDatesResult.courseBanner + val datesBannerInfo = courseDates.courseBanner - checkIfCalendarOutOfDate(courseDatesResult.datesSection.values.flatten()) + checkIfCalendarOutOfDate(courseDates.datesSection.values.flatten()) updateOutdatedOfflineXBlocks(courseStructure) initializeCourseData(blocks, courseStructure, courseStatus, datesBannerInfo) - } catch (e: Exception) { - handleCourseDataError(e) } } } - private suspend fun fetchCourseStatus(): CourseComponentStatus { - return if (networkConnection.isOnline()) { - interactor.getCourseStatus(courseId) - } else { - CourseComponentStatus("") - } - } - - private suspend fun fetchCourseDates(): CourseDatesResult { - return if (networkConnection.isOnline()) { - interactor.getCourseDates(courseId) - } else { - CourseDatesResult( - datesSection = linkedMapOf(), - courseBanner = CourseDatesBannerInfo( - missedDeadlines = false, - missedGatedContent = false, - verifiedUpgradeLink = "", - contentTypeGatingEnabled = false, - hasEnded = false - ) - ) - } - } - private suspend fun initializeCourseData( blocks: List, courseStructure: CourseStructure, @@ -253,10 +237,10 @@ class CourseOutlineViewModel( ) } - private suspend fun handleCourseDataError(e: Exception) { + private suspend fun handleCourseDataError(e: Throwable?) { _uiState.value = CourseOutlineUIState.Error val errorMessage = when { - e.isInternetError() -> R.string.core_error_no_connection + e?.isInternetError() == true -> R.string.core_error_no_connection else -> R.string.core_error_unknown_error } _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(errorMessage))) @@ -279,8 +263,10 @@ class CourseOutlineViewModel( block.descendants.forEach { descendantId -> val sequentialBlock = blocks.find { it.id == descendantId } ?: return@forEach addSequentialBlockToSubSections(block, sequentialBlock) - courseSubSectionUnit[sequentialBlock.id] = sequentialBlock.getFirstDescendantBlock(blocks) - subSectionsDownloadsCount[sequentialBlock.id] = sequentialBlock.getDownloadsCount(blocks) + courseSubSectionUnit[sequentialBlock.id] = + sequentialBlock.getFirstDescendantBlock(blocks) + subSectionsDownloadsCount[sequentialBlock.id] = + sequentialBlock.getDownloadsCount(blocks) addDownloadableChildrenForSequentialBlock(sequentialBlock) } } @@ -434,10 +420,12 @@ class CourseOutlineViewModel( viewModelScope.launch { val courseData = _uiState.value as? CourseOutlineUIState.CourseData ?: return@launch - val subSectionsBlocks = courseData.courseSubSections.values.flatten().filter { it.id in blocksIds } + val subSectionsBlocks = + courseData.courseSubSections.values.flatten().filter { it.id in blocksIds } val blocks = subSectionsBlocks.flatMap { subSectionsBlock -> - val verticalBlocks = allBlocks.values.filter { it.id in subSectionsBlock.descendants } + val verticalBlocks = + allBlocks.values.filter { it.id in subSectionsBlock.descendants } allBlocks.values.filter { it.id in verticalBlocks.flatMap { it.descendants } } } @@ -446,9 +434,12 @@ class CourseOutlineViewModel( val isAllBlocksDownloaded = downloadableBlocks.all { isBlockDownloaded(it.id) } val notDownloadedSubSectionBlocks = subSectionsBlocks.mapNotNull { subSectionsBlock -> - val verticalBlocks = allBlocks.values.filter { it.id in subSectionsBlock.descendants } + val verticalBlocks = + allBlocks.values.filter { it.id in subSectionsBlock.descendants } val notDownloadedBlocks = allBlocks.values.filter { - it.id in verticalBlocks.flatMap { it.descendants } && it.isDownloadable && !isBlockDownloaded(it.id) + it.id in verticalBlocks.flatMap { it.descendants } && it.isDownloadable && !isBlockDownloaded( + it.id + ) } if (notDownloadedBlocks.isNotEmpty()) { subSectionsBlock @@ -462,7 +453,8 @@ class CourseOutlineViewModel( } if (downloadingBlocks.isNotEmpty()) { - val downloadableChildren = downloadingBlocks.flatMap { getDownloadableChildren(it).orEmpty() } + val downloadableChildren = + downloadingBlocks.flatMap { getDownloadableChildren(it).orEmpty() } if (config.getCourseUIConfig().isCourseDownloadQueueEnabled) { courseRouter.navigateToDownloadQueue(fragmentManager, downloadableChildren) } else { diff --git a/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt index 531bef58f..f9b17792c 100644 --- a/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt @@ -11,6 +11,8 @@ import io.mockk.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain @@ -215,6 +217,7 @@ class CourseContainerViewModelTest { Dispatchers.resetMain() } + @Suppress("TooGenericExceptionThrown") @Test fun `getCourseEnrollmentDetails unknown exception`() = runTest { val viewModel = CourseContainerViewModel( @@ -233,8 +236,12 @@ class CourseContainerViewModelTest { courseRouter ) every { networkConnection.isOnline() } returns true - coEvery { interactor.getCourseStructure(any(), any()) } throws Exception() - coEvery { interactor.getEnrollmentDetails(any()) } throws Exception() + coEvery { + interactor.getCourseStructureFlow(any(), any()) + } returns flowOf(null) + coEvery { + interactor.getEnrollmentDetailsFlow(any()) + } returns flow { throw Exception() } every { analytics.logScreenEvent( CourseAnalyticsEvent.DASHBOARD.eventName, @@ -250,7 +257,7 @@ class CourseContainerViewModelTest { viewModel.fetchCourseDetails() advanceUntilIdle() - coVerify(exactly = 1) { interactor.getEnrollmentDetails(any()) } + coVerify(exactly = 1) { interactor.getEnrollmentDetailsFlow(any()) } verify(exactly = 1) { analytics.logScreenEvent( CourseAnalyticsEvent.DASHBOARD.eventName, @@ -285,8 +292,8 @@ class CourseContainerViewModelTest { courseRouter ) every { networkConnection.isOnline() } returns true - coEvery { interactor.getCourseStructure(any(), any()) } returns courseStructure - coEvery { interactor.getEnrollmentDetails(any()) } returns enrollmentDetails + coEvery { interactor.getCourseStructureFlow(any(), any()) } returns flowOf(courseStructure) + coEvery { interactor.getEnrollmentDetailsFlow(any()) } returns flowOf(enrollmentDetails) every { analytics.logScreenEvent( CourseAnalyticsEvent.DASHBOARD.eventName, @@ -302,7 +309,7 @@ class CourseContainerViewModelTest { viewModel.fetchCourseDetails() advanceUntilIdle() - coVerify(exactly = 1) { interactor.getEnrollmentDetails(any()) } + coVerify(exactly = 1) { interactor.getEnrollmentDetailsFlow(any()) } verify(exactly = 1) { analytics.logScreenEvent( CourseAnalyticsEvent.DASHBOARD.eventName, @@ -338,7 +345,8 @@ class CourseContainerViewModelTest { courseRouter ) every { networkConnection.isOnline() } returns false - coEvery { interactor.getEnrollmentDetails(any()) } returns enrollmentDetails + coEvery { interactor.getCourseStructureFlow(any(), any()) } returns flowOf(courseStructure) + coEvery { interactor.getEnrollmentDetailsFlow(any()) } returns flowOf(enrollmentDetails) every { analytics.logScreenEvent( CourseAnalyticsEvent.DASHBOARD.eventName, diff --git a/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt index 663409188..c95916668 100644 --- a/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain @@ -239,6 +240,7 @@ class CourseOutlineViewModelTest { every { preferencesManager.isRelativeDatesEnabled } returns true coEvery { interactor.getCourseDates(any()) } returns mockedCourseDatesResult + coEvery { interactor.getCourseDatesFlow(any()) } returns flowOf(mockedCourseDatesResult) } @After @@ -247,51 +249,65 @@ class CourseOutlineViewModelTest { } @Test - fun `getCourseDataInternal no internet connection exception`() = runTest(UnconfinedTestDispatcher()) { - coEvery { interactor.getCourseStructure(any()) } returns courseStructure - every { networkConnection.isOnline() } returns true - every { downloadDao.getAllDataFlow() } returns flow { emit(emptyList()) } - every { downloadDialogManager.showPopup(any(), any(), any(), any(), any(), any(), any()) } returns Unit - coEvery { interactor.getCourseStatus(any()) } throws UnknownHostException() - - val viewModel = CourseOutlineViewModel( - "", - "", - config, - interactor, - resourceManager, - notifier, - networkConnection, - preferencesManager, - analytics, - downloadDialogManager, - fileUtil, - courseRouter, - coreAnalytics, - downloadDao, - workerController, - downloadHelper, - ) + fun `getCourseDataInternal no internet connection exception`() = + runTest(UnconfinedTestDispatcher()) { + coEvery { interactor.getCourseStructureFlow(any(), any()) } returns flowOf( + courseStructure + ) + every { networkConnection.isOnline() } returns true + every { downloadDao.getAllDataFlow() } returns flow { emit(emptyList()) } + every { + downloadDialogManager.showPopup( + any(), + any(), + any(), + any(), + any(), + any(), + any() + ) + } returns Unit + coEvery { interactor.getCourseStatusFlow(any()) } returns flow { throw UnknownHostException() } + + val viewModel = CourseOutlineViewModel( + "", + "", + config, + interactor, + resourceManager, + notifier, + networkConnection, + preferencesManager, + analytics, + downloadDialogManager, + fileUtil, + courseRouter, + coreAnalytics, + downloadDao, + workerController, + downloadHelper, + ) - val message = async { - viewModel.uiMessage.first() as? UIMessage.SnackBarMessage - } - viewModel.getCourseData() - advanceUntilIdle() + val message = async { + viewModel.uiMessage.first() as? UIMessage.SnackBarMessage + } + viewModel.getCourseData() + advanceUntilIdle() - coVerify(exactly = 2) { interactor.getCourseStructure(any()) } - coVerify(exactly = 2) { interactor.getCourseStatus(any()) } + coVerify(exactly = 2) { interactor.getCourseStructureFlow(any(), any()) } + coVerify(exactly = 2) { interactor.getCourseStatusFlow(any()) } - assertEquals(noInternet, message.await()?.message) - assert(viewModel.uiState.value is CourseOutlineUIState.Error) - } + assertEquals(noInternet, message.await()?.message) + assert(viewModel.uiState.value is CourseOutlineUIState.Error) + } + @Suppress("TooGenericExceptionThrown") @Test fun `getCourseDataInternal unknown exception`() = runTest(UnconfinedTestDispatcher()) { - coEvery { interactor.getCourseStructure(any()) } returns courseStructure + coEvery { interactor.getCourseStructureFlow(any(), any()) } returns flowOf(courseStructure) every { networkConnection.isOnline() } returns true every { downloadDao.getAllDataFlow() } returns flow { emit(emptyList()) } - coEvery { interactor.getCourseStatus(any()) } throws Exception() + coEvery { interactor.getCourseStatusFlow(any()) } returns flow { throw Exception() } val viewModel = CourseOutlineViewModel( "", "", @@ -317,167 +333,181 @@ class CourseOutlineViewModelTest { viewModel.getCourseData() advanceUntilIdle() - coVerify(exactly = 2) { interactor.getCourseStructure(any()) } - coVerify(exactly = 2) { interactor.getCourseStatus(any()) } + coVerify(exactly = 2) { interactor.getCourseStructureFlow(any(), any()) } + coVerify(exactly = 2) { interactor.getCourseStatusFlow(any()) } assertEquals(somethingWrong, message.await()?.message) assert(viewModel.uiState.value is CourseOutlineUIState.Error) } @Test - fun `getCourseDataInternal success with internet connection`() = runTest(UnconfinedTestDispatcher()) { - coEvery { interactor.getCourseStructure(any()) } returns courseStructure - every { networkConnection.isOnline() } returns true - coEvery { downloadDao.getAllDataFlow() } returns flow { - emit( - listOf( - DownloadModelEntity.createFrom( - downloadModel + fun `getCourseDataInternal success with internet connection`() = + runTest(UnconfinedTestDispatcher()) { + coEvery { interactor.getCourseStructureFlow(any(), any()) } returns flowOf( + courseStructure + ) + every { networkConnection.isOnline() } returns true + coEvery { downloadDao.getAllDataFlow() } returns flow { + emit( + listOf( + DownloadModelEntity.createFrom( + downloadModel + ) ) ) + } + coEvery { interactor.getCourseStatusFlow(any()) } returns flowOf(CourseComponentStatus("id")) + every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false + + val viewModel = CourseOutlineViewModel( + "", + "", + config, + interactor, + resourceManager, + notifier, + networkConnection, + preferencesManager, + analytics, + downloadDialogManager, + fileUtil, + courseRouter, + coreAnalytics, + downloadDao, + workerController, + downloadHelper, ) - } - coEvery { interactor.getCourseStatus(any()) } returns CourseComponentStatus("id") - every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false - val viewModel = CourseOutlineViewModel( - "", - "", - config, - interactor, - resourceManager, - notifier, - networkConnection, - preferencesManager, - analytics, - downloadDialogManager, - fileUtil, - courseRouter, - coreAnalytics, - downloadDao, - workerController, - downloadHelper, - ) - - val message = async { - withTimeoutOrNull(5000) { - viewModel.uiMessage.first() as? UIMessage.SnackBarMessage + val message = async { + withTimeoutOrNull(5000) { + viewModel.uiMessage.first() as? UIMessage.SnackBarMessage + } } - } - viewModel.getCourseData() - advanceUntilIdle() + viewModel.getCourseData() + advanceUntilIdle() - coVerify(exactly = 2) { interactor.getCourseStructure(any()) } - coVerify(exactly = 2) { interactor.getCourseStatus(any()) } + coVerify(exactly = 2) { interactor.getCourseStructureFlow(any(), any()) } + coVerify(exactly = 2) { interactor.getCourseStatusFlow(any()) } - assert(message.await() == null) - assert(viewModel.uiState.value is CourseOutlineUIState.CourseData) - } + assert(message.await() == null) + assert(viewModel.uiState.value is CourseOutlineUIState.CourseData) + } @Test - fun `getCourseDataInternal success without internet connection`() = runTest(UnconfinedTestDispatcher()) { - coEvery { interactor.getCourseStructure(any()) } returns courseStructure - every { networkConnection.isOnline() } returns false - coEvery { downloadDao.getAllDataFlow() } returns flow { - emit( - listOf( - DownloadModelEntity.createFrom( - downloadModel + fun `getCourseDataInternal success without internet connection`() = + runTest(UnconfinedTestDispatcher()) { + coEvery { interactor.getCourseStructureFlow(any(), any()) } returns flowOf( + courseStructure + ) + every { networkConnection.isOnline() } returns false + coEvery { downloadDao.getAllDataFlow() } returns flow { + emit( + listOf( + DownloadModelEntity.createFrom( + downloadModel + ) ) ) + } + coEvery { interactor.getCourseStatusFlow(any()) } returns flowOf(CourseComponentStatus("id")) + every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false + + val viewModel = CourseOutlineViewModel( + "", + "", + config, + interactor, + resourceManager, + notifier, + networkConnection, + preferencesManager, + analytics, + downloadDialogManager, + fileUtil, + courseRouter, + coreAnalytics, + downloadDao, + workerController, + downloadHelper, ) - } - coEvery { interactor.getCourseStatus(any()) } returns CourseComponentStatus("id") - every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false - val viewModel = CourseOutlineViewModel( - "", - "", - config, - interactor, - resourceManager, - notifier, - networkConnection, - preferencesManager, - analytics, - downloadDialogManager, - fileUtil, - courseRouter, - coreAnalytics, - downloadDao, - workerController, - downloadHelper, - ) - - val message = async { - withTimeoutOrNull(5000) { - viewModel.uiMessage.first() as? UIMessage.SnackBarMessage + val message = async { + withTimeoutOrNull(5000) { + viewModel.uiMessage.first() as? UIMessage.SnackBarMessage + } } - } - viewModel.getCourseData() - advanceUntilIdle() + viewModel.getCourseData() + advanceUntilIdle() - coVerify(exactly = 2) { interactor.getCourseStructure(any()) } - coVerify(exactly = 0) { interactor.getCourseStatus(any()) } + coVerify(exactly = 2) { interactor.getCourseStructureFlow(any(), any()) } + coVerify(exactly = 2) { interactor.getCourseStatusFlow(any()) } - assert(message.await() == null) - assert(viewModel.uiState.value is CourseOutlineUIState.CourseData) - } + assert(message.await() == null) + assert(viewModel.uiState.value is CourseOutlineUIState.CourseData) + } @Test - fun `updateCourseData success with internet connection`() = runTest(UnconfinedTestDispatcher()) { - coEvery { interactor.getCourseStructure(any()) } returns courseStructure - every { networkConnection.isOnline() } returns true - coEvery { downloadDao.getAllDataFlow() } returns flow { - emit( - listOf( - DownloadModelEntity.createFrom( - downloadModel + fun `updateCourseData success with internet connection`() = + runTest(UnconfinedTestDispatcher()) { + coEvery { interactor.getCourseStructureFlow(any(), any()) } returns flowOf( + courseStructure + ) + every { networkConnection.isOnline() } returns true + coEvery { downloadDao.getAllDataFlow() } returns flow { + emit( + listOf( + DownloadModelEntity.createFrom( + downloadModel + ) ) ) + } + coEvery { interactor.getCourseStatusFlow(any()) } returns flowOf(CourseComponentStatus("id")) + every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false + + val viewModel = CourseOutlineViewModel( + "", + "", + config, + interactor, + resourceManager, + notifier, + networkConnection, + preferencesManager, + analytics, + downloadDialogManager, + fileUtil, + courseRouter, + coreAnalytics, + downloadDao, + workerController, + downloadHelper, ) - } - coEvery { interactor.getCourseStatus(any()) } returns CourseComponentStatus("id") - every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false - val viewModel = CourseOutlineViewModel( - "", - "", - config, - interactor, - resourceManager, - notifier, - networkConnection, - preferencesManager, - analytics, - downloadDialogManager, - fileUtil, - courseRouter, - coreAnalytics, - downloadDao, - workerController, - downloadHelper, - ) - - val message = async { - withTimeoutOrNull(5000) { - viewModel.uiMessage.first() as? UIMessage.SnackBarMessage + val message = async { + withTimeoutOrNull(5000) { + viewModel.uiMessage.first() as? UIMessage.SnackBarMessage + } } - } - viewModel.getCourseData() - advanceUntilIdle() + viewModel.getCourseData() + advanceUntilIdle() - coVerify(exactly = 2) { interactor.getCourseStructure(any()) } - coVerify(exactly = 2) { interactor.getCourseStatus(any()) } + coVerify(exactly = 2) { interactor.getCourseStructureFlow(any(), any()) } + coVerify(exactly = 2) { interactor.getCourseStatusFlow(any()) } - assert(message.await() == null) - assert(viewModel.uiState.value is CourseOutlineUIState.CourseData) - } + assert(message.await() == null) + assert(viewModel.uiState.value is CourseOutlineUIState.CourseData) + } @Test fun `CourseStructureUpdated notifier test`() = runTest(UnconfinedTestDispatcher()) { coEvery { downloadDao.getAllDataFlow() } returns flow { emit(emptyList()) } + coEvery { interactor.getCourseStructureFlow(any(), any()) } returns flowOf(courseStructure) + coEvery { notifier.notifier } returns flow { emit(CourseStructureUpdated("")) } + every { networkConnection.isOnline() } returns true + coEvery { interactor.getCourseStatusFlow(any()) } returns flowOf(CourseComponentStatus("id")) + val viewModel = CourseOutlineViewModel( "", "", @@ -496,10 +526,6 @@ class CourseOutlineViewModelTest { workerController, downloadHelper, ) - coEvery { notifier.notifier } returns flow { emit(CourseStructureUpdated("")) } - coEvery { interactor.getCourseStructure(any()) } returns courseStructure - every { networkConnection.isOnline() } returns true - coEvery { interactor.getCourseStatus(any()) } returns CourseComponentStatus("id") val mockLifeCycleOwner: LifecycleOwner = mockk() val lifecycleRegistry = LifecycleRegistry(mockLifeCycleOwner) @@ -509,14 +535,15 @@ class CourseOutlineViewModelTest { viewModel.getCourseData() advanceUntilIdle() - coVerify(exactly = 2) { interactor.getCourseStructure(any()) } - coVerify(exactly = 1) { interactor.getCourseStatus(any()) } + coVerify(exactly = 3) { interactor.getCourseStructureFlow(any(), any()) } + coVerify(exactly = 3) { interactor.getCourseStatusFlow(any()) } } @Test fun `saveDownloadModels test`() = runTest(UnconfinedTestDispatcher()) { every { preferencesManager.videoSettings.wifiDownloadOnly } returns false coEvery { interactor.getCourseStructure(any()) } returns courseStructure + coEvery { interactor.getCourseStructureFlow(any(), any()) } returns flowOf(courseStructure) every { networkConnection.isWifiConnected() } returns true every { networkConnection.isOnline() } returns true every { @@ -527,6 +554,7 @@ class CourseOutlineViewModelTest { } returns Unit coEvery { workerController.saveModels(any()) } returns Unit coEvery { interactor.getCourseStatus(any()) } returns CourseComponentStatus("id") + coEvery { interactor.getCourseStatusFlow(any()) } returns flowOf(CourseComponentStatus("id")) coEvery { downloadDao.getAllDataFlow() } returns flow { emit(emptyList()) } every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false @@ -566,43 +594,48 @@ class CourseOutlineViewModelTest { } @Test - fun `saveDownloadModels only wifi download, with connection`() = runTest(UnconfinedTestDispatcher()) { - coEvery { interactor.getCourseStructure(any()) } returns courseStructure - coEvery { interactor.getCourseStatus(any()) } returns CourseComponentStatus("id") - every { preferencesManager.videoSettings.wifiDownloadOnly } returns true - every { networkConnection.isWifiConnected() } returns true - every { networkConnection.isOnline() } returns true - coEvery { workerController.saveModels(any()) } returns Unit - coEvery { downloadDao.getAllDataFlow() } returns flow { emit(emptyList()) } - every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false - every { coreAnalytics.logEvent(any(), any()) } returns Unit - - val viewModel = CourseOutlineViewModel( - "", - "", - config, - interactor, - resourceManager, - notifier, - networkConnection, - preferencesManager, - analytics, - downloadDialogManager, - fileUtil, - courseRouter, - coreAnalytics, - downloadDao, - workerController, - downloadHelper, - ) - val message = async { - withTimeoutOrNull(5000) { - viewModel.uiMessage.first() as? UIMessage.SnackBarMessage + fun `saveDownloadModels only wifi download, with connection`() = + runTest(UnconfinedTestDispatcher()) { + coEvery { interactor.getCourseStructure(any()) } returns courseStructure + coEvery { interactor.getCourseStructureFlow(any(), any()) } returns flowOf( + courseStructure + ) + coEvery { interactor.getCourseStatus(any()) } returns CourseComponentStatus("id") + coEvery { interactor.getCourseStatusFlow(any()) } returns flowOf(CourseComponentStatus("id")) + every { preferencesManager.videoSettings.wifiDownloadOnly } returns true + every { networkConnection.isWifiConnected() } returns true + every { networkConnection.isOnline() } returns true + coEvery { workerController.saveModels(any()) } returns Unit + coEvery { downloadDao.getAllDataFlow() } returns flow { emit(emptyList()) } + every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false + every { coreAnalytics.logEvent(any(), any()) } returns Unit + + val viewModel = CourseOutlineViewModel( + "", + "", + config, + interactor, + resourceManager, + notifier, + networkConnection, + preferencesManager, + analytics, + downloadDialogManager, + fileUtil, + courseRouter, + coreAnalytics, + downloadDao, + workerController, + downloadHelper, + ) + val message = async { + withTimeoutOrNull(5000) { + viewModel.uiMessage.first() as? UIMessage.SnackBarMessage + } } - } - viewModel.saveDownloadModels("", "") - advanceUntilIdle() + viewModel.saveDownloadModels("", "") + advanceUntilIdle() - assert(message.await()?.message.isNullOrEmpty()) - } + assert(message.await()?.message.isNullOrEmpty()) + } } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt index c8363e24d..80c0d5fce 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt @@ -58,11 +58,24 @@ class AllEnrolledCoursesViewModel( init { collectDiscoveryNotifier() - getCourses(currentFilter.value) + loadInitialCourses() } - fun getCourses(courseStatusFilter: CourseStatusFilter? = null) { - _uiState.update { it.copy(showProgress = true) } + private fun loadInitialCourses() { + viewModelScope.launch { + _uiState.update { it.copy(showProgress = true) } + val cachedList = interactor.getEnrolledCoursesFromCache() + if (cachedList.isNotEmpty()) { + _uiState.update { it.copy(courses = cachedList.toList(), showProgress = false) } + } + getCourses(showLoadingProgress = false) + } + } + + fun getCourses(courseStatusFilter: CourseStatusFilter? = null, showLoadingProgress: Boolean = true) { + if (showLoadingProgress) { + _uiState.update { it.copy(showProgress = true) } + } coursesList.clear() internalLoadingCourses(courseStatusFilter ?: currentFilter.value) } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt index aacb85719..0ca8f4a6e 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt @@ -67,6 +67,20 @@ class DashboardGalleryViewModel( fun getCourses() { viewModelScope.launch { try { + val cachedCourseEnrollments = fileUtil.getObjectFromFile() + if (cachedCourseEnrollments == null) { + if (networkConnection.isOnline()) { + _uiState.value = DashboardGalleryUIState.Loading + } else { + _uiState.value = DashboardGalleryUIState.Empty + } + } else { + _uiState.value = + DashboardGalleryUIState.Courses( + cachedCourseEnrollments.mapToDomain(), + corePreferences.isRelativeDatesEnabled + ) + } if (networkConnection.isOnline()) { isLoading = true val pageSize = if (windowSize.isTablet) { @@ -83,17 +97,6 @@ class DashboardGalleryViewModel( corePreferences.isRelativeDatesEnabled ) } - } else { - val courseEnrollments = fileUtil.getObjectFromFile() - if (courseEnrollments == null) { - _uiState.value = DashboardGalleryUIState.Empty - } else { - _uiState.value = - DashboardGalleryUIState.Courses( - courseEnrollments.mapToDomain(), - corePreferences.isRelativeDatesEnabled - ) - } } } catch (e: Exception) { if (e.isInternetError()) {