diff --git a/app/build.gradle b/app/build.gradle index dcc58cb90..5fd9292da 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -166,6 +166,7 @@ dependencies { implementation "com.google.android.material:material:1.3.0" implementation "com.github.wulkanowy:material-chips-input:2.1.1" implementation "com.github.PhilJay:MPAndroidChart:v3.1.0" + implementation 'com.mikhaellopez:circularimageview:4.2.0' implementation "androidx.work:work-runtime-ktx:$work_manager" playImplementation "androidx.work:work-gcm:$work_manager" diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/35.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/35.json new file mode 100644 index 000000000..f166b2d72 --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/35.json @@ -0,0 +1,2148 @@ +{ + "formatVersion": 1, + "database": { + "version": 35, + "identityHash": "15fb91ec180fe60bf400cfa729c3418d", + "entities": [ + { + "tableName": "Students", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `nick` TEXT NOT NULL, `avatar_color` INTEGER NOT NULL, `scrapper_base_url` TEXT NOT NULL, `mobile_base_url` TEXT NOT NULL, `login_type` TEXT NOT NULL, `login_mode` TEXT NOT NULL, `certificate_key` TEXT NOT NULL, `private_key` TEXT NOT NULL, `is_parent` INTEGER NOT NULL, `email` TEXT NOT NULL, `password` TEXT NOT NULL, `symbol` TEXT NOT NULL, `student_id` INTEGER NOT NULL, `user_login_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `student_name` TEXT NOT NULL, `school_id` TEXT NOT NULL, `school_short` TEXT NOT NULL, `school_name` TEXT NOT NULL, `class_name` TEXT NOT NULL, `class_id` INTEGER NOT NULL, `is_current` INTEGER NOT NULL, `registration_date` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nick", + "columnName": "nick", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarColor", + "columnName": "avatar_color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scrapperBaseUrl", + "columnName": "scrapper_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mobileBaseUrl", + "columnName": "mobile_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginType", + "columnName": "login_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginMode", + "columnName": "login_mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificateKey", + "columnName": "certificate_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "private_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isParent", + "columnName": "is_parent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "user_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "student_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolSymbol", + "columnName": "school_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "school_short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolName", + "columnName": "school_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "className", + "columnName": "class_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCurrent", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registrationDate", + "columnName": "registration_date", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Students_email_symbol_student_id_school_id_class_id", + "unique": true, + "columnNames": [ + "email", + "symbol", + "student_id", + "school_id", + "class_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryName", + "columnName": "diary_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolYear", + "columnName": "school_year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterName", + "columnName": "semester_name", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "semester_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `semester_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Exams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Timetable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `number` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `subjectOld` TEXT NOT NULL, `group` TEXT NOT NULL, `room` TEXT NOT NULL, `roomOld` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacherOld` TEXT NOT NULL, `info` TEXT NOT NULL, `student_plan` INTEGER NOT NULL, `changes` INTEGER NOT NULL, `canceled` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectOld", + "columnName": "subjectOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "room", + "columnName": "room", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roomOld", + "columnName": "roomOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherOld", + "columnName": "teacherOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isStudentPlan", + "columnName": "student_plan", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "changes", + "columnName": "changes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canceled", + "columnName": "canceled", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Attendance", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `time_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `excusable` INTEGER NOT NULL, `excuse_status` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeId", + "columnName": "time_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excusable", + "columnName": "excusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excuseStatus", + "columnName": "excuse_status", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AttendanceSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `subject_id` INTEGER NOT NULL, `month` INTEGER NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `absence_excused` INTEGER NOT NULL, `absence_for_school_reasons` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `lateness_excused` INTEGER NOT NULL, `exemption` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subject_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "month", + "columnName": "month", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceExcused", + "columnName": "absence_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceForSchoolReasons", + "columnName": "absence_for_school_reasons", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latenessExcused", + "columnName": "lateness_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Grades", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `entry` TEXT NOT NULL, `value` REAL NOT NULL, `modifier` REAL NOT NULL, `comment` TEXT NOT NULL, `color` TEXT NOT NULL, `grade_symbol` TEXT NOT NULL, `description` TEXT NOT NULL, `weight` TEXT NOT NULL, `weightValue` REAL NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entry", + "columnName": "entry", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "modifier", + "columnName": "modifier", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gradeSymbol", + "columnName": "grade_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weightValue", + "columnName": "weightValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_predicted_grade_notified` INTEGER NOT NULL, `is_final_grade_notified` INTEGER NOT NULL, `predicted_grade_last_change` INTEGER NOT NULL, `final_grade_last_change` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `subject` TEXT NOT NULL, `predicted_grade` TEXT NOT NULL, `final_grade` TEXT NOT NULL, `proposed_points` TEXT NOT NULL, `final_points` TEXT NOT NULL, `points_sum` TEXT NOT NULL, `average` REAL NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPredictedGradeNotified", + "columnName": "is_predicted_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFinalGradeNotified", + "columnName": "is_final_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "predictedGradeLastChange", + "columnName": "predicted_grade_last_change", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "finalGradeLastChange", + "columnName": "final_grade_last_change", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "predictedGrade", + "columnName": "predicted_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalGrade", + "columnName": "final_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "proposedPoints", + "columnName": "proposed_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalPoints", + "columnName": "final_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pointsSum", + "columnName": "points_sum", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "average", + "columnName": "average", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradePartialStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `class_average` TEXT NOT NULL, `student_average` TEXT NOT NULL, `class_amounts` TEXT NOT NULL, `student_amounts` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAverage", + "columnName": "class_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAverage", + "columnName": "student_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAmounts", + "columnName": "class_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAmounts", + "columnName": "student_amounts", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesPointsStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `others` REAL NOT NULL, `student` REAL NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "others", + "columnName": "others", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "student", + "columnName": "student", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradeSemesterStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `amounts` TEXT NOT NULL, `student_grade` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amounts", + "columnName": "amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentGrade", + "columnName": "student_grade", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `unread_by` INTEGER NOT NULL, `read_by` INTEGER NOT NULL, `content` TEXT NOT NULL, `student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `recipient_name` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `removed` INTEGER NOT NULL, `has_attachments` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipient", + "columnName": "recipient_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "removed", + "columnName": "removed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasAttachments", + "columnName": "has_attachments", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `one_drive_id` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`real_id`))", + "fields": [ + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "oneDriveId", + "columnName": "one_drive_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "real_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `category` TEXT NOT NULL, `category_type` INTEGER NOT NULL, `is_points_show` INTEGER NOT NULL, `points` INTEGER NOT NULL, `content` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryType", + "columnName": "category_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPointsShow", + "columnName": "is_points_show", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Homework", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_done` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `attachments` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "is_done", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Subjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `name` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "LuckyNumbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `lucky_number` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "luckyNumber", + "columnName": "lucky_number", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CompletedLesson", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `topic` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `substitution` TEXT NOT NULL, `absence` TEXT NOT NULL, `resources` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "topic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "substitution", + "columnName": "substitution", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "resources", + "columnName": "resources", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ReportingUnits", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `short` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `roles` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "senderName", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roles", + "columnName": "roles", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `real_id` TEXT NOT NULL, `name` TEXT NOT NULL, `real_name` TEXT NOT NULL, `login_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `role` INTEGER NOT NULL, `hash` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userLoginId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "realName", + "columnName": "real_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginId", + "columnName": "login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MobileDevices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `date` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userLoginId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Teachers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `short_name` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "School", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `contact` TEXT NOT NULL, `headmaster` TEXT NOT NULL, `pedagogue` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contact", + "columnName": "contact", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headmaster", + "columnName": "headmaster", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pedagogue", + "columnName": "pedagogue", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Conferences", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `subject` TEXT NOT NULL, `agenda` TEXT NOT NULL, `present_on_conference` TEXT NOT NULL, `conference_id` INTEGER NOT NULL, `date` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agenda", + "columnName": "agenda", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presentOnConference", + "columnName": "present_on_conference", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conference_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableAdditional", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StudentInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `full_name` TEXT NOT NULL, `first_name` TEXT NOT NULL, `second_name` TEXT NOT NULL, `surname` TEXT NOT NULL, `birth_date` INTEGER NOT NULL, `birth_place` TEXT NOT NULL, `gender` TEXT NOT NULL, `has_polish_citizenship` INTEGER NOT NULL, `family_name` TEXT NOT NULL, `parents_names` TEXT NOT NULL, `address` TEXT NOT NULL, `registered_address` TEXT NOT NULL, `correspondence_address` TEXT NOT NULL, `phone_number` TEXT NOT NULL, `cell_phone_number` TEXT NOT NULL, `email` TEXT NOT NULL, `first_guardian_full_name` TEXT, `first_guardian_kinship` TEXT, `first_guardian_address` TEXT, `first_guardian_phones` TEXT, `first_guardian_email` TEXT, `second_guardian_full_name` TEXT, `second_guardian_kinship` TEXT, `second_guardian_address` TEXT, `second_guardian_phones` TEXT, `second_guardian_email` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "full_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondName", + "columnName": "second_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "surname", + "columnName": "surname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "birthDate", + "columnName": "birth_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthPlace", + "columnName": "birth_place", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gender", + "columnName": "gender", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPolishCitizenship", + "columnName": "has_polish_citizenship", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "familyName", + "columnName": "family_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentsNames", + "columnName": "parents_names", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "registeredAddress", + "columnName": "registered_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "correspondenceAddress", + "columnName": "correspondence_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cellPhoneNumber", + "columnName": "cell_phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstGuardian.fullName", + "columnName": "first_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.kinship", + "columnName": "first_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.address", + "columnName": "first_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.phones", + "columnName": "first_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.email", + "columnName": "first_guardian_email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.fullName", + "columnName": "second_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.kinship", + "columnName": "second_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.address", + "columnName": "second_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.phones", + "columnName": "second_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.email", + "columnName": "second_guardian_email", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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, '15fb91ec180fe60bf400cfa729c3418d')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/io/github/wulkanowy/data/db/migrations/AbstractMigrationTest.kt b/app/src/androidTest/java/io/github/wulkanowy/data/db/migrations/AbstractMigrationTest.kt index aca287322..f9fc76311 100644 --- a/app/src/androidTest/java/io/github/wulkanowy/data/db/migrations/AbstractMigrationTest.kt +++ b/app/src/androidTest/java/io/github/wulkanowy/data/db/migrations/AbstractMigrationTest.kt @@ -9,6 +9,7 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.platform.app.InstrumentationRegistry import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.SharedPrefProvider +import io.github.wulkanowy.utils.AppInfo import org.junit.Rule abstract class AbstractMigrationTest { @@ -24,12 +25,16 @@ abstract class AbstractMigrationTest { fun getMigratedRoomDatabase(): AppDatabase { val context = ApplicationProvider.getApplicationContext() - val database = Room.databaseBuilder(ApplicationProvider.getApplicationContext(), - AppDatabase::class.java, dbName) - .addMigrations(*AppDatabase.getMigrations(SharedPrefProvider(PreferenceManager - .getDefaultSharedPreferences(context))) + val database = Room.databaseBuilder( + ApplicationProvider.getApplicationContext(), + AppDatabase::class.java, + dbName + ).addMigrations( + *AppDatabase.getMigrations( + SharedPrefProvider(PreferenceManager.getDefaultSharedPreferences(context)), + AppInfo() ) - .build() + ).build() // close the database and release any stream resources when the test finishes helper.closeWhenFinished(database) return database diff --git a/app/src/androidTest/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt b/app/src/androidTest/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt new file mode 100644 index 000000000..0757cf927 --- /dev/null +++ b/app/src/androidTest/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt @@ -0,0 +1,60 @@ +package io.github.wulkanowy.data.db.migrations + +import android.content.ContentValues +import android.database.sqlite.SQLiteDatabase +import androidx.sqlite.db.SupportSQLiteDatabase +import io.github.wulkanowy.utils.AppInfo +import kotlinx.coroutines.runBlocking +import org.junit.Test +import kotlin.random.Random +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class Migration35Test : AbstractMigrationTest() { + + @Test + fun addRandomAvatarColorsForStudents() { + with(helper.createDatabase(dbName, 34)) { + createStudent(this, 1) + createStudent(this, 2) + } + + helper.runMigrationsAndValidate(dbName, 35, true, Migration35(AppInfo())) + + val db = getMigratedRoomDatabase() + val students = runBlocking { db.studentDao.loadAll() } + + assertEquals(2, students.size) + + assertTrue { students[0].avatarColor in AppInfo().defaultColorsForAvatar } + assertTrue { students[1].avatarColor in AppInfo().defaultColorsForAvatar } + } + + private fun createStudent(db: SupportSQLiteDatabase, id: Long) { + db.insert("Students", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply { + put("id", id) + put("scrapper_base_url", "https://fakelog.cf") + put("mobile_base_url", "") + put("login_mode", "SCRAPPER") + put("login_type", "STANDARD") + put("certificate_key", "") + put("private_key", "") + put("is_parent", false) + put("email", "jan@fakelog.cf") + put("password", "******") + put("symbol", "Default") + put("school_short", "") + put("class_name", "") + put("student_id", Random.nextInt()) + put("class_id", Random.nextInt()) + put("school_id", "123") + put("school_name", "Wulkan first class school") + put("is_current", false) + put("registration_date", "0") + put("user_login_id", Random.nextInt()) + put("student_name", "") + put("user_name", "") + put("nick", "") + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt b/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt index 223224e2d..46ff3651e 100644 --- a/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt +++ b/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt @@ -1,11 +1,13 @@ package io.github.wulkanowy +import android.annotation.SuppressLint import android.app.Application import android.content.Context import android.util.Log.DEBUG import android.util.Log.INFO import android.util.Log.VERBOSE import android.webkit.WebView +import androidx.fragment.app.FragmentManager import androidx.hilt.work.HiltWorkerFactory import androidx.multidex.MultiDex import androidx.work.Configuration @@ -46,8 +48,10 @@ class WulkanowyApp : Application(), Configuration.Provider { MultiDex.install(this) } + @SuppressLint("UnsafeOptInUsageWarning") override fun onCreate() { super.onCreate() + FragmentManager.enableNewStateManager(false) initializeAppLanguage() themeManager.applyDefaultTheme() diff --git a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt b/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt index 8b850659d..f61af4afd 100644 --- a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt +++ b/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt @@ -17,6 +17,7 @@ import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.utils.AppInfo import timber.log.Timber import javax.inject.Singleton @@ -60,7 +61,8 @@ internal class RepositoryModule { fun provideDatabase( @ApplicationContext context: Context, sharedPrefProvider: SharedPrefProvider, - ) = AppDatabase.newInstance(context, sharedPrefProvider) + appInfo: AppInfo + ) = AppDatabase.newInstance(context, sharedPrefProvider, appInfo) @Singleton @Provides diff --git a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt index ab89b84cb..ac05695b8 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt @@ -6,7 +6,6 @@ import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.RoomDatabase.JournalMode.TRUNCATE import androidx.room.TypeConverters -import androidx.room.migration.Migration import io.github.wulkanowy.data.db.dao.AttendanceDao import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao import io.github.wulkanowy.data.db.dao.CompletedLessonsDao @@ -86,12 +85,14 @@ import io.github.wulkanowy.data.db.migrations.Migration31 import io.github.wulkanowy.data.db.migrations.Migration32 import io.github.wulkanowy.data.db.migrations.Migration33 import io.github.wulkanowy.data.db.migrations.Migration34 +import io.github.wulkanowy.data.db.migrations.Migration35 import io.github.wulkanowy.data.db.migrations.Migration4 import io.github.wulkanowy.data.db.migrations.Migration5 import io.github.wulkanowy.data.db.migrations.Migration6 import io.github.wulkanowy.data.db.migrations.Migration7 import io.github.wulkanowy.data.db.migrations.Migration8 import io.github.wulkanowy.data.db.migrations.Migration9 +import io.github.wulkanowy.utils.AppInfo import javax.inject.Singleton @Singleton @@ -131,54 +132,55 @@ import javax.inject.Singleton abstract class AppDatabase : RoomDatabase() { companion object { - const val VERSION_SCHEMA = 34 + const val VERSION_SCHEMA = 35 - fun getMigrations(sharedPrefProvider: SharedPrefProvider): Array { - return arrayOf( - Migration2(), - Migration3(), - Migration4(), - Migration5(), - Migration6(), - Migration7(), - Migration8(), - Migration9(), - Migration10(), - Migration11(), - Migration12(), - Migration13(), - Migration14(), - Migration15(), - Migration16(), - Migration17(), - Migration18(), - Migration19(sharedPrefProvider), - Migration20(), - Migration21(), - Migration22(), - Migration23(), - Migration24(), - Migration25(), - Migration26(), - Migration27(), - Migration28(), - Migration29(), - Migration30(), - Migration31(), - Migration32(), - Migration33(), - Migration34() - ) - } + fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( + Migration2(), + Migration3(), + Migration4(), + Migration5(), + Migration6(), + Migration7(), + Migration8(), + Migration9(), + Migration10(), + Migration11(), + Migration12(), + Migration13(), + Migration14(), + Migration15(), + Migration16(), + Migration17(), + Migration18(), + Migration19(sharedPrefProvider), + Migration20(), + Migration21(), + Migration22(), + Migration23(), + Migration24(), + Migration25(), + Migration26(), + Migration27(), + Migration28(), + Migration29(), + Migration30(), + Migration31(), + Migration32(), + Migration33(), + Migration34(), + Migration35(appInfo) + ) - fun newInstance(context: Context, sharedPrefProvider: SharedPrefProvider): AppDatabase { - return Room.databaseBuilder(context, AppDatabase::class.java, "wulkanowy_database") - .setJournalMode(TRUNCATE) - .fallbackToDestructiveMigrationFrom(VERSION_SCHEMA + 1) - .fallbackToDestructiveMigrationOnDowngrade() - .addMigrations(*getMigrations(sharedPrefProvider)) - .build() - } + fun newInstance( + context: Context, + sharedPrefProvider: SharedPrefProvider, + appInfo: AppInfo + ) = Room.databaseBuilder(context, AppDatabase::class.java, "wulkanowy_database") + .setJournalMode(TRUNCATE) + .fallbackToDestructiveMigrationFrom(VERSION_SCHEMA + 1) + .fallbackToDestructiveMigrationOnDowngrade() + .addMigrations(*getMigrations(sharedPrefProvider, appInfo)) + .build() } abstract val studentDao: StudentDao diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt index e9c5f157e..0ad2ee590 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt @@ -8,7 +8,7 @@ import androidx.room.Query import androidx.room.Transaction import androidx.room.Update import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.db.entities.StudentNick +import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar import io.github.wulkanowy.data.db.entities.StudentWithSemesters import javax.inject.Singleton @@ -23,13 +23,13 @@ interface StudentDao { suspend fun delete(student: Student) @Update(entity = Student::class) - suspend fun update(studentNick: StudentNick) + suspend fun update(studentNickAndAvatar: StudentNickAndAvatar) @Query("SELECT * FROM Students WHERE is_current = 1") suspend fun loadCurrent(): Student? @Query("SELECT * FROM Students WHERE id = :id") - suspend fun loadById(id: Int): Student? + suspend fun loadById(id: Long): Student? @Query("SELECT * FROM Students") suspend fun loadAll(): List diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Message.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Message.kt index 1f10a1645..7b6e0dbf2 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Message.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Message.kt @@ -10,7 +10,7 @@ import java.time.LocalDateTime data class Message( @ColumnInfo(name = "student_id") - val studentId: Int, + val studentId: Long, @ColumnInfo(name = "real_id") val realId: Int, diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Student.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Student.kt index 6b60c8146..af9fe831a 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Student.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Student.kt @@ -81,4 +81,7 @@ data class Student( var id: Long = 0 var nick = "" + + @ColumnInfo(name = "avatar_color") + var avatarColor = 0L } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/StudentNick.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/StudentNickAndAvatar.kt similarity index 57% rename from app/src/main/java/io/github/wulkanowy/data/db/entities/StudentNick.kt rename to app/src/main/java/io/github/wulkanowy/data/db/entities/StudentNickAndAvatar.kt index 71f48f7a3..546059eea 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/StudentNick.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/StudentNickAndAvatar.kt @@ -1,13 +1,17 @@ package io.github.wulkanowy.data.db.entities +import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import java.io.Serializable @Entity -data class StudentNick( +data class StudentNickAndAvatar( - val nick: String + val nick: String, + + @ColumnInfo(name = "avatar_color") + var avatarColor: Long ) : Serializable { diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration35.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration35.kt new file mode 100644 index 000000000..cc540388c --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration35.kt @@ -0,0 +1,24 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.core.database.getLongOrNull +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import io.github.wulkanowy.utils.AppInfo + +class Migration35(private val appInfo: AppInfo) : Migration(34, 35) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE Students ADD COLUMN `avatar_color` INTEGER NOT NULL DEFAULT 0") + + val studentsCursor = database.query("SELECT * FROM Students") + + while (studentsCursor.moveToNext()) { + val studentId = studentsCursor.getLongOrNull(0) + database.execSQL( + """UPDATE Students + SET avatar_color = ${appInfo.defaultColorsForAvatar.random()} + WHERE id = $studentId""" + ) + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/MessageMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/MessageMapper.kt index 2c815b305..913e4d030 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/MessageMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/MessageMapper.kt @@ -4,14 +4,14 @@ import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.MessageAttachment import io.github.wulkanowy.data.db.entities.Recipient import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.sdk.pojo.Recipient as SdkRecipient -import io.github.wulkanowy.sdk.pojo.MessageAttachment as SdkMessageAttachment import java.time.LocalDateTime import io.github.wulkanowy.sdk.pojo.Message as SdkMessage +import io.github.wulkanowy.sdk.pojo.MessageAttachment as SdkMessageAttachment +import io.github.wulkanowy.sdk.pojo.Recipient as SdkRecipient fun List.mapToEntities(student: Student) = map { Message( - studentId = student.id.toInt(), + studentId = student.id, realId = it.id ?: 0, messageId = it.messageId ?: 0, sender = it.sender?.name.orEmpty(), diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/StudentMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/StudentMapper.kt index 67f56c628..c93323038 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/StudentMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/StudentMapper.kt @@ -5,7 +5,7 @@ import io.github.wulkanowy.data.db.entities.StudentWithSemesters import java.time.LocalDateTime import io.github.wulkanowy.sdk.pojo.Student as SdkStudent -fun List.mapToEntities(password: String = "") = map { +fun List.mapToEntities(password: String = "", colors: List) = map { StudentWithSemesters( student = Student( email = it.email, @@ -28,8 +28,10 @@ fun List.mapToEntities(password: String = "") = map { mobileBaseUrl = it.mobileBaseUrl, privateKey = it.privateKey, certificateKey = it.certificateKey, - loginMode = it.loginMode.name - ), + loginMode = it.loginMode.name, + ).apply { + avatarColor = colors.random() + }, semesters = it.semesters.mapToEntities(it.studentId) ) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt index 558214799..c2f364b3d 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt @@ -5,11 +5,12 @@ import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.data.db.dao.SemesterDao import io.github.wulkanowy.data.db.dao.StudentDao import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.db.entities.StudentNick +import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.DispatchersProvider import io.github.wulkanowy.utils.security.decrypt import io.github.wulkanowy.utils.security.encrypt @@ -23,7 +24,8 @@ class StudentRepository @Inject constructor( private val dispatchers: DispatchersProvider, private val studentDb: StudentDao, private val semesterDb: SemesterDao, - private val sdk: Sdk + private val sdk: Sdk, + private val appInfo: AppInfo ) { suspend fun isStudentSaved() = getSavedStudents(false).isNotEmpty() @@ -35,7 +37,8 @@ class StudentRepository @Inject constructor( symbol: String, token: String ): List = - sdk.getStudentsFromMobileApi(token, pin, symbol, "").mapToEntities() + sdk.getStudentsFromMobileApi(token, pin, symbol, "") + .mapToEntities(colors = appInfo.defaultColorsForAvatar) suspend fun getStudentsScrapper( email: String, @@ -44,7 +47,7 @@ class StudentRepository @Inject constructor( symbol: String ): List = sdk.getStudentsFromScrapper(email, password, scrapperBaseUrl, symbol) - .mapToEntities(password) + .mapToEntities(password, appInfo.defaultColorsForAvatar) suspend fun getStudentsHybrid( email: String, @@ -52,46 +55,58 @@ class StudentRepository @Inject constructor( scrapperBaseUrl: String, symbol: String ): List = - sdk.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol).mapToEntities(password) + sdk.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol) + .mapToEntities(password, appInfo.defaultColorsForAvatar) suspend fun getSavedStudents(decryptPass: Boolean = true) = - withContext(dispatchers.backgroundThread) { - studentDb.loadStudentsWithSemesters().map { + studentDb.loadStudentsWithSemesters() + .map { it.apply { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { - student.password = decrypt(student.password) + student.password = withContext(dispatchers.backgroundThread) { + decrypt(student.password) + } } } } - } - suspend fun getStudentById(id: Int) = withContext(dispatchers.backgroundThread) { - studentDb.loadById(id)?.apply { - if (Sdk.Mode.valueOf(loginMode) != Sdk.Mode.API) { - password = decrypt(password) + suspend fun getStudentById(id: Long, decryptPass: Boolean = true): Student { + val student = studentDb.loadById(id) ?: throw NoCurrentStudentException() + + if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { + student.password = withContext(dispatchers.backgroundThread) { + decrypt(student.password) } } - } ?: throw NoCurrentStudentException() + return student + } - suspend fun getCurrentStudent(decryptPass: Boolean = true) = - withContext(dispatchers.backgroundThread) { - studentDb.loadCurrent()?.apply { - if (decryptPass && Sdk.Mode.valueOf(loginMode) != Sdk.Mode.API) { - password = decrypt(password) - } + suspend fun getCurrentStudent(decryptPass: Boolean = true): Student { + val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException() + + if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { + student.password = withContext(dispatchers.backgroundThread) { + decrypt(student.password) } - } ?: throw NoCurrentStudentException() + } + return student + } suspend fun saveStudents(studentsWithSemesters: List): List { - semesterDb.insertSemesters(studentsWithSemesters.flatMap { it.semesters }) + val semesters = studentsWithSemesters.flatMap { it.semesters } + val students = studentsWithSemesters.map { it.student } + .map { + it.apply { + if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.API) { + password = withContext(dispatchers.backgroundThread) { + encrypt(password, context) + } + } + } + } - return withContext(dispatchers.backgroundThread) { - studentDb.insertAll(studentsWithSemesters.map { it.student }.map { - if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.API) { - it.copy(password = encrypt(it.password, context)) - } else it - }) - } + semesterDb.insertSemesters(semesters) + return studentDb.insertAll(students) } suspend fun switchStudent(studentWithSemesters: StudentWithSemesters) { @@ -103,5 +118,6 @@ class StudentRepository @Inject constructor( suspend fun logoutStudent(student: Student) = studentDb.delete(student) - suspend fun updateStudentNick(studentNick: StudentNick) = studentDb.update(studentNick) + suspend fun updateStudentNickAndAvatar(studentNickAndAvatar: StudentNickAndAvatar) = + studentDb.update(studentNickAndAvatar) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountAdapter.kt index 342fd2d4b..227f06613 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountAdapter.kt @@ -1,16 +1,17 @@ package io.github.wulkanowy.ui.modules.account import android.annotation.SuppressLint -import android.graphics.PorterDuff import android.view.LayoutInflater import android.view.View.GONE import android.view.View.VISIBLE import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.databinding.HeaderAccountBinding import io.github.wulkanowy.databinding.ItemAccountBinding +import io.github.wulkanowy.utils.createNameInitialsDrawable import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.nickOrName import javax.inject.Inject @@ -72,9 +73,13 @@ class AccountAdapter @Inject constructor() : RecyclerView.Adapter(R.layout.fragment_a override var subtitleString = "" - override val isViewEmpty get() = accountAdapter.items.isEmpty() - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) } + @Suppress("UNCHECKED_CAST") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + binding = FragmentAccountBinding.bind(view) presenter.onAttachView(this) } override fun initView() { - binding.accountErrorRetry.setOnClickListener { presenter.onRetry() } - binding.accountErrorDetails.setOnClickListener { presenter.onDetailsClick() } - binding.accountRecycler.apply { layoutManager = LinearLayoutManager(context) adapter = accountAdapter @@ -60,9 +57,7 @@ class AccountFragment : BaseFragment(R.layout.fragment_a accountAdapter.onClickListener = presenter::onItemSelected - with(binding) { - accountAdd.setOnClickListener { presenter.onAddSelected() } - } + binding.accountAdd.setOnClickListener { presenter.onAddSelected() } } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -84,28 +79,7 @@ class AccountFragment : BaseFragment(R.layout.fragment_a override fun openAccountDetailsView(studentWithSemesters: StudentWithSemesters) { (activity as? MainActivity)?.pushView( - AccountDetailsFragment.newInstance( - studentWithSemesters - ) + AccountDetailsFragment.newInstance(studentWithSemesters) ) } - - override fun showErrorView(show: Boolean) { - binding.accountError.visibility = if (show) View.VISIBLE else View.GONE - } - - override fun setErrorDetails(message: String) { - binding.accountErrorMessage.text = message - } - - override fun showProgress(show: Boolean) { - binding.accountProgress.visibility = if (show) View.VISIBLE else View.GONE - } - - override fun showContent(show: Boolean) { - with(binding) { - accountRecycler.visibility = if (show) View.VISIBLE else View.GONE - accountAdd.visibility = if (show) View.VISIBLE else View.GONE - } - } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountPresenter.kt index 366793b6d..8d1651395 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountPresenter.kt @@ -5,7 +5,6 @@ import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler -import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.flowWithResource import kotlinx.coroutines.flow.onEach import timber.log.Timber @@ -16,28 +15,13 @@ class AccountPresenter @Inject constructor( studentRepository: StudentRepository, ) : BasePresenter(errorHandler, studentRepository) { - private lateinit var lastError: Throwable - override fun onAttachView(view: AccountView) { super.onAttachView(view) view.initView() Timber.i("Account view was initialized") - errorHandler.showErrorMessage = ::showErrorViewOnError loadData() } - fun onRetry() { - view?.run { - showErrorView(false) - showProgress(true) - } - loadData() - } - - fun onDetailsClick() { - view?.showErrorDetailsDialog(lastError) - } - fun onAddSelected() { Timber.i("Select add account") view?.openLoginView() @@ -47,6 +31,24 @@ class AccountPresenter @Inject constructor( view?.openAccountDetailsView(studentWithSemesters) } + private fun loadData() { + flowWithResource { studentRepository.getSavedStudents(false) } + .onEach { + when (it.status) { + Status.LOADING -> Timber.i("Loading account data started") + Status.SUCCESS -> { + Timber.i("Loading account result: Success") + view?.updateData(createAccountItems(it.data!!)) + } + Status.ERROR -> { + Timber.i("Loading account result: An exception occurred") + errorHandler.dispatch(it.error!!) + } + } + } + .launch("load") + } + private fun createAccountItems(items: List): List> { return items.groupBy { Account("${it.student.userName} (${it.student.email})", it.student.isParent) @@ -60,45 +62,4 @@ class AccountPresenter @Inject constructor( } .flatten() } - - private fun loadData() { - flowWithResource { studentRepository.getSavedStudents(false) } - .onEach { - when (it.status) { - Status.LOADING -> { - Timber.i("Loading account data started") - view?.run { - showProgress(true) - showContent(false) - } - } - Status.SUCCESS -> { - Timber.i("Loading account result: Success") - view?.updateData(createAccountItems(it.data!!)) - view?.run { - showContent(true) - showErrorView(false) - } - } - Status.ERROR -> { - Timber.i("Loading account result: An exception occurred") - errorHandler.dispatch(it.error!!) - } - } - } - .afterLoading { view?.showProgress(false) } - .launch() - } - - private fun showErrorViewOnError(message: String, error: Throwable) { - view?.run { - if (isViewEmpty) { - lastError = error - setErrorDetails(message) - showErrorView(true) - showContent(false) - showProgress(false) - } else showError(message, error) - } - } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountView.kt index 3453909d1..d7deefafd 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountView.kt @@ -5,8 +5,6 @@ import io.github.wulkanowy.ui.base.BaseView interface AccountView : BaseView { - val isViewEmpty: Boolean - fun initView() fun updateData(data: List>) @@ -14,13 +12,4 @@ interface AccountView : BaseView { fun openLoginView() fun openAccountDetailsView(studentWithSemesters: StudentWithSemesters) - - fun showErrorView(show: Boolean) - - fun setErrorDetails(message: String) - - fun showProgress(show: Boolean) - - fun showContent(show: Boolean) } - diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt index f4b2c833d..28b6bd2ed 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt @@ -7,6 +7,7 @@ import android.view.MenuItem import android.view.View import androidx.appcompat.app.AlertDialog import androidx.core.view.get +import androidx.core.view.isVisible import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Student @@ -18,6 +19,7 @@ import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoFragment import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView +import io.github.wulkanowy.utils.createNameInitialsDrawable import io.github.wulkanowy.utils.nickOrName import javax.inject.Inject @@ -88,8 +90,15 @@ class AccountDetailsFragment : override fun showAccountData(student: Student) { with(binding) { + accountDetailsCheck.isVisible = student.isCurrent accountDetailsName.text = student.nickOrName accountDetailsSchool.text = student.schoolName + accountDetailsAvatar.setImageDrawable( + requireContext().createNameInitialsDrawable( + student.nickOrName, + student.avatarColor + ) + ) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsPresenter.kt index 7b93d3d87..cc53c9b63 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsPresenter.kt @@ -21,7 +21,7 @@ class AccountDetailsPresenter @Inject constructor( private val syncManager: SyncManager ) : BasePresenter(errorHandler, studentRepository) { - private lateinit var studentWithSemesters: StudentWithSemesters + private var studentWithSemesters: StudentWithSemesters? = null private lateinit var lastError: Throwable @@ -69,10 +69,10 @@ class AccountDetailsPresenter @Inject constructor( } Status.SUCCESS -> { Timber.i("Loading account details view result: Success") - studentWithSemesters = it.data!! + studentWithSemesters = it.data view?.run { - showAccountData(studentWithSemesters.student) - enableSelectStudentButton(!studentWithSemesters.student.isCurrent) + showAccountData(studentWithSemesters!!.student) + enableSelectStudentButton(!studentWithSemesters!!.student.isCurrent) showContent(true) showErrorView(false) } @@ -88,17 +88,23 @@ class AccountDetailsPresenter @Inject constructor( } fun onAccountEditSelected() { - view?.showAccountEditDetailsDialog(studentWithSemesters.student) + studentWithSemesters?.let { + view?.showAccountEditDetailsDialog(it.student) + } } fun onStudentInfoSelected(infoType: StudentInfoView.Type) { - view?.openStudentInfoView(infoType, studentWithSemesters) + studentWithSemesters?.let { + view?.openStudentInfoView(infoType, it) + } } fun onStudentSelect() { - Timber.i("Select student ${studentWithSemesters.student.id}") + if (studentWithSemesters == null) return - flowWithResource { studentRepository.switchStudent(studentWithSemesters) } + Timber.i("Select student ${studentWithSemesters!!.student.id}") + + flowWithResource { studentRepository.switchStudent(studentWithSemesters!!) } .onEach { when (it.status) { Status.LOADING -> Timber.i("Attempt to change a student") @@ -122,8 +128,10 @@ class AccountDetailsPresenter @Inject constructor( } fun onLogoutConfirm() { + if (studentWithSemesters == null) return + flowWithResource { - val studentToLogout = studentWithSemesters.student + val studentToLogout = studentWithSemesters!!.student studentRepository.logoutStudent(studentToLogout) val students = studentRepository.getSavedStudents(false) @@ -143,7 +151,7 @@ class AccountDetailsPresenter @Inject constructor( syncManager.stopSyncWorker() openClearLoginView() } - studentWithSemesters.student.isCurrent -> { + studentWithSemesters!!.student.isCurrent -> { Timber.i("Logout result: Logout student and switch to another") recreateMainView() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditColorAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditColorAdapter.kt new file mode 100644 index 000000000..ab6eec417 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditColorAdapter.kt @@ -0,0 +1,90 @@ +package io.github.wulkanowy.ui.modules.account.accountedit + +import android.annotation.SuppressLint +import android.content.res.ColorStateList +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.RippleDrawable +import android.graphics.drawable.StateListDrawable +import android.os.Build +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.databinding.ItemAccountEditColorBinding +import javax.inject.Inject + +class AccountEditColorAdapter @Inject constructor() : + RecyclerView.Adapter() { + + var items = listOf() + + var selectedColor = 0 + + override fun getItemCount() = items.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + ItemAccountEditColorBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + + @SuppressLint("RestrictedApi", "NewApi") + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = items[position] + + with(holder.binding) { + accountEditItemColor.setImageDrawable(GradientDrawable().apply { + shape = GradientDrawable.OVAL + setColor(item) + }) + + accountEditItemColorContainer.foreground = item.createForegroundDrawable() + accountEditCheck.isVisible = selectedColor == item + + root.setOnClickListener { + val oldSelectedPosition = items.indexOf(selectedColor) + selectedColor = item + + notifyItemChanged(oldSelectedPosition) + notifyItemChanged(position) + } + } + } + + private fun Int.createForegroundDrawable(): Drawable = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + val mask = GradientDrawable().apply { + shape = GradientDrawable.OVAL + setColor(Color.BLACK) + } + RippleDrawable(ColorStateList.valueOf(this.rippleColor), null, mask) + } else { + val foreground = StateListDrawable().apply { + alpha = 80 + setEnterFadeDuration(250) + setExitFadeDuration(250) + } + + val mask = GradientDrawable().apply { + shape = GradientDrawable.OVAL + setColor(this@createForegroundDrawable.rippleColor) + } + + foreground.apply { + addState(intArrayOf(android.R.attr.state_pressed), mask) + addState(intArrayOf(), ColorDrawable(Color.TRANSPARENT)) + } + } + + private inline val Int.rippleColor: Int + get() { + val hsv = FloatArray(3) + Color.colorToHSV(this, hsv) + hsv[2] = hsv[2] * 0.5f + return Color.HSVToColor(hsv) + } + + class ViewHolder(val binding: ItemAccountEditColorBinding) : + RecyclerView.ViewHolder(binding.root) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditDialog.kt index 89f23e29f..21a7a492d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditDialog.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.recyclerview.widget.GridLayoutManager import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.databinding.DialogAccountEditBinding @@ -16,6 +17,9 @@ class AccountEditDialog : BaseDialogFragment(), Accoun @Inject lateinit var presenter: AccountEditPresenter + @Inject + lateinit var accountEditColorAdapter: AccountEditColorAdapter + companion object { private const val ARGUMENT_KEY = "student_with_semesters" @@ -48,8 +52,30 @@ class AccountEditDialog : BaseDialogFragment(), Accoun with(binding) { accountEditDetailsCancel.setOnClickListener { dismiss() } accountEditDetailsSave.setOnClickListener { - presenter.changeStudentNick(binding.accountEditDetailsNickText.text.toString()) + presenter.changeStudentNickAndAvatar( + binding.accountEditDetailsNickText.text.toString(), + accountEditColorAdapter.selectedColor + ) } + + with(binding.accountEditColors) { + layoutManager = GridLayoutManager(context, 4) + adapter = accountEditColorAdapter + } + } + } + + override fun updateSelectedColorData(color: Int) { + with(accountEditColorAdapter) { + selectedColor = color + notifyDataSetChanged() + } + } + + override fun updateColorsData(colors: List) { + with(accountEditColorAdapter) { + items = colors + notifyDataSetChanged() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditPresenter.kt index 7830605c6..62dd70ab4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditPresenter.kt @@ -2,10 +2,11 @@ package io.github.wulkanowy.ui.modules.account.accountedit import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.db.entities.StudentNick +import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler +import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.flowWithResource import kotlinx.coroutines.flow.onEach @@ -13,12 +14,15 @@ import timber.log.Timber import javax.inject.Inject class AccountEditPresenter @Inject constructor( + private val appInfo: AppInfo, errorHandler: ErrorHandler, studentRepository: StudentRepository ) : BasePresenter(errorHandler, studentRepository) { lateinit var student: Student + private val colors = appInfo.defaultColorsForAvatar.map { it.toInt() } + fun onAttachView(view: AccountEditView, student: Student) { super.onAttachView(view) this.student = student @@ -28,27 +32,49 @@ class AccountEditPresenter @Inject constructor( showCurrentNick(student.nick.trim()) } Timber.i("Account edit dialog view was initialized") + loadData() + + view.updateColorsData(colors) } - fun changeStudentNick(nick: String) { + private fun loadData() { + flowWithResource { + studentRepository.getStudentById(student.id, false).avatarColor + }.onEach { resource -> + when (resource.status) { + Status.LOADING -> Timber.i("Attempt to load student") + Status.SUCCESS -> { + view?.updateSelectedColorData(resource.data?.toInt()!!) + Timber.i("Attempt to load student: Success") + } + Status.ERROR -> { + Timber.i("Attempt to load student: An exception occurred") + errorHandler.dispatch(resource.error!!) + } + } + }.launch("load_data") + } + + fun changeStudentNickAndAvatar(nick: String, avatarColor: Int) { flowWithResource { val studentNick = - StudentNick(nick = nick.trim()).apply { id = student.id } - studentRepository.updateStudentNick(studentNick) + StudentNickAndAvatar(nick = nick.trim(), avatarColor = avatarColor.toLong()) + .apply { id = student.id } + studentRepository.updateStudentNickAndAvatar(studentNick) }.onEach { when (it.status) { - Status.LOADING -> Timber.i("Attempt to change a student nick") + Status.LOADING -> Timber.i("Attempt to change a student nick and avatar") Status.SUCCESS -> { - Timber.i("Change a student nick result: Success") + Timber.i("Change a student nick and avatar result: Success") view?.recreateMainView() } Status.ERROR -> { - Timber.i("Change a student result: An exception occurred") + Timber.i("Change a student nick and avatar result: An exception occurred") errorHandler.dispatch(it.error!!) } } } .afterLoading { view?.popView() } - .launch() + .launch("update_student") } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditView.kt index b25eec6c8..517492de1 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditView.kt @@ -11,4 +11,8 @@ interface AccountEditView : BaseView { fun recreateMainView() fun showCurrentNick(nick: String) + + fun updateSelectedColorData(color: Int) + + fun updateColorsData(colors: List) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickDialog.kt index cb64a8fd3..4279102e1 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickDialog.kt @@ -6,6 +6,7 @@ import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.databinding.DialogAccountQuickBinding import io.github.wulkanowy.ui.base.BaseDialogFragment import io.github.wulkanowy.ui.modules.account.AccountAdapter @@ -24,7 +25,15 @@ class AccountQuickDialog : BaseDialogFragment(), Acco lateinit var presenter: AccountQuickPresenter companion object { - fun newInstance() = AccountQuickDialog() + + private const val STUDENTS_ARGUMENT_KEY = "students" + + fun newInstance(studentsWithSemesters: List) = + AccountQuickDialog().apply { + arguments = Bundle().apply { + putSerializable(STUDENTS_ARGUMENT_KEY, studentsWithSemesters.toTypedArray()) + } + } } override fun onCreate(savedInstanceState: Bundle?) { @@ -38,8 +47,12 @@ class AccountQuickDialog : BaseDialogFragment(), Acco savedInstanceState: Bundle? ) = DialogAccountQuickBinding.inflate(inflater).apply { binding = this }.root + @Suppress("UNCHECKED_CAST") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - presenter.onAttachView(this) + val studentsWithSemesters = + (requireArguments()[STUDENTS_ARGUMENT_KEY] as Array).toList() + + presenter.onAttachView(this, studentsWithSemesters) } override fun initView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickPresenter.kt index 43cc8bc77..39d8fce24 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickPresenter.kt @@ -17,11 +17,15 @@ class AccountQuickPresenter @Inject constructor( studentRepository: StudentRepository ) : BasePresenter(errorHandler, studentRepository) { - override fun onAttachView(view: AccountQuickView) { + private lateinit var studentsWithSemesters: List + + fun onAttachView(view: AccountQuickView, studentsWithSemesters: List) { super.onAttachView(view) + this.studentsWithSemesters = studentsWithSemesters + view.initView() Timber.i("Account quick dialog view was initialized") - loadData() + view.updateData(createAccountItems(studentsWithSemesters)) } fun onManagerSelected() { @@ -57,22 +61,6 @@ class AccountQuickPresenter @Inject constructor( .launch("switch") } - private fun loadData() { - flowWithResource { studentRepository.getSavedStudents(false) }.onEach { - when (it.status) { - Status.LOADING -> Timber.i("Loading account data started") - Status.SUCCESS -> { - Timber.i("Loading account result: Success") - view?.updateData(createAccountItems(it.data!!)) - } - Status.ERROR -> { - Timber.i("Loading account result: An exception occurred") - errorHandler.dispatch(it.error!!) - } - } - }.launch() - } - private fun createAccountItems(items: List) = items.map { AccountItem(it, AccountItem.ViewType.ITEM) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt index 5585ac27c..efa83f443 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt @@ -28,6 +28,8 @@ import com.ncapdevi.fragnav.FragNavController import com.ncapdevi.fragnav.FragNavController.Companion.HIDE import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.databinding.ActivityMainBinding import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog @@ -43,8 +45,10 @@ import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.UpdateHelper +import io.github.wulkanowy.utils.createNameInitialsDrawable import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.getThemeAttrColor +import io.github.wulkanowy.utils.nickOrName import io.github.wulkanowy.utils.safelyPopFragments import io.github.wulkanowy.utils.setOnViewChangeListener import timber.log.Timber @@ -65,6 +69,8 @@ class MainActivity : BaseActivity(), MainVie @Inject lateinit var appInfo: AppInfo + private var accountMenu: MenuItem? = null + private val overlayProvider by lazy { ElevationOverlayProvider(this) } private val navController = @@ -192,6 +198,9 @@ class MainActivity : BaseActivity(), MainVie override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.action_menu_main, menu) + accountMenu = menu?.findItem(R.id.mainMenuAccount) + + presenter.onActionMenuCreated() return true } @@ -288,8 +297,8 @@ class MainActivity : BaseActivity(), MainVie supportActionBar?.setDisplayHomeAsUpEnabled(show) } - override fun showAccountPicker() { - navController.showDialogFragment(AccountQuickDialog.newInstance()) + override fun showAccountPicker(studentWithSemesters: List) { + navController.showDialogFragment(AccountQuickDialog.newInstance(studentWithSemesters)) } override fun showActionBarElevation(show: Boolean) { @@ -323,6 +332,13 @@ class MainActivity : BaseActivity(), MainVie presenter.onBackPressed { super.onBackPressed() } } + override fun showStudentAvatar(student: Student) { + accountMenu?.run { + icon = createNameInitialsDrawable(student.nickOrName, student.avatarColor, 0.44f) + title = getString(R.string.main_account_picker) + } + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) navController.onSaveInstanceState(outState) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt index 59937b332..857779838 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt @@ -1,5 +1,7 @@ package io.github.wulkanowy.ui.modules.main +import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.services.sync.SyncManager @@ -9,6 +11,8 @@ import io.github.wulkanowy.ui.modules.main.MainView.Section.GRADE import io.github.wulkanowy.ui.modules.main.MainView.Section.MESSAGE import io.github.wulkanowy.ui.modules.main.MainView.Section.SCHOOL import io.github.wulkanowy.utils.AnalyticsHelper +import io.github.wulkanowy.utils.flowWithResource +import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -17,9 +21,11 @@ class MainPresenter @Inject constructor( studentRepository: StudentRepository, private val prefRepository: PreferencesRepository, private val syncManager: SyncManager, - private val analytics: AnalyticsHelper + private val analytics: AnalyticsHelper, ) : BasePresenter(errorHandler, studentRepository) { + var studentsWitSemesters: List? = null + fun onAttachView(view: MainView, initMenu: MainView.Section?) { super.onAttachView(view) view.apply { @@ -35,6 +41,28 @@ class MainPresenter @Inject constructor( analytics.logEvent("app_open", "destination" to initMenu?.name) } + fun onActionMenuCreated() { + if (!studentsWitSemesters.isNullOrEmpty()) { + showCurrentStudentAvatar() + return + } + + flowWithResource { studentRepository.getSavedStudents(false) } + .onEach { resource -> + when (resource.status) { + Status.LOADING -> Timber.i("Loading student avatar data started") + Status.SUCCESS -> { + studentsWitSemesters = resource.data + showCurrentStudentAvatar() + } + Status.ERROR -> { + Timber.i("Loading student avatar result: An exception occurred") + errorHandler.dispatch(resource.error!!) + } + } + }.launch("avatar") + } + fun onViewChange(section: MainView.Section?) { view?.apply { showActionBarElevation(section != GRADE && section != MESSAGE && section != SCHOOL) @@ -48,8 +76,10 @@ class MainPresenter @Inject constructor( } fun onAccountManagerSelected(): Boolean { + if (studentsWitSemesters.isNullOrEmpty()) return true + Timber.i("Select account manager") - view?.showAccountPicker() + view?.showAccountPicker(studentsWitSemesters!!) return true } @@ -81,6 +111,13 @@ class MainPresenter @Inject constructor( } == true } + private fun showCurrentStudentAvatar() { + val currentStudent = + studentsWitSemesters!!.single { it.student.isCurrent }.student + + view?.showStudentAvatar(currentStudent) + } + private fun getProperViewIndexes(initMenu: MainView.Section?): Pair { return when (initMenu?.id) { in 0..3 -> initMenu!!.id to -1 diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt index 7f4098147..a4b7d094b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt @@ -1,5 +1,7 @@ package io.github.wulkanowy.ui.modules.main +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.ui.base.BaseView interface MainView : BaseView { @@ -22,7 +24,7 @@ interface MainView : BaseView { fun showHomeArrow(show: Boolean) - fun showAccountPicker() + fun showAccountPicker(studentWithSemesters: List) fun showActionBarElevation(show: Boolean) @@ -36,6 +38,8 @@ interface MainView : BaseView { fun popView(depth: Int = 1) + fun showStudentAvatar(student: Student) + interface MainChildView { fun onFragmentReselected() diff --git a/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt b/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt index 82671a7f4..a3961aed8 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt @@ -35,8 +35,8 @@ open class AppInfo @Inject constructor() { open val systemLanguage: String get() = Resources.getSystem().configuration.locale.language - open val defaultColorsForAvatar = listOf( - 0xe57373, 0xf06292, 0xba68c8, 0x9575cd, 0x7986cb, 0x64b5f6, 0x4fc3f7, 0x4dd0e1, 0x4db6ac, - 0x81c784, 0xaed581, 0xff8a65, 0xd4e157, 0xffd54f, 0xffb74d, 0xa1887f, 0x90a4ae - ) + val defaultColorsForAvatar = listOf( + 0xd32f2f, 0xE64A19, 0xFFA000, 0xAFB42B, 0x689F38, 0x388E3C, 0x00796B, 0x0097A7, + 0x1976D2, 0x3647b5, 0x6236c9, 0x9225c1, 0xC2185B, 0x616161, 0x455A64, 0x7a5348 + ).map { (it and 0x00ffffff or (255 shl 24)).toLong() } } diff --git a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt index cf715e657..92c0ddfc5 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt @@ -1,8 +1,15 @@ package io.github.wulkanowy.utils +import android.annotation.SuppressLint import android.content.Context import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.Typeface import android.net.Uri +import android.text.TextPaint import android.util.DisplayMetrics.DENSITY_DEFAULT import androidx.annotation.AttrRes import androidx.annotation.ColorInt @@ -10,6 +17,9 @@ import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.core.content.ContextCompat import androidx.core.graphics.ColorUtils +import androidx.core.graphics.applyCanvas +import androidx.core.graphics.drawable.RoundedBitmapDrawable +import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import io.github.wulkanowy.BuildConfig.APPLICATION_ID @ColorInt @@ -30,7 +40,8 @@ fun Context.getThemeAttrColor(@AttrRes colorAttr: Int, alpha: Int): Int { @ColorInt fun Context.getCompatColor(@ColorRes colorRes: Int) = ContextCompat.getColor(this, colorRes) -fun Context.getCompatDrawable(@DrawableRes drawableRes: Int) = ContextCompat.getDrawable(this, drawableRes) +fun Context.getCompatDrawable(@DrawableRes drawableRes: Int) = + ContextCompat.getDrawable(this, drawableRes) fun Context.openInternetBrowser(uri: String, onActivityNotFound: (uri: String) -> Unit) { Intent.parseUri(uri, 0).let { @@ -45,7 +56,13 @@ fun Context.openAppInMarket(onActivityNotFound: (uri: String) -> Unit) { } } -fun Context.openEmailClient(chooserTitle: String, email: String, subject: String, body: String, onActivityNotFound: () -> Unit = {}) { +fun Context.openEmailClient( + chooserTitle: String, + email: String, + subject: String, + body: String, + onActivityNotFound: () -> Unit = {} +) { val intent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:")).apply { putExtra(Intent.EXTRA_EMAIL, arrayOf(email)) putExtra(Intent.EXTRA_SUBJECT, subject) @@ -85,3 +102,39 @@ fun Context.shareText(text: String, subject: String?) { } fun Context.dpToPx(dp: Float) = dp * resources.displayMetrics.densityDpi / DENSITY_DEFAULT + +@SuppressLint("DefaultLocale") +fun Context.createNameInitialsDrawable( + text: String, + backgroundColor: Long, + scaleFactory: Float = 1f +): RoundedBitmapDrawable { + val words = text.split(" ") + val firstCharFirstWord = words.getOrNull(0)?.firstOrNull() ?: "" + val firstCharSecondWord = words.getOrNull(1)?.firstOrNull() ?: "" + + val initials = "$firstCharFirstWord$firstCharSecondWord".toUpperCase() + + val bounds = Rect() + val dimension = this.dpToPx(64f * scaleFactory).toInt() + val textPaint = TextPaint().apply { + typeface = Typeface.SANS_SERIF + color = Color.WHITE + textAlign = Paint.Align.CENTER + isAntiAlias = true + textSize = this@createNameInitialsDrawable.dpToPx(30f * scaleFactory) + getTextBounds(initials, 0, initials.length, bounds) + } + + val xCoordinate = (dimension / 2).toFloat() + val yCoordinate = (dimension / 2 + (bounds.bottom - bounds.top) / 2).toFloat() + + val bitmap = Bitmap.createBitmap(dimension, dimension, Bitmap.Config.ARGB_8888) + .applyCanvas { + drawColor(backgroundColor.toInt()) + drawText(initials, 0, initials.length, xCoordinate, yCoordinate, textPaint) + } + + return RoundedBitmapDrawableFactory.create(this.resources, bitmap) + .apply { isCircular = true } +} diff --git a/app/src/main/res/drawable/ic_all_round_check.xml b/app/src/main/res/drawable/ic_all_round_check.xml new file mode 100644 index 000000000..aecaf234a --- /dev/null +++ b/app/src/main/res/drawable/ic_all_round_check.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/dialog_account_edit.xml b/app/src/main/res/layout/dialog_account_edit.xml index b65f85ac3..3177efba9 100644 --- a/app/src/main/res/layout/dialog_account_edit.xml +++ b/app/src/main/res/layout/dialog_account_edit.xml @@ -1,87 +1,123 @@ - - - - + android:layout_height="wrap_content"> - + - + - - + - + - - + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_account_quick.xml b/app/src/main/res/layout/dialog_account_quick.xml index da31d31d3..4095c91ae 100644 --- a/app/src/main/res/layout/dialog_account_quick.xml +++ b/app/src/main/res/layout/dialog_account_quick.xml @@ -2,7 +2,7 @@ - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_account_details.xml b/app/src/main/res/layout/fragment_account_details.xml index 1a0de147e..af9564b5e 100644 --- a/app/src/main/res/layout/fragment_account_details.xml +++ b/app/src/main/res/layout/fragment_account_details.xml @@ -21,9 +21,9 @@ android:layout_height="match_parent" android:gravity="center" android:orientation="vertical" - android:visibility="invisible" + android:visibility="gone" tools:ignore="UseCompoundDrawables" - tools:visibility="visible"> + tools:visibility="gone"> + android:layout_height="match_parent" + android:visibility="gone" + tools:visibility="visible"> + tools:ignore="ContentDescription" + tools:src="@tools:sample/avatars" /> + + + tools:src="@tools:sample/avatars" /> + + + + + + + diff --git a/app/src/main/res/layout/item_grade_statistics_pie.xml b/app/src/main/res/layout/item_grade_statistics_pie.xml index 15992a37e..2ea91ecf9 100644 --- a/app/src/main/res/layout/item_grade_statistics_pie.xml +++ b/app/src/main/res/layout/item_grade_statistics_pie.xml @@ -23,5 +23,4 @@ android:layout_margin="10dp" android:background="?android:windowBackground" tools:context=".ui.modules.grade.statistics.GradeStatisticsAdapter" /> - diff --git a/app/src/main/res/menu/action_menu_main.xml b/app/src/main/res/menu/action_menu_main.xml index 72bea25a2..219059391 100644 --- a/app/src/main/res/menu/action_menu_main.xml +++ b/app/src/main/res/menu/action_menu_main.xml @@ -1,11 +1,11 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + + app:showAsAction="always" + tools:ignore="MenuTitle" /> diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt index 415f6e0af..402b22723 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt @@ -5,6 +5,7 @@ import io.github.wulkanowy.data.db.dao.SemesterDao import io.github.wulkanowy.data.db.dao.StudentDao import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.Student +import io.github.wulkanowy.utils.AppInfo import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.impl.annotations.MockK @@ -30,7 +31,14 @@ class StudentTest { @Before fun initApi() { MockKAnnotations.init(this) - studentRepository = StudentRepository(mockk(), TestDispatchersProvider(), studentDb, semesterDb, mockSdk) + studentRepository = StudentRepository( + mockk(), + TestDispatchersProvider(), + studentDb, + semesterDb, + mockSdk, + AppInfo() + ) } @Test diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 25d326531..8cf6eb5ad 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists