diff --git a/.idea/dictionaries/Kuba.xml b/.idea/dictionaries/Kuba.xml
index 592a5d5e..aba38775 100644
--- a/.idea/dictionaries/Kuba.xml
+++ b/.idea/dictionaries/Kuba.xml
@@ -13,6 +13,7 @@
synergia
szczodrzyński
szkolny
+ usos
\ No newline at end of file
diff --git a/app/schemas/pl.szczodrzynski.edziennik.data.db.AppDb/98.json b/app/schemas/pl.szczodrzynski.edziennik.data.db.AppDb/98.json
new file mode 100644
index 00000000..1a291aef
--- /dev/null
+++ b/app/schemas/pl.szczodrzynski.edziennik.data.db.AppDb/98.json
@@ -0,0 +1,2314 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 98,
+ "identityHash": "2612ebba9802eedc7ebc69724606a23c",
+ "entities": [
+ {
+ "tableName": "grades",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `gradeId` INTEGER NOT NULL, `gradeName` TEXT NOT NULL, `gradeType` INTEGER NOT NULL, `gradeValue` REAL NOT NULL, `gradeWeight` REAL NOT NULL, `gradeColor` INTEGER NOT NULL, `gradeCategory` TEXT, `gradeDescription` TEXT, `gradeComment` TEXT, `gradeSemester` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `gradeValueMax` REAL, `gradeClassAverage` REAL, `gradeParentId` INTEGER, `gradeIsImprovement` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `gradeId`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "gradeId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "gradeName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "gradeType",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "gradeValue",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "weight",
+ "columnName": "gradeWeight",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "color",
+ "columnName": "gradeColor",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "category",
+ "columnName": "gradeCategory",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "gradeDescription",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "comment",
+ "columnName": "gradeComment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "semester",
+ "columnName": "gradeSemester",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "teacherId",
+ "columnName": "teacherId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subjectId",
+ "columnName": "subjectId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "addedDate",
+ "columnName": "addedDate",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "valueMax",
+ "columnName": "gradeValueMax",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "classAverage",
+ "columnName": "gradeClassAverage",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "parentId",
+ "columnName": "gradeParentId",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isImprovement",
+ "columnName": "gradeIsImprovement",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "keep",
+ "columnName": "keep",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profileId",
+ "gradeId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_grades_profileId",
+ "unique": false,
+ "columnNames": [
+ "profileId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_grades_profileId` ON `${TABLE_NAME}` (`profileId`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "teachers",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `teacherLoginId` TEXT, `teacherName` TEXT, `teacherSurname` TEXT, `teacherType` INTEGER NOT NULL, `teacherTypeDescription` TEXT, `teacherSubjects` TEXT NOT NULL, PRIMARY KEY(`profileId`, `teacherId`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "teacherId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "loginId",
+ "columnName": "teacherLoginId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "teacherName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "surname",
+ "columnName": "teacherSurname",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "teacherType",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "typeDescription",
+ "columnName": "teacherTypeDescription",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "subjects",
+ "columnName": "teacherSubjects",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profileId",
+ "teacherId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "teacherAbsence",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `teacherAbsenceId` INTEGER NOT NULL, `teacherAbsenceType` INTEGER NOT NULL, `teacherAbsenceName` TEXT, `teacherAbsenceDateFrom` TEXT NOT NULL, `teacherAbsenceDateTo` TEXT NOT NULL, `teacherAbsenceTimeFrom` TEXT, `teacherAbsenceTimeTo` TEXT, `teacherId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `teacherAbsenceId`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "teacherAbsenceId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "teacherAbsenceType",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "teacherAbsenceName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateFrom",
+ "columnName": "teacherAbsenceDateFrom",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "dateTo",
+ "columnName": "teacherAbsenceDateTo",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timeFrom",
+ "columnName": "teacherAbsenceTimeFrom",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "timeTo",
+ "columnName": "teacherAbsenceTimeTo",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "teacherId",
+ "columnName": "teacherId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "addedDate",
+ "columnName": "addedDate",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "keep",
+ "columnName": "keep",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profileId",
+ "teacherAbsenceId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_teacherAbsence_profileId",
+ "unique": false,
+ "columnNames": [
+ "profileId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_teacherAbsence_profileId` ON `${TABLE_NAME}` (`profileId`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "teacherAbsenceTypes",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `teacherAbsenceTypeId` INTEGER NOT NULL, `teacherAbsenceTypeName` TEXT NOT NULL, PRIMARY KEY(`profileId`, `teacherAbsenceTypeId`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "teacherAbsenceTypeId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "teacherAbsenceTypeName",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profileId",
+ "teacherAbsenceTypeId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "subjects",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `subjectLongName` TEXT, `subjectShortName` TEXT, `subjectColor` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `subjectId`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "subjectId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "longName",
+ "columnName": "subjectLongName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "shortName",
+ "columnName": "subjectShortName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "color",
+ "columnName": "subjectColor",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profileId",
+ "subjectId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "notices",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `noticeId` INTEGER NOT NULL, `noticeType` INTEGER NOT NULL, `noticeSemester` INTEGER NOT NULL, `noticeText` TEXT NOT NULL, `noticeCategory` TEXT, `noticePoints` REAL, `teacherId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `noticeId`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "noticeId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "noticeType",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "semester",
+ "columnName": "noticeSemester",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "text",
+ "columnName": "noticeText",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "category",
+ "columnName": "noticeCategory",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "points",
+ "columnName": "noticePoints",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "teacherId",
+ "columnName": "teacherId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "addedDate",
+ "columnName": "addedDate",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "keep",
+ "columnName": "keep",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profileId",
+ "noticeId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_notices_profileId",
+ "unique": false,
+ "columnNames": [
+ "profileId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_notices_profileId` ON `${TABLE_NAME}` (`profileId`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "teams",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `teamId` INTEGER NOT NULL, `teamType` INTEGER NOT NULL, `teamName` TEXT, `teamCode` TEXT, `teamTeacherId` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `teamId`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "teamId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "teamType",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "teamName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "code",
+ "columnName": "teamCode",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "teacherId",
+ "columnName": "teamTeacherId",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profileId",
+ "teamId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "attendances",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `attendanceId` INTEGER NOT NULL, `attendanceBaseType` INTEGER NOT NULL, `attendanceTypeName` TEXT NOT NULL, `attendanceTypeShort` TEXT NOT NULL, `attendanceTypeSymbol` TEXT NOT NULL, `attendanceTypeColor` INTEGER, `attendanceDate` TEXT NOT NULL, `attendanceTime` TEXT, `attendanceSemester` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `attendanceLessonTopic` TEXT, `attendanceLessonNumber` INTEGER, `attendanceIsCounted` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `attendanceId`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "attendanceId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "baseType",
+ "columnName": "attendanceBaseType",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "typeName",
+ "columnName": "attendanceTypeName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "typeShort",
+ "columnName": "attendanceTypeShort",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "typeSymbol",
+ "columnName": "attendanceTypeSymbol",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "typeColor",
+ "columnName": "attendanceTypeColor",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "date",
+ "columnName": "attendanceDate",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "startTime",
+ "columnName": "attendanceTime",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "semester",
+ "columnName": "attendanceSemester",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "teacherId",
+ "columnName": "teacherId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subjectId",
+ "columnName": "subjectId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "addedDate",
+ "columnName": "addedDate",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lessonTopic",
+ "columnName": "attendanceLessonTopic",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "lessonNumber",
+ "columnName": "attendanceLessonNumber",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isCounted",
+ "columnName": "attendanceIsCounted",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "keep",
+ "columnName": "keep",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profileId",
+ "attendanceId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_attendances_profileId",
+ "unique": false,
+ "columnNames": [
+ "profileId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_attendances_profileId` ON `${TABLE_NAME}` (`profileId`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "events",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `eventId` INTEGER NOT NULL, `eventDate` TEXT NOT NULL, `eventTime` TEXT, `eventTopic` TEXT NOT NULL, `eventColor` INTEGER, `eventType` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `teamId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `eventAddedManually` INTEGER NOT NULL, `eventSharedBy` TEXT, `eventSharedByName` TEXT, `eventBlacklisted` INTEGER NOT NULL, `eventIsDone` INTEGER NOT NULL, `eventIsDownloaded` INTEGER NOT NULL, `homeworkBody` TEXT, `attachmentIds` TEXT, `attachmentNames` TEXT, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `eventId`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "eventId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "date",
+ "columnName": "eventDate",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "time",
+ "columnName": "eventTime",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "topic",
+ "columnName": "eventTopic",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "color",
+ "columnName": "eventColor",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "eventType",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "teacherId",
+ "columnName": "teacherId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subjectId",
+ "columnName": "subjectId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "teamId",
+ "columnName": "teamId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "addedDate",
+ "columnName": "addedDate",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "addedManually",
+ "columnName": "eventAddedManually",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sharedBy",
+ "columnName": "eventSharedBy",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "sharedByName",
+ "columnName": "eventSharedByName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "blacklisted",
+ "columnName": "eventBlacklisted",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isDone",
+ "columnName": "eventIsDone",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isDownloaded",
+ "columnName": "eventIsDownloaded",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "homeworkBody",
+ "columnName": "homeworkBody",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "attachmentIds",
+ "columnName": "attachmentIds",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "attachmentNames",
+ "columnName": "attachmentNames",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "keep",
+ "columnName": "keep",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profileId",
+ "eventId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_events_profileId_eventDate_eventTime",
+ "unique": false,
+ "columnNames": [
+ "profileId",
+ "eventDate",
+ "eventTime"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_events_profileId_eventDate_eventTime` ON `${TABLE_NAME}` (`profileId`, `eventDate`, `eventTime`)"
+ },
+ {
+ "name": "index_events_profileId_eventType",
+ "unique": false,
+ "columnNames": [
+ "profileId",
+ "eventType"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_events_profileId_eventType` ON `${TABLE_NAME}` (`profileId`, `eventType`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "eventTypes",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `eventType` INTEGER NOT NULL, `eventTypeName` TEXT NOT NULL, `eventTypeColor` INTEGER NOT NULL, `eventTypeOrder` INTEGER NOT NULL, `eventTypeSource` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `eventType`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "eventType",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "eventTypeName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "color",
+ "columnName": "eventTypeColor",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "eventTypeOrder",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "source",
+ "columnName": "eventTypeSource",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profileId",
+ "eventType"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "loginStores",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`loginStoreId` INTEGER NOT NULL, `loginStoreType` INTEGER NOT NULL, `loginStoreMode` INTEGER NOT NULL, `loginStoreData` TEXT NOT NULL, PRIMARY KEY(`loginStoreId`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "loginStoreId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "loginStoreType",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mode",
+ "columnName": "loginStoreMode",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "data",
+ "columnName": "loginStoreData",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "loginStoreId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "profiles",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `loginStoreId` INTEGER NOT NULL, `loginStoreType` INTEGER NOT NULL, `name` TEXT NOT NULL, `subname` TEXT, `studentNameLong` TEXT NOT NULL, `studentNameShort` TEXT NOT NULL, `accountName` TEXT, `studentData` TEXT NOT NULL, `image` TEXT, `empty` INTEGER NOT NULL, `archived` INTEGER NOT NULL, `archiveId` INTEGER, `syncEnabled` INTEGER NOT NULL, `enableSharedEvents` INTEGER NOT NULL, `registration` INTEGER NOT NULL, `userCode` TEXT NOT NULL, `studentNumber` INTEGER NOT NULL, `studentClassName` TEXT, `studentSchoolYearStart` INTEGER NOT NULL, `dateSemester1Start` TEXT NOT NULL, `dateSemester2Start` TEXT NOT NULL, `dateYearEnd` TEXT NOT NULL, `disabledNotifications` TEXT, `lastReceiversSync` INTEGER NOT NULL, PRIMARY KEY(`profileId`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "loginStoreId",
+ "columnName": "loginStoreId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "loginStoreType",
+ "columnName": "loginStoreType",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subname",
+ "columnName": "subname",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "studentNameLong",
+ "columnName": "studentNameLong",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "studentNameShort",
+ "columnName": "studentNameShort",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "accountName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "studentData",
+ "columnName": "studentData",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "image",
+ "columnName": "image",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "empty",
+ "columnName": "empty",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "archived",
+ "columnName": "archived",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "archiveId",
+ "columnName": "archiveId",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "syncEnabled",
+ "columnName": "syncEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "enableSharedEvents",
+ "columnName": "enableSharedEvents",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "registration",
+ "columnName": "registration",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userCode",
+ "columnName": "userCode",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "studentNumber",
+ "columnName": "studentNumber",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "studentClassName",
+ "columnName": "studentClassName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "studentSchoolYearStart",
+ "columnName": "studentSchoolYearStart",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "dateSemester1Start",
+ "columnName": "dateSemester1Start",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "dateSemester2Start",
+ "columnName": "dateSemester2Start",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "dateYearEnd",
+ "columnName": "dateYearEnd",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "disabledNotifications",
+ "columnName": "disabledNotifications",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "lastReceiversSync",
+ "columnName": "lastReceiversSync",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profileId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "luckyNumbers",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `luckyNumberDate` INTEGER NOT NULL, `luckyNumber` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `luckyNumberDate`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "date",
+ "columnName": "luckyNumberDate",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "number",
+ "columnName": "luckyNumber",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "keep",
+ "columnName": "keep",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profileId",
+ "luckyNumberDate"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "announcements",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `announcementId` INTEGER NOT NULL, `announcementSubject` TEXT NOT NULL, `announcementText` TEXT, `announcementStartDate` TEXT, `announcementEndDate` TEXT, `teacherId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `announcementIdString` TEXT, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `announcementId`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "announcementId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subject",
+ "columnName": "announcementSubject",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "text",
+ "columnName": "announcementText",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "startDate",
+ "columnName": "announcementStartDate",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "endDate",
+ "columnName": "announcementEndDate",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "teacherId",
+ "columnName": "teacherId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "addedDate",
+ "columnName": "addedDate",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "idString",
+ "columnName": "announcementIdString",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "keep",
+ "columnName": "keep",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profileId",
+ "announcementId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_announcements_profileId",
+ "unique": false,
+ "columnNames": [
+ "profileId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_announcements_profileId` ON `${TABLE_NAME}` (`profileId`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "gradeCategories",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `categoryId` INTEGER NOT NULL, `weight` REAL NOT NULL, `color` INTEGER NOT NULL, `text` TEXT, `columns` TEXT, `valueFrom` REAL NOT NULL, `valueTo` REAL NOT NULL, `type` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `categoryId`, `type`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "categoryId",
+ "columnName": "categoryId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "weight",
+ "columnName": "weight",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "color",
+ "columnName": "color",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "text",
+ "columnName": "text",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "columns",
+ "columnName": "columns",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "valueFrom",
+ "columnName": "valueFrom",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "valueTo",
+ "columnName": "valueTo",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profileId",
+ "categoryId",
+ "type"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "feedbackMessages",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `received` INTEGER NOT NULL, `text` TEXT NOT NULL, `senderName` TEXT NOT NULL, `deviceId` TEXT, `deviceName` TEXT, `devId` INTEGER, `devImage` TEXT, `sentTime` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "messageId",
+ "columnName": "messageId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "received",
+ "columnName": "received",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "text",
+ "columnName": "text",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "senderName",
+ "columnName": "senderName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "deviceId",
+ "columnName": "deviceId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "deviceName",
+ "columnName": "deviceName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "devId",
+ "columnName": "devId",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "devImage",
+ "columnName": "devImage",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "sentTime",
+ "columnName": "sentTime",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "messageId"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "messages",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `messageId` INTEGER NOT NULL, `messageType` INTEGER NOT NULL, `messageSubject` TEXT NOT NULL, `messageBody` TEXT, `senderId` INTEGER, `addedDate` INTEGER NOT NULL, `messageIsPinned` INTEGER NOT NULL, `hasAttachments` INTEGER NOT NULL, `attachmentIds` TEXT, `attachmentNames` TEXT, `attachmentSizes` TEXT, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `messageId`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "messageId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "messageType",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subject",
+ "columnName": "messageSubject",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "body",
+ "columnName": "messageBody",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "senderId",
+ "columnName": "senderId",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedDate",
+ "columnName": "addedDate",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isStarred",
+ "columnName": "messageIsPinned",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasAttachments",
+ "columnName": "hasAttachments",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "attachmentIds",
+ "columnName": "attachmentIds",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "attachmentNames",
+ "columnName": "attachmentNames",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "attachmentSizes",
+ "columnName": "attachmentSizes",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "keep",
+ "columnName": "keep",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profileId",
+ "messageId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_messages_profileId_messageType",
+ "unique": false,
+ "columnNames": [
+ "profileId",
+ "messageType"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_profileId_messageType` ON `${TABLE_NAME}` (`profileId`, `messageType`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "messageRecipients",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `messageRecipientId` INTEGER NOT NULL, `messageRecipientReplyId` INTEGER NOT NULL, `messageRecipientReadDate` INTEGER NOT NULL, `messageId` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `messageRecipientId`, `messageId`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "messageRecipientId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replyId",
+ "columnName": "messageRecipientReplyId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "readDate",
+ "columnName": "messageRecipientReadDate",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "messageId",
+ "columnName": "messageId",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profileId",
+ "messageRecipientId",
+ "messageId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "debugLogs",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "text",
+ "columnName": "text",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "endpointTimers",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `endpointId` INTEGER NOT NULL, `endpointLastSync` INTEGER, `endpointNextSync` INTEGER NOT NULL, `endpointViewId` INTEGER, PRIMARY KEY(`profileId`, `endpointId`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "endpointId",
+ "columnName": "endpointId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastSync",
+ "columnName": "endpointLastSync",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "nextSync",
+ "columnName": "endpointNextSync",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "viewId",
+ "columnName": "endpointViewId",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profileId",
+ "endpointId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "lessonRanges",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `lessonRangeNumber` INTEGER NOT NULL, `lessonRangeStart` TEXT NOT NULL, `lessonRangeEnd` TEXT NOT NULL, PRIMARY KEY(`profileId`, `lessonRangeNumber`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lessonNumber",
+ "columnName": "lessonRangeNumber",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "startTime",
+ "columnName": "lessonRangeStart",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "endTime",
+ "columnName": "lessonRangeEnd",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profileId",
+ "lessonRangeNumber"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "notifications",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `text` TEXT NOT NULL, `textLong` TEXT, `type` INTEGER NOT NULL, `profileId` INTEGER, `profileName` TEXT, `posted` INTEGER NOT NULL, `viewId` INTEGER, `extras` TEXT, `addedDate` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "text",
+ "columnName": "text",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "textLong",
+ "columnName": "textLong",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileName",
+ "columnName": "profileName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "posted",
+ "columnName": "posted",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "viewId",
+ "columnName": "viewId",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "extras",
+ "columnName": "extras",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedDate",
+ "columnName": "addedDate",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "classrooms",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `id` INTEGER NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`profileId`, `id`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profileId",
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "noticeTypes",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `id` INTEGER NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`profileId`, `id`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profileId",
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "attendanceTypes",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `id` INTEGER NOT NULL, `baseType` INTEGER NOT NULL, `typeName` TEXT NOT NULL, `typeShort` TEXT NOT NULL, `typeSymbol` TEXT NOT NULL, `typeColor` INTEGER, PRIMARY KEY(`profileId`, `id`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "baseType",
+ "columnName": "baseType",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "typeName",
+ "columnName": "typeName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "typeShort",
+ "columnName": "typeShort",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "typeSymbol",
+ "columnName": "typeSymbol",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "typeColor",
+ "columnName": "typeColor",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profileId",
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "timetable",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `date` TEXT, `lessonNumber` INTEGER, `startTime` TEXT, `endTime` TEXT, `subjectId` INTEGER, `teacherId` INTEGER, `teamId` INTEGER, `classroom` TEXT, `oldDate` TEXT, `oldLessonNumber` INTEGER, `oldStartTime` TEXT, `oldEndTime` TEXT, `oldSubjectId` INTEGER, `oldTeacherId` INTEGER, `oldTeamId` INTEGER, `oldClassroom` TEXT, `isExtra` INTEGER NOT NULL, `color` INTEGER, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `id`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "date",
+ "columnName": "date",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "lessonNumber",
+ "columnName": "lessonNumber",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "startTime",
+ "columnName": "startTime",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "endTime",
+ "columnName": "endTime",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "subjectId",
+ "columnName": "subjectId",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "teacherId",
+ "columnName": "teacherId",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "teamId",
+ "columnName": "teamId",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "classroom",
+ "columnName": "classroom",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "oldDate",
+ "columnName": "oldDate",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "oldLessonNumber",
+ "columnName": "oldLessonNumber",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "oldStartTime",
+ "columnName": "oldStartTime",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "oldEndTime",
+ "columnName": "oldEndTime",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "oldSubjectId",
+ "columnName": "oldSubjectId",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "oldTeacherId",
+ "columnName": "oldTeacherId",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "oldTeamId",
+ "columnName": "oldTeamId",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "oldClassroom",
+ "columnName": "oldClassroom",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isExtra",
+ "columnName": "isExtra",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "color",
+ "columnName": "color",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "keep",
+ "columnName": "keep",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profileId",
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_timetable_profileId_type_date",
+ "unique": false,
+ "columnNames": [
+ "profileId",
+ "type",
+ "date"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_timetable_profileId_type_date` ON `${TABLE_NAME}` (`profileId`, `type`, `date`)"
+ },
+ {
+ "name": "index_timetable_profileId_type_oldDate",
+ "unique": false,
+ "columnNames": [
+ "profileId",
+ "type",
+ "oldDate"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_timetable_profileId_type_oldDate` ON `${TABLE_NAME}` (`profileId`, `type`, `oldDate`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "config",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `key` TEXT NOT NULL, `value` TEXT, PRIMARY KEY(`profileId`, `key`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profileId",
+ "key"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "librusLessons",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `lessonId` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `teamId` INTEGER, PRIMARY KEY(`profileId`, `lessonId`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lessonId",
+ "columnName": "lessonId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "teacherId",
+ "columnName": "teacherId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subjectId",
+ "columnName": "subjectId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "teamId",
+ "columnName": "teamId",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profileId",
+ "lessonId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_librusLessons_profileId",
+ "unique": false,
+ "columnNames": [
+ "profileId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_librusLessons_profileId` ON `${TABLE_NAME}` (`profileId`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "timetableManual",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `type` INTEGER NOT NULL, `repeatBy` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `date` INTEGER, `weekDay` INTEGER, `lessonNumber` INTEGER, `startTime` TEXT, `endTime` TEXT, `subjectId` INTEGER, `teacherId` INTEGER, `teamId` INTEGER, `classroom` TEXT)",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "repeatBy",
+ "columnName": "repeatBy",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "date",
+ "columnName": "date",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "weekDay",
+ "columnName": "weekDay",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "lessonNumber",
+ "columnName": "lessonNumber",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "startTime",
+ "columnName": "startTime",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "endTime",
+ "columnName": "endTime",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "subjectId",
+ "columnName": "subjectId",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "teacherId",
+ "columnName": "teacherId",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "teamId",
+ "columnName": "teamId",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "classroom",
+ "columnName": "classroom",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "index_timetableManual_profileId_date",
+ "unique": false,
+ "columnNames": [
+ "profileId",
+ "date"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_timetableManual_profileId_date` ON `${TABLE_NAME}` (`profileId`, `date`)"
+ },
+ {
+ "name": "index_timetableManual_profileId_weekDay",
+ "unique": false,
+ "columnNames": [
+ "profileId",
+ "weekDay"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_timetableManual_profileId_weekDay` ON `${TABLE_NAME}` (`profileId`, `weekDay`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "notes",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `noteId` INTEGER NOT NULL, `noteOwnerType` TEXT, `noteOwnerId` INTEGER, `noteReplacesOriginal` INTEGER NOT NULL, `noteTopic` TEXT, `noteBody` TEXT NOT NULL, `noteColor` INTEGER, `noteSharedBy` TEXT, `noteSharedByName` TEXT, `addedDate` INTEGER NOT NULL, PRIMARY KEY(`noteId`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "noteId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "ownerType",
+ "columnName": "noteOwnerType",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "ownerId",
+ "columnName": "noteOwnerId",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "replacesOriginal",
+ "columnName": "noteReplacesOriginal",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "topic",
+ "columnName": "noteTopic",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "body",
+ "columnName": "noteBody",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "color",
+ "columnName": "noteColor",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "sharedBy",
+ "columnName": "noteSharedBy",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "sharedByName",
+ "columnName": "noteSharedByName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedDate",
+ "columnName": "addedDate",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "noteId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_notes_profileId_noteOwnerType_noteOwnerId",
+ "unique": false,
+ "columnNames": [
+ "profileId",
+ "noteOwnerType",
+ "noteOwnerId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_notes_profileId_noteOwnerType_noteOwnerId` ON `${TABLE_NAME}` (`profileId`, `noteOwnerType`, `noteOwnerId`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "metadata",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `metadataId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `thingType` INTEGER NOT NULL, `thingId` INTEGER NOT NULL, `seen` INTEGER NOT NULL, `notified` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "metadataId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "thingType",
+ "columnName": "thingType",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "thingId",
+ "columnName": "thingId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "seen",
+ "columnName": "seen",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notified",
+ "columnName": "notified",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "metadataId"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "index_metadata_profileId_thingType_thingId",
+ "unique": true,
+ "columnNames": [
+ "profileId",
+ "thingType",
+ "thingId"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_metadata_profileId_thingType_thingId` ON `${TABLE_NAME}` (`profileId`, `thingType`, `thingId`)"
+ }
+ ],
+ "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, '2612ebba9802eedc7ebc69724606a23c')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4119bf65..2c24b426 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -157,6 +157,10 @@
android:configChanges="orientation|keyboardHidden"
android:exported="false"
android:theme="@style/Base.Theme.AppCompat" />
+
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt
index 7197ea10..000f9be6 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt
@@ -34,6 +34,7 @@ import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import pl.droidsonroids.gif.GifDrawable
+import pl.szczodrzynski.edziennik.data.api.ERROR_REQUIRES_USER_ACTION
import pl.szczodrzynski.edziennik.data.api.ERROR_VULCAN_API_DEPRECATED
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.events.*
@@ -69,6 +70,7 @@ import pl.szczodrzynski.edziennik.ui.grades.editor.GradesEditorFragment
import pl.szczodrzynski.edziennik.ui.home.HomeFragment
import pl.szczodrzynski.edziennik.ui.homework.HomeworkFragment
import pl.szczodrzynski.edziennik.ui.login.LoginActivity
+import pl.szczodrzynski.edziennik.ui.login.LoginProgressFragment
import pl.szczodrzynski.edziennik.ui.messages.compose.MessagesComposeFragment
import pl.szczodrzynski.edziennik.ui.messages.list.MessagesFragment
import pl.szczodrzynski.edziennik.ui.messages.single.MessageFragment
@@ -83,6 +85,7 @@ import pl.szczodrzynski.edziennik.utils.*
import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.Utils.dpToPx
import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager.Error.Type
+import pl.szczodrzynski.edziennik.utils.managers.UserActionManager
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.NavTarget
import pl.szczodrzynski.navlib.*
@@ -853,7 +856,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
@Subscribe(threadMode = ThreadMode.MAIN)
fun onUserActionRequiredEvent(event: UserActionRequiredEvent) {
- app.userActionManager.execute(this, event.profileId, event.type)
+ app.userActionManager.execute(this, event, UserActionManager.UserActionCallback())
}
private fun fragmentToSyncName(currentFragment: Int): Int {
@@ -911,11 +914,13 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
false
}
"userActionRequired" -> {
- app.userActionManager.execute(
- this,
- extras.getInt("profileId"),
- extras.getInt("type")
+ val event = UserActionRequiredEvent(
+ profileId = extras.getInt("profileId"),
+ type = extras.getEnum("type") ?: return,
+ params = extras.getBundle("params") ?: return,
+ errorText = 0,
)
+ app.userActionManager.execute(this, event, UserActionManager.UserActionCallback())
true
}
"createManualEvent" -> {
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/ApiService.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/ApiService.kt
index f68e8840..0d9fe92c 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/ApiService.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/ApiService.kt
@@ -84,19 +84,21 @@ class ApiService : Service() {
runTask()
}
+ override fun onRequiresUserAction(event: UserActionRequiredEvent) {
+ app.userActionManager.sendToUser(event)
+ taskRunning?.cancel()
+ clearTask()
+ runTask()
+ }
+
override fun onError(apiError: ApiError) {
lastEventTime = System.currentTimeMillis()
d(TAG, "Task $taskRunningId threw an error - $apiError")
apiError.profileId = taskProfileId
- if (app.userActionManager.requiresUserAction(apiError)) {
- app.userActionManager.sendToUser(apiError)
- }
- else {
- EventBus.getDefault().postSticky(ApiTaskErrorEvent(apiError))
- errorList.add(apiError)
- apiError.throwable?.printStackTrace()
- }
+ EventBus.getDefault().postSticky(ApiTaskErrorEvent(apiError))
+ errorList.add(apiError)
+ apiError.throwable?.printStackTrace()
if (apiError.isCritical) {
taskRunning?.cancel()
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt
index 3df74621..c29b8698 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt
@@ -59,6 +59,9 @@ const val LIBRUS_SANDBOX_URL = "https://sandbox.librus.pl/index.php?action="
const val LIBRUS_SYNERGIA_HOMEWORK_ATTACHMENT_URL = "https://synergia.librus.pl/homework/downloadFile"
const val LIBRUS_SYNERGIA_MESSAGES_ATTACHMENT_URL = "https://synergia.librus.pl/wiadomosci/pobierz_zalacznik"
+const val LIBRUS_PORTAL_RECAPTCHA_KEY = "6Lf48moUAAAAAB9ClhdvHr46gRWR"
+const val LIBRUS_PORTAL_RECAPTCHA_REFERER = "https://portal.librus.pl/rodzina/login"
+
val MOBIDZIENNIK_USER_AGENT = SYSTEM_USER_AGENT
@@ -99,3 +102,12 @@ const val PODLASIE_API_VERSION = "1.0.62"
const val PODLASIE_API_URL = "https://cpdklaser.zeto.bialystok.pl/api"
const val PODLASIE_API_USER_ENDPOINT = "/pobierzDaneUcznia"
const val PODLASIE_API_LOGOUT_DEVICES_ENDPOINT = "/wyczyscUrzadzenia"
+
+const val USOS_API_OAUTH_REDIRECT_URL = "szkolny://redirect/usos"
+
+val USOS_API_SCOPES by lazy { listOf(
+ "offline_access",
+ "studies",
+ "grades",
+ "events",
+) }
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt
index 227561e6..93a7228f 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt
@@ -58,11 +58,7 @@ const val ERROR_INVALID_LOGIN_MODE = 110
const val ERROR_LOGIN_METHOD_NOT_SATISFIED = 111
const val ERROR_NOT_IMPLEMENTED = 112
const val ERROR_FILE_DOWNLOAD = 113
-
-const val ERROR_NO_STUDENTS_IN_ACCOUNT = 115
-
-const val ERROR_CAPTCHA_NEEDED = 3000
-const val ERROR_CAPTCHA_LIBRUS_PORTAL = 3001
+const val ERROR_REQUIRES_USER_ACTION = 114
const val ERROR_API_PDO_ERROR = 5000
const val ERROR_API_INVALID_CLIENT = 5001
@@ -204,6 +200,12 @@ const val ERROR_PODLASIE_API_NO_TOKEN = 630
const val ERROR_PODLASIE_API_OTHER = 631
const val ERROR_PODLASIE_API_DATA_MISSING = 632
+const val ERROR_USOS_OAUTH_GOT_DIFFERENT_TOKEN = 702
+const val ERROR_USOS_OAUTH_INCOMPLETE_RESPONSE = 703
+const val ERROR_USOS_NO_STUDENT_PROGRAMMES = 704
+const val ERROR_USOS_API_INCOMPLETE_RESPONSE = 705
+const val ERROR_USOS_API_MISSING_RESPONSE = 706
+
const val ERROR_TEMPLATE_WEB_OTHER = 801
const val EXCEPTION_API_TASK = 900
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/LoginMethods.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/LoginMethods.kt
index 0f0c07ae..9825ea3f 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/LoginMethods.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/LoginMethods.kt
@@ -13,6 +13,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login.Mobidzie
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.login.PodlasieLoginApi
import pl.szczodrzynski.edziennik.data.api.edziennik.template.login.TemplateLoginApi
import pl.szczodrzynski.edziennik.data.api.edziennik.template.login.TemplateLoginWeb
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.login.UsosLoginApi
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginHebe
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginWebMain
import pl.szczodrzynski.edziennik.data.api.models.LoginMethod
@@ -127,6 +128,15 @@ val podlasieLoginMethods = listOf(
.withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED }
)
+const val LOGIN_TYPE_USOS = 7
+const val LOGIN_MODE_USOS_OAUTH = 0
+const val LOGIN_METHOD_USOS_API = 100
+val usosLoginMethods = listOf(
+ LoginMethod(LOGIN_TYPE_USOS, LOGIN_METHOD_USOS_API, UsosLoginApi::class.java)
+ .withIsPossible { _, _ -> true }
+ .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED }
+)
+
val templateLoginMethods = listOf(
LoginMethod(LOGIN_TYPE_TEMPLATE, LOGIN_METHOD_TEMPLATE_WEB, TemplateLoginWeb::class.java)
.withIsPossible { _, _ -> true }
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt
index 0b49e38b..097ed29e 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt
@@ -12,6 +12,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.Librus
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.Mobidziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.Podlasie
import pl.szczodrzynski.edziennik.data.api.edziennik.template.Template
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.Usos
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.Vulcan
import pl.szczodrzynski.edziennik.data.api.events.RegisterAvailabilityEvent
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
@@ -113,6 +114,7 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
LOGIN_TYPE_VULCAN -> Vulcan(app, profile, loginStore, taskCallback)
LOGIN_TYPE_PODLASIE -> Podlasie(app, profile, loginStore, taskCallback)
LOGIN_TYPE_TEMPLATE -> Template(app, profile, loginStore, taskCallback)
+ LOGIN_TYPE_USOS -> Usos(app, profile, loginStore, taskCallback)
else -> null
}
if (edziennikInterface == null) {
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/Librus.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/Librus.kt
index abf3ac5f..0830752f 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/Librus.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/Librus.kt
@@ -16,6 +16,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages.Librus
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia.*
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.firstlogin.LibrusFirstLogin
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLogin
+import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError
@@ -162,6 +163,7 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va
private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback {
return object : EdziennikCallback {
override fun onCompleted() { callback.onCompleted() }
+ override fun onRequiresUserAction(event: UserActionRequiredEvent) { callback.onRequiresUserAction(event) }
override fun onProgress(step: Float) { callback.onProgress(step) }
override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) }
override fun onError(apiError: ApiError) {
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginPortal.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginPortal.kt
index ed01d01f..ab21c692 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginPortal.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginPortal.kt
@@ -10,6 +10,7 @@ import im.wangchao.mhttp.callback.TextCallbackHandler
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus
+import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.ext.*
import pl.szczodrzynski.edziennik.utils.Utils.d
@@ -148,12 +149,23 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
val error = if (response.code() == 200) null else
json.getJsonArray("errors")?.getString(0)
?: json.getJsonObject("errors")?.entrySet()?.firstOrNull()?.value?.asString
+
+ if (error?.contains("robotem") == true || json.getBoolean("captchaRequired") == true) {
+ data.requireUserAction(
+ type = UserActionRequiredEvent.Type.RECAPTCHA,
+ params = Bundle(
+ "siteKey" to LIBRUS_PORTAL_RECAPTCHA_KEY,
+ "referer" to LIBRUS_PORTAL_RECAPTCHA_REFERER,
+ ),
+ errorText = R.string.notification_user_action_required_captcha_librus,
+ )
+ return
+ }
+
error?.let { code ->
when {
code.contains("Sesja logowania wygasła") -> ERROR_LOGIN_LIBRUS_PORTAL_CSRF_EXPIRED
code.contains("Upewnij się, że nie") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN
- // this doesn't work anyway: `errors` is an object with `g-recaptcha-response` set
- code.contains("robotem") -> ERROR_CAPTCHA_LIBRUS_PORTAL
code.contains("Podany adres e-mail jest nieprawidłowy.") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN
else -> ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR
}.let { errorCode ->
@@ -163,12 +175,6 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
return
}
}
- if (json.getBoolean("captchaRequired") == true) {
- data.error(ApiError(TAG, ERROR_CAPTCHA_LIBRUS_PORTAL)
- .withResponse(response)
- .withApiResponse(json))
- return
- }
authorize(json.getString("redirect", LIBRUS_AUTHORIZE_URL))
}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/Mobidziennik.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/Mobidziennik.kt
index e19fabca..0144ad5d 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/Mobidziennik.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/Mobidziennik.kt
@@ -11,6 +11,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.Mobidzien
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web.*
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.firstlogin.MobidziennikFirstLogin
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login.MobidziennikLogin
+import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError
@@ -142,6 +143,7 @@ class Mobidziennik(val app: App, val profile: Profile?, val loginStore: LoginSto
private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback {
return object : EdziennikCallback {
override fun onCompleted() { callback.onCompleted() }
+ override fun onRequiresUserAction(event: UserActionRequiredEvent) { callback.onRequiresUserAction(event) }
override fun onProgress(step: Float) { callback.onProgress(step) }
override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) }
override fun onError(apiError: ApiError) {
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/Podlasie.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/Podlasie.kt
index d7ec441f..0eaaebae 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/Podlasie.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/Podlasie.kt
@@ -12,6 +12,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data.PodlasieData
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.firstlogin.PodlasieFirstLogin
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.login.PodlasieLogin
import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent
+import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError
@@ -142,6 +143,10 @@ class Podlasie(val app: App, val profile: Profile?, val loginStore: LoginStore,
callback.onCompleted()
}
+ override fun onRequiresUserAction(event: UserActionRequiredEvent) {
+ callback.onRequiresUserAction(event)
+ }
+
override fun onProgress(step: Float) {
callback.onProgress(step)
}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/Template.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/Template.kt
index 843d36e9..8abf079f 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/Template.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/Template.kt
@@ -10,6 +10,7 @@ import pl.szczodrzynski.edziennik.data.api.CODE_INTERNAL_LIBRUS_ACCOUNT_410
import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.TemplateData
import pl.szczodrzynski.edziennik.data.api.edziennik.template.firstlogin.TemplateFirstLogin
import pl.szczodrzynski.edziennik.data.api.edziennik.template.login.TemplateLogin
+import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError
@@ -108,6 +109,10 @@ class Template(val app: App, val profile: Profile?, val loginStore: LoginStore,
callback.onCompleted()
}
+ override fun onRequiresUserAction(event: UserActionRequiredEvent) {
+ callback.onRequiresUserAction(event)
+ }
+
override fun onProgress(step: Float) {
callback.onProgress(step)
}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt
new file mode 100644
index 00000000..5cd054f8
--- /dev/null
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) Kuba Szczodrzyński 2022-10-11.
+ */
+
+package pl.szczodrzynski.edziennik.data.api.edziennik.usos
+
+import pl.szczodrzynski.edziennik.App
+import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_USOS_API
+import pl.szczodrzynski.edziennik.data.api.models.Data
+import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
+import pl.szczodrzynski.edziennik.data.db.entity.Profile
+
+class DataUsos(
+ app: App,
+ profile: Profile?,
+ loginStore: LoginStore,
+) : Data(app, profile, loginStore) {
+
+ fun isApiLoginValid() = oauthTokenKey != null && oauthTokenSecret != null && oauthTokenIsUser
+
+ override fun satisfyLoginMethods() {
+ loginMethods.clear()
+ if (isApiLoginValid()) {
+ loginMethods += LOGIN_METHOD_USOS_API
+ }
+ }
+
+ override fun generateUserCode() = "$schoolId:${profile?.studentNumber ?: studentId}"
+
+ var schoolId: String?
+ get() { mSchoolId = mSchoolId ?: loginStore.getLoginData("schoolId", null); return mSchoolId }
+ set(value) { loginStore.putLoginData("schoolId", value); mSchoolId = value }
+ private var mSchoolId: String? = null
+
+ var instanceUrl: String?
+ get() { mInstanceUrl = mInstanceUrl ?: loginStore.getLoginData("instanceUrl", null); return mInstanceUrl }
+ set(value) { loginStore.putLoginData("instanceUrl", value); mInstanceUrl = value }
+ private var mInstanceUrl: String? = null
+
+ var oauthLoginResponse: String?
+ get() { mOauthLoginResponse = mOauthLoginResponse ?: loginStore.getLoginData("oauthLoginResponse", null); return mOauthLoginResponse }
+ set(value) { loginStore.putLoginData("oauthLoginResponse", value); mOauthLoginResponse = value }
+ private var mOauthLoginResponse: String? = null
+
+ var oauthConsumerKey: String?
+ get() { mOauthConsumerKey = mOauthConsumerKey ?: loginStore.getLoginData("oauthConsumerKey", null); return mOauthConsumerKey }
+ set(value) { loginStore.putLoginData("oauthConsumerKey", value); mOauthConsumerKey = value }
+ private var mOauthConsumerKey: String? = null
+
+ var oauthConsumerSecret: String?
+ get() { mOauthConsumerSecret = mOauthConsumerSecret ?: loginStore.getLoginData("oauthConsumerSecret", null); return mOauthConsumerSecret }
+ set(value) { loginStore.putLoginData("oauthConsumerSecret", value); mOauthConsumerSecret = value }
+ private var mOauthConsumerSecret: String? = null
+
+ var oauthTokenKey: String?
+ get() { mOauthTokenKey = mOauthTokenKey ?: loginStore.getLoginData("oauthTokenKey", null); return mOauthTokenKey }
+ set(value) { loginStore.putLoginData("oauthTokenKey", value); mOauthTokenKey = value }
+ private var mOauthTokenKey: String? = null
+
+ var oauthTokenSecret: String?
+ get() { mOauthTokenSecret = mOauthTokenSecret ?: loginStore.getLoginData("oauthTokenSecret", null); return mOauthTokenSecret }
+ set(value) { loginStore.putLoginData("oauthTokenSecret", value); mOauthTokenSecret = value }
+ private var mOauthTokenSecret: String? = null
+
+ var oauthTokenIsUser: Boolean
+ get() { mOauthTokenIsUser = mOauthTokenIsUser ?: loginStore.getLoginData("oauthTokenIsUser", false); return mOauthTokenIsUser ?: false }
+ set(value) { loginStore.putLoginData("oauthTokenIsUser", value); mOauthTokenIsUser = value }
+ private var mOauthTokenIsUser: Boolean? = null
+
+ var studentId: Int
+ get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", 0); return mStudentId ?: 0 }
+ set(value) { profile?.putStudentData("studentId", value) ?: return; mStudentId = value }
+ private var mStudentId: Int? = null
+}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/Usos.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/Usos.kt
new file mode 100644
index 00000000..cbf92ca8
--- /dev/null
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/Usos.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) Kuba Szczodrzyński 2022-10-11.
+ */
+
+package pl.szczodrzynski.edziennik.data.api.edziennik.usos
+
+import com.google.gson.JsonObject
+import pl.szczodrzynski.edziennik.App
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosData
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.firstlogin.UsosFirstLogin
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.login.UsosLogin
+import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
+import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
+import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
+import pl.szczodrzynski.edziennik.data.api.models.ApiError
+import pl.szczodrzynski.edziennik.data.api.prepare
+import pl.szczodrzynski.edziennik.data.api.usosLoginMethods
+import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
+import pl.szczodrzynski.edziennik.data.db.entity.Profile
+import pl.szczodrzynski.edziennik.data.db.entity.Teacher
+import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
+import pl.szczodrzynski.edziennik.data.db.full.EventFull
+import pl.szczodrzynski.edziennik.data.db.full.MessageFull
+import pl.szczodrzynski.edziennik.utils.Utils.d
+
+class Usos(
+ val app: App,
+ val profile: Profile?,
+ val loginStore: LoginStore,
+ val callback: EdziennikCallback,
+) : EdziennikInterface {
+ companion object {
+ private const val TAG = "Usos"
+ }
+
+ val internalErrorList = mutableListOf()
+ val data: DataUsos
+
+ init {
+ data = DataUsos(app, profile, loginStore).apply {
+ callback = wrapCallback(this@Usos.callback)
+ satisfyLoginMethods()
+ }
+ }
+
+ private fun completed() {
+ data.saveData()
+ callback.onCompleted()
+ }
+
+ override fun sync(
+ featureIds: List,
+ viewId: Int?,
+ onlyEndpoints: List?,
+ arguments: JsonObject?,
+ ) {
+ data.arguments = arguments
+ data.prepare(usosLoginMethods, UsosFeatures, featureIds, viewId, onlyEndpoints)
+ d(TAG, "LoginMethod IDs: ${data.targetLoginMethodIds}")
+ d(TAG, "Endpoint IDs: ${data.targetEndpointIds}")
+ UsosLogin(data) {
+ UsosData(data) {
+ completed()
+ }
+ }
+ }
+
+ override fun getMessage(message: MessageFull) {}
+ override fun sendMessage(recipients: List, subject: String, text: String) {}
+ override fun markAllAnnouncementsAsRead() {}
+ override fun getAnnouncement(announcement: AnnouncementFull) {}
+ override fun getAttachment(owner: Any, attachmentId: Long, attachmentName: String) {}
+ override fun getRecipientList() {}
+ override fun getEvent(eventFull: EventFull) {}
+
+ override fun firstLogin() {
+ UsosFirstLogin(data) {
+ completed()
+ }
+ }
+
+ override fun cancel() {
+ d(TAG, "Cancelled")
+ data.cancel()
+ }
+
+ private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback {
+ return object : EdziennikCallback {
+ override fun onCompleted() {
+ callback.onCompleted()
+ }
+
+ override fun onRequiresUserAction(event: UserActionRequiredEvent) {
+ callback.onRequiresUserAction(event)
+ }
+
+ override fun onProgress(step: Float) {
+ callback.onProgress(step)
+ }
+
+ override fun onStartProgress(stringRes: Int) {
+ callback.onStartProgress(stringRes)
+ }
+
+ override fun onError(apiError: ApiError) {
+ when (apiError.errorCode) {
+ in internalErrorList -> {
+ // finish immediately if the same error occurs twice during the same sync
+ callback.onError(apiError)
+ }
+ else -> callback.onError(apiError)
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/UsosFeatures.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/UsosFeatures.kt
new file mode 100644
index 00000000..50c15582
--- /dev/null
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/UsosFeatures.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) Kuba Szczodrzyński 2022-10-11.
+ */
+
+package pl.szczodrzynski.edziennik.data.api.edziennik.usos
+
+import pl.szczodrzynski.edziennik.data.api.*
+import pl.szczodrzynski.edziennik.data.api.models.Feature
+
+const val ENDPOINT_USOS_API_USER = 7000
+const val ENDPOINT_USOS_API_TERMS = 7010
+const val ENDPOINT_USOS_API_COURSES = 7020
+const val ENDPOINT_USOS_API_TIMETABLE = 7030
+
+val UsosFeatures = listOf(
+ /*
+ * Student information
+ */
+ Feature(LOGIN_TYPE_USOS, FEATURE_STUDENT_INFO, listOf(
+ ENDPOINT_USOS_API_USER to LOGIN_METHOD_USOS_API,
+ ), listOf(LOGIN_METHOD_USOS_API)),
+
+ /*
+ * Terms & courses
+ */
+ Feature(LOGIN_TYPE_USOS, FEATURE_SCHOOL_INFO, listOf(
+ ENDPOINT_USOS_API_TERMS to LOGIN_METHOD_USOS_API,
+ ), listOf(LOGIN_METHOD_USOS_API)),
+ Feature(LOGIN_TYPE_USOS, FEATURE_TEAM_INFO, listOf(
+ ENDPOINT_USOS_API_COURSES to LOGIN_METHOD_USOS_API,
+ ), listOf(LOGIN_METHOD_USOS_API)),
+
+ /*
+ * Timetable
+ */
+ Feature(LOGIN_TYPE_USOS, FEATURE_TIMETABLE, listOf(
+ ENDPOINT_USOS_API_TIMETABLE to LOGIN_METHOD_USOS_API,
+ ), listOf(LOGIN_METHOD_USOS_API)),
+)
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt
new file mode 100644
index 00000000..9f716d50
--- /dev/null
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt
@@ -0,0 +1,209 @@
+/*
+ * Copyright (c) Kuba Szczodrzyński 2022-10-13.
+ */
+
+package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data
+
+import com.google.gson.JsonArray
+import com.google.gson.JsonObject
+import im.wangchao.mhttp.Request
+import im.wangchao.mhttp.Response
+import im.wangchao.mhttp.body.MediaTypeUtils
+import im.wangchao.mhttp.callback.JsonArrayCallbackHandler
+import im.wangchao.mhttp.callback.JsonCallbackHandler
+import im.wangchao.mhttp.callback.TextCallbackHandler
+import pl.szczodrzynski.edziennik.data.api.ERROR_REQUEST_FAILURE
+import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_MISSING_RESPONSE
+import pl.szczodrzynski.edziennik.data.api.SERVER_USER_AGENT
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.login.UsosLoginApi
+import pl.szczodrzynski.edziennik.data.api.models.ApiError
+import pl.szczodrzynski.edziennik.ext.*
+import pl.szczodrzynski.edziennik.utils.Utils.d
+import java.net.HttpURLConnection.*
+import java.util.UUID
+
+open class UsosApi(open val data: DataUsos, open val lastSync: Long?) {
+ companion object {
+ private const val TAG = "UsosApi"
+ }
+
+ enum class ResponseType {
+ OBJECT,
+ ARRAY,
+ PLAIN,
+ }
+
+ val profileId
+ get() = data.profile?.id ?: -1
+
+ val profile
+ get() = data.profile
+
+ protected fun JsonObject.getLangString(key: String) =
+ this.getJsonObject(key)?.getString("pl")
+
+ protected fun JsonObject.getLecturerIds(key: String) =
+ this.getJsonArray(key)?.asJsonObjectList()?.mapNotNull {
+ val id = it.getLong("id") ?: return@mapNotNull null
+ val firstName = it.getString("first_name") ?: return@mapNotNull null
+ val lastName = it.getString("last_name") ?: return@mapNotNull null
+ data.getTeacher(firstName, lastName, id = id).id
+ } ?: listOf()
+
+ private fun valueToString(value: Any) = when (value) {
+ is String -> value
+ is Number -> value.toString()
+ is List<*> -> listToString(value)
+ else -> value.toString()
+ }
+
+ private fun listToString(list: List<*>): String {
+ return list.map {
+ if (it is Pair<*, *> && it.first is String && it.second is List<*>)
+ return@map "${it.first}[${listToString(it.second as List<*>)}]"
+ return@map valueToString(it ?: "")
+ }.joinToString("|")
+ }
+
+ private fun buildSignature(method: String, url: String, params: Map): String {
+ val query = params.toQueryString()
+ val signatureString = listOf(
+ method.uppercase(),
+ url.urlEncode(),
+ query.urlEncode(),
+ ).joinToString("&")
+ val signingKey = listOf(
+ data.oauthConsumerSecret ?: "",
+ data.oauthTokenSecret ?: "",
+ ).joinToString("&") { it.urlEncode() }
+ return signatureString.hmacSHA1(signingKey)
+ }
+
+ fun apiRequest(
+ tag: String,
+ service: String,
+ params: Map? = null,
+ fields: List? = null,
+ responseType: ResponseType,
+ onSuccess: (data: T, response: Response?) -> Unit,
+ ) {
+ val url = "${data.instanceUrl}services/$service"
+ d(tag, "Request: Usos/Api - $url")
+
+ val formData = mutableMapOf()
+ if (params != null)
+ formData.putAll(params.mapValues {
+ valueToString(it.value)
+ })
+ if (fields != null)
+ formData["fields"] = valueToString(fields)
+
+ val auth = mutableMapOf(
+ "realm" to url,
+ "oauth_consumer_key" to (data.oauthConsumerKey ?: ""),
+ "oauth_nonce" to UUID.randomUUID().toString(),
+ "oauth_signature_method" to "HMAC-SHA1",
+ "oauth_timestamp" to currentTimeUnix().toString(),
+ "oauth_token" to (data.oauthTokenKey ?: ""),
+ "oauth_version" to "1.0",
+ )
+ val signature = buildSignature(
+ method = "POST",
+ url = url,
+ params = formData + auth.filterKeys { it.startsWith("oauth_") },
+ )
+ auth["oauth_signature"] = signature
+
+ val authString = auth.map {
+ """${it.key}="${it.value.urlEncode()}""""
+ }.joinToString(", ")
+
+ Request.builder()
+ .url(url)
+ .userAgent(SERVER_USER_AGENT)
+ .addHeader("Authorization", "OAuth $authString")
+ .post()
+ .setTextBody(formData.toQueryString(), MediaTypeUtils.APPLICATION_FORM)
+ .allowErrorCode(HTTP_BAD_REQUEST)
+ .allowErrorCode(HTTP_UNAUTHORIZED)
+ .allowErrorCode(HTTP_FORBIDDEN)
+ .allowErrorCode(HTTP_NOT_FOUND)
+ .allowErrorCode(HTTP_UNAVAILABLE)
+ .callback(getCallback(tag, responseType, onSuccess))
+ .build()
+ .enqueue()
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ private fun getCallback(
+ tag: String,
+ responseType: ResponseType,
+ onSuccess: (data: T, response: Response?) -> Unit,
+ ) = when (responseType) {
+ ResponseType.OBJECT -> object : JsonCallbackHandler() {
+ override fun onSuccess(data: JsonObject?, response: Response) {
+ processResponse(tag, response, data as T?, onSuccess)
+ }
+
+ override fun onFailure(response: Response?, throwable: Throwable?) {
+ processError(tag, response, throwable)
+ }
+ }
+ ResponseType.ARRAY -> object : JsonArrayCallbackHandler() {
+ override fun onSuccess(data: JsonArray?, response: Response) {
+ processResponse(tag, response, data as T?, onSuccess)
+ }
+
+ override fun onFailure(response: Response?, throwable: Throwable?) {
+ processError(tag, response, throwable)
+ }
+ }
+ ResponseType.PLAIN -> object : TextCallbackHandler() {
+ override fun onSuccess(data: String?, response: Response) {
+ processResponse(tag, response, data as T?, onSuccess)
+ }
+
+ override fun onFailure(response: Response?, throwable: Throwable?) {
+ processError(tag, response, throwable)
+ }
+ }
+ }
+
+ private fun processResponse(
+ tag: String,
+ response: Response,
+ value: T?,
+ onSuccess: (data: T, response: Response?) -> Unit,
+ ) {
+ val errorCode = when {
+ response.code() == HTTP_UNAUTHORIZED -> {
+ data.oauthTokenKey = null
+ data.oauthTokenSecret = null
+ data.oauthTokenIsUser = false
+ data.oauthLoginResponse = null
+ UsosLoginApi(data) { }
+ return
+ }
+ value == null -> ERROR_USOS_API_MISSING_RESPONSE
+ response.code() == HTTP_OK -> {
+ onSuccess(value, response)
+ null
+ }
+ else -> response.toErrorCode()
+ }
+ if (errorCode != null) {
+ data.error(tag, errorCode, response, value.toString())
+ }
+ }
+
+ private fun processError(
+ tag: String,
+ response: Response?,
+ throwable: Throwable?,
+ ) {
+ data.error(ApiError(tag, ERROR_REQUEST_FAILURE)
+ .withResponse(response)
+ .withThrowable(throwable))
+ }
+}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt
new file mode 100644
index 00000000..d688bebf
--- /dev/null
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) Kuba Szczodrzyński 2022-10-13.
+ */
+
+package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data
+
+import pl.szczodrzynski.edziennik.R
+import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.web.TemplateWebSample
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.*
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiCourses
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiTerms
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiTimetable
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiUser
+import pl.szczodrzynski.edziennik.utils.Utils.d
+
+class UsosData(val data: DataUsos, val onSuccess: () -> Unit) {
+ companion object {
+ private const val TAG = "UsosData"
+ }
+
+ init {
+ nextEndpoint(onSuccess)
+ }
+
+ private fun nextEndpoint(onSuccess: () -> Unit) {
+ if (data.targetEndpointIds.isEmpty()) {
+ onSuccess()
+ return
+ }
+ if (data.cancelled) {
+ onSuccess()
+ return
+ }
+ val id = data.targetEndpointIds.firstKey()
+ val lastSync = data.targetEndpointIds.remove(id)
+ useEndpoint(id, lastSync) { endpointId ->
+ data.progress(data.progressStep)
+ nextEndpoint(onSuccess)
+ }
+ }
+
+ private fun useEndpoint(endpointId: Int, lastSync: Long?, onSuccess: (endpointId: Int) -> Unit) {
+ d(TAG, "Using endpoint $endpointId. Last sync time = $lastSync")
+ when (endpointId) {
+ ENDPOINT_USOS_API_USER -> {
+ data.startProgress(R.string.edziennik_progress_endpoint_student_info)
+ UsosApiUser(data, lastSync, onSuccess)
+ }
+ ENDPOINT_USOS_API_TERMS -> {
+ data.startProgress(R.string.edziennik_progress_endpoint_school_info)
+ UsosApiTerms(data, lastSync, onSuccess)
+ }
+ ENDPOINT_USOS_API_COURSES -> {
+ data.startProgress(R.string.edziennik_progress_endpoint_teams)
+ UsosApiCourses(data, lastSync, onSuccess)
+ }
+ ENDPOINT_USOS_API_TIMETABLE -> {
+ data.startProgress(R.string.edziennik_progress_endpoint_timetable)
+ UsosApiTimetable(data, lastSync, onSuccess)
+ }
+ else -> onSuccess(endpointId)
+ }
+ }
+}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiCourses.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiCourses.kt
new file mode 100644
index 00000000..e93c63dd
--- /dev/null
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiCourses.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) Kuba Szczodrzyński 2022-10-15.
+ */
+
+package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api
+
+import com.google.gson.JsonObject
+import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_COURSES
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi
+import pl.szczodrzynski.edziennik.data.db.entity.Team
+import pl.szczodrzynski.edziennik.ext.*
+
+class UsosApiCourses(
+ override val data: DataUsos,
+ override val lastSync: Long?,
+ val onSuccess: (endpointId: Int) -> Unit,
+) : UsosApi(data, lastSync) {
+ companion object {
+ const val TAG = "UsosApiCourses"
+ }
+
+ init {
+ apiRequest(
+ tag = TAG,
+ service = "courses/user",
+ fields = listOf(
+ // "terms" to listOf("id", "name", "start_date", "end_date"),
+ "course_editions" to listOf(
+ "course_id",
+ "course_name",
+ // "term_id",
+ "user_groups" to listOf(
+ "course_unit_id",
+ "group_number",
+ // "class_type",
+ "class_type_id",
+ "lecturers",
+ ),
+ ),
+ ),
+ responseType = ResponseType.OBJECT,
+ ) { json, response ->
+ if (!processResponse(json)) {
+ data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response)
+ return@apiRequest
+ }
+
+ data.setSyncNext(ENDPOINT_USOS_API_COURSES, 2 * DAY)
+ onSuccess(ENDPOINT_USOS_API_COURSES)
+ }
+ }
+
+ private fun processResponse(json: JsonObject): Boolean {
+ // val term = json.getJsonArray("terms")?.firstOrNull() ?: return false
+ val courseEditions = json.getJsonObject("course_editions")
+ ?.entrySet()
+ ?.flatMap { it.value.asJsonArray }
+ ?.map { it.asJsonObject } ?: return false
+
+ var hasValidTeam = false
+ for (courseEdition in courseEditions) {
+ val courseId = courseEdition.getString("course_id") ?: continue
+ val courseName = courseEdition.getLangString("course_name") ?: continue
+ val userGroups = courseEdition.getJsonArray("user_groups")?.asJsonObjectList() ?: continue
+ for (userGroup in userGroups) {
+ val courseUnitId = userGroup.getLong("course_unit_id") ?: continue
+ val groupNumber = userGroup.getInt("group_number") ?: continue
+ // val classType = userGroup.getLangString("class_type") ?: continue
+ val classTypeId = userGroup.getString("class_type_id") ?: continue
+ val lecturers = userGroup.getLecturerIds("lecturers")
+
+ data.teamList.put(courseUnitId, Team(
+ profileId,
+ courseUnitId,
+ "${profile?.studentClassName} $classTypeId$groupNumber - $courseName",
+ 2,
+ "${data.schoolId}:${courseId} $classTypeId$groupNumber",
+ lecturers.firstOrNull() ?: -1L,
+ ))
+ hasValidTeam = true
+ }
+ }
+ return hasValidTeam
+ }
+}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTerms.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTerms.kt
new file mode 100644
index 00000000..1bf47288
--- /dev/null
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTerms.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) Kuba Szczodrzyński 2022-10-15.
+ */
+
+package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api
+
+import com.google.gson.JsonArray
+import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_TERMS
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi
+import pl.szczodrzynski.edziennik.ext.*
+import pl.szczodrzynski.edziennik.utils.models.Date
+
+class UsosApiTerms(
+ override val data: DataUsos,
+ override val lastSync: Long?,
+ val onSuccess: (endpointId: Int) -> Unit,
+) : UsosApi(data, lastSync) {
+ companion object {
+ const val TAG = "UsosApiTerms"
+ }
+
+ init {
+ apiRequest(
+ tag = TAG,
+ service = "terms/search",
+ params = mapOf(
+ "query" to Date.getToday().year.toString(),
+ ),
+ responseType = ResponseType.ARRAY,
+ ) { json, response ->
+ if (!processResponse(json)) {
+ data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response)
+ return@apiRequest
+ }
+
+ data.setSyncNext(ENDPOINT_USOS_API_TERMS, 7 * DAY)
+ onSuccess(ENDPOINT_USOS_API_TERMS)
+ }
+ }
+
+ private fun processResponse(json: JsonArray): Boolean {
+ val dates = mutableSetOf()
+ for (term in json.asJsonObjectList()) {
+ if (!term.getBoolean("is_active", false))
+ continue
+ val startDate = term.getString("start_date")?.let { Date.fromY_m_d(it) }
+ val finishDate = term.getString("finish_date")?.let { Date.fromY_m_d(it) }
+ if (startDate != null)
+ dates += startDate
+ if (finishDate != null)
+ dates += finishDate
+ }
+ val datesSorted = dates.sorted()
+ if (datesSorted.size != 3)
+ return false
+ profile?.studentSchoolYearStart = datesSorted[0].year
+ profile?.dateSemester1Start = datesSorted[0]
+ profile?.dateSemester2Start = datesSorted[1]
+ profile?.dateYearEnd = datesSorted[2]
+ return true
+ }
+}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTimetable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTimetable.kt
new file mode 100644
index 00000000..6c1acc54
--- /dev/null
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTimetable.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright (c) Kuba Szczodrzyński 2022-10-16.
+ */
+
+package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api
+
+import com.google.gson.JsonArray
+import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_TIMETABLE
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi
+import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel
+import pl.szczodrzynski.edziennik.data.db.entity.Lesson
+import pl.szczodrzynski.edziennik.data.db.entity.Metadata
+import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
+import pl.szczodrzynski.edziennik.ext.*
+import pl.szczodrzynski.edziennik.utils.models.Date
+import pl.szczodrzynski.edziennik.utils.models.Time
+import pl.szczodrzynski.edziennik.utils.models.Week
+
+class UsosApiTimetable(
+ override val data: DataUsos,
+ override val lastSync: Long?,
+ val onSuccess: (endpointId: Int) -> Unit,
+) : UsosApi(data, lastSync) {
+ companion object {
+ const val TAG = "UsosApiTimetable"
+ }
+
+ init {
+ val currentWeekStart = Week.getWeekStart()
+ if (Date.getToday().weekDay > 4)
+ currentWeekStart.stepForward(0, 0, 7)
+
+ val weekStart = data.arguments
+ ?.getString("weekStart")
+ ?.let { Date.fromY_m_d(it) }
+ ?: currentWeekStart
+ val weekEnd = weekStart.clone().stepForward(0, 0, 6)
+
+ apiRequest(
+ tag = TAG,
+ service = "tt/user",
+ params = mapOf(
+ "start" to weekStart.stringY_m_d,
+ "days" to 7,
+ ),
+ fields = listOf(
+ "type",
+ "start_time",
+ "end_time",
+ "unit_id",
+ "course_id",
+ "course_name",
+ "lecturer_ids",
+ "building_id",
+ "room_number",
+ "classtype_id",
+ "group_number",
+ ),
+ responseType = ResponseType.ARRAY,
+ ) { json, response ->
+ if (!processResponse(json, weekStart..weekEnd)) {
+ data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response)
+ return@apiRequest
+ }
+
+ data.toRemove.add(DataRemoveModel.Timetable.between(weekStart, weekEnd))
+ data.setSyncNext(ENDPOINT_USOS_API_TIMETABLE, SYNC_ALWAYS)
+ onSuccess(ENDPOINT_USOS_API_TIMETABLE)
+ }
+ }
+
+ private fun processResponse(json: JsonArray, syncRange: ClosedRange): Boolean {
+ val foundDates = mutableSetOf()
+
+ for (activity in json.asJsonObjectList()) {
+ val type = activity.getString("type")
+ if (type !in listOf("classgroup", "classgroup2"))
+ continue
+
+ val startTime = activity.getString("start_time") ?: continue
+ val endTime = activity.getString("end_time") ?: continue
+ val unitId = activity.getLong("unit_id", -1)
+ val courseName = activity.getLangString("course_name") ?: continue
+ val courseId = activity.getString("course_id") ?: continue
+ val lecturerIds = activity.getJsonArray("lecturer_ids")?.map { it.asLong }
+ val buildingId = activity.getString("building_id")
+ val roomNumber = activity.getString("room_number")
+ val classTypeId = activity.getString("classtype_id")
+ val groupNumber = activity.getString("group_number")
+
+ val lesson = Lesson(profileId, -1).also {
+ it.type = Lesson.TYPE_NORMAL
+ it.date = Date.fromY_m_d(startTime)
+ it.startTime = Time.fromY_m_d_H_m_s(startTime)
+ it.endTime = Time.fromY_m_d_H_m_s(endTime)
+ it.subjectId = data.getSubject(
+ id = null,
+ name = courseName,
+ shortName = courseId,
+ ).id
+ it.teacherId = lecturerIds?.firstOrNull() ?: -1L
+ it.teamId = unitId
+ val groupName = classTypeId?.plus(groupNumber)?.let { s -> "($s)" }
+ it.classroom = "$buildingId / $roomNumber ${groupName ?: ""}"
+ it.id = it.buildId()
+
+ it.color = when (classTypeId) {
+ "WYK" -> 0xff0d6091
+ "CW" -> 0xff54306e
+ "LAB" -> 0xff772747
+ "KON" -> 0xff1e5128
+ "^P?SEM" -> 0xff1e5128 // TODO make it regex
+ else -> 0xff08534c
+ }.toInt()
+ }
+ lesson.date?.let { foundDates += it }
+
+ val seen = profile?.empty != false || lesson.date!! < Date.getToday()
+ data.lessonList.add(lesson)
+ if (lesson.type != Lesson.TYPE_NORMAL)
+ data.metadataList += Metadata(
+ profileId,
+ Metadata.TYPE_LESSON_CHANGE,
+ lesson.id,
+ seen,
+ seen,
+ )
+ }
+
+ val notFoundDates = syncRange.asSequence() - foundDates
+ for (date in notFoundDates) {
+ data.lessonList += Lesson(profileId, date.value.toLong()).also {
+ it.type = Lesson.TYPE_NO_LESSONS
+ it.date = date
+ }
+ }
+ return true
+ }
+}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiUser.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiUser.kt
new file mode 100644
index 00000000..29a3e22b
--- /dev/null
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiUser.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) Kuba Szczodrzyński 2022-10-16.
+ */
+
+package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api
+
+import com.google.gson.JsonObject
+import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_NO_STUDENT_PROGRAMMES
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_USER
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi
+import pl.szczodrzynski.edziennik.data.api.models.ApiError
+import pl.szczodrzynski.edziennik.ext.*
+
+class UsosApiUser(
+ override val data: DataUsos,
+ override val lastSync: Long?,
+ val onSuccess: (endpointId: Int) -> Unit,
+) : UsosApi(data, lastSync) {
+ companion object {
+ const val TAG = "UsosApiUser"
+ }
+
+ init {
+ apiRequest(
+ tag = TAG,
+ service = "users/user",
+ params = mapOf(
+ "fields" to listOf(
+ "id",
+ "first_name",
+ "last_name",
+ "student_number",
+ "student_programmes" to listOf(
+ "programme" to listOf("id"),
+ ),
+ ),
+ ),
+ responseType = ResponseType.OBJECT,
+ ) { json, response ->
+ val programmes = json.getJsonArray("student_programmes")
+ if (programmes.isNullOrEmpty()) {
+ data.error(ApiError(TAG, ERROR_USOS_NO_STUDENT_PROGRAMMES)
+ .withApiResponse(json)
+ .withResponse(response))
+ return@apiRequest
+ }
+
+ val firstName = json.getString("first_name")
+ val lastName = json.getString("last_name")
+ val studentName = buildFullName(firstName, lastName)
+
+ data.studentId = json.getInt("id") ?: data.studentId
+ profile?.studentNameLong = studentName
+ profile?.studentNameShort = studentName.getShortName()
+ profile?.studentNumber = json.getInt("student_number", -1)
+ profile?.studentClassName = programmes.getJsonObject(0).getJsonObject("programme").getString("id")
+
+ profile?.studentClassName?.let {
+ data.getTeam(
+ id = null,
+ name = it,
+ schoolCode = data.schoolId ?: "",
+ isTeamClass = true,
+ )
+ }
+
+ data.setSyncNext(ENDPOINT_USOS_API_USER, 4 * DAY)
+ onSuccess(ENDPOINT_USOS_API_USER)
+ }
+ }
+}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/firstlogin/UsosFirstLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/firstlogin/UsosFirstLogin.kt
new file mode 100644
index 00000000..4cb38cc6
--- /dev/null
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/firstlogin/UsosFirstLogin.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) Kuba Szczodrzyński 2022-10-14.
+ */
+
+package pl.szczodrzynski.edziennik.data.api.edziennik.usos.firstlogin
+
+import com.google.gson.JsonObject
+import org.greenrobot.eventbus.EventBus
+import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_NO_STUDENT_PROGRAMMES
+import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_USOS
+import pl.szczodrzynski.edziennik.data.api.edziennik.librus.firstlogin.LibrusFirstLogin
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.login.UsosLoginApi
+import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent
+import pl.szczodrzynski.edziennik.data.api.models.ApiError
+import pl.szczodrzynski.edziennik.data.db.entity.Profile
+import pl.szczodrzynski.edziennik.ext.*
+
+class UsosFirstLogin(val data: DataUsos, val onSuccess: () -> Unit) {
+ companion object {
+ private const val TAG = "UsosFirstLogin"
+ }
+
+ private val api = UsosApi(data, null)
+
+ init {
+ val loginStoreId = data.loginStore.id
+ val loginStoreType = LOGIN_TYPE_USOS
+ var firstProfileId = loginStoreId
+
+ UsosLoginApi(data) {
+ api.apiRequest(
+ tag = TAG,
+ service = "users/user",
+ params = mapOf(
+ "fields" to listOf(
+ "id",
+ "first_name",
+ "last_name",
+ "student_number",
+ "student_programmes" to listOf(
+ "programme" to listOf("id"),
+ ),
+ ),
+ ),
+ responseType = UsosApi.ResponseType.OBJECT,
+ ) { json, response ->
+ val programmes = json.getJsonArray("student_programmes")
+ if (programmes.isNullOrEmpty()) {
+ data.error(ApiError(TAG, ERROR_USOS_NO_STUDENT_PROGRAMMES)
+ .withApiResponse(json)
+ .withResponse(response))
+ return@apiRequest
+ }
+
+ val firstName = json.getString("first_name")
+ val lastName = json.getString("last_name")
+ val studentName = buildFullName(firstName, lastName)
+
+ val profile = Profile(
+ id = firstProfileId++,
+ loginStoreId = loginStoreId, loginStoreType = loginStoreType,
+ name = studentName,
+ subname = data.schoolId,
+ studentNameLong = studentName,
+ studentNameShort = studentName.getShortName(),
+ accountName = null, // student account
+ studentData = JsonObject(
+ "studentId" to json.getInt("id"),
+ ),
+ ).also {
+ it.studentNumber = json.getInt("student_number", -1)
+ it.studentClassName = programmes.getJsonObject(0).getJsonObject("programme").getString("id")
+ }
+
+ EventBus.getDefault().postSticky(
+ FirstLoginFinishedEvent(listOf(profile), data.loginStore),
+ )
+ onSuccess()
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLogin.kt
new file mode 100644
index 00000000..599b8f16
--- /dev/null
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLogin.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) Kuba Szczodrzyński 2022-10-11.
+ */
+
+package pl.szczodrzynski.edziennik.data.api.edziennik.usos.login
+
+import pl.szczodrzynski.edziennik.R
+import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_USOS_API
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
+import pl.szczodrzynski.edziennik.utils.Utils.d
+
+class UsosLogin(val data: DataUsos, val onSuccess: () -> Unit) {
+ companion object {
+ private const val TAG = "UsosLogin"
+ }
+
+ private var cancelled = false
+
+ init {
+ nextLoginMethod(onSuccess)
+ }
+
+ private fun nextLoginMethod(onSuccess: () -> Unit) {
+ if (data.targetLoginMethodIds.isEmpty()) {
+ onSuccess()
+ return
+ }
+ if (cancelled) {
+ onSuccess()
+ return
+ }
+ useLoginMethod(data.targetLoginMethodIds.removeAt(0)) { usedMethodId ->
+ data.progress(data.progressStep)
+ if (usedMethodId != -1)
+ data.loginMethods.add(usedMethodId)
+ nextLoginMethod(onSuccess)
+ }
+ }
+
+ private fun useLoginMethod(loginMethodId: Int, onSuccess: (usedMethodId: Int) -> Unit) {
+ // this should never be true
+ if (data.loginMethods.contains(loginMethodId)) {
+ onSuccess(-1)
+ return
+ }
+ d(TAG, "Using login method $loginMethodId")
+ when (loginMethodId) {
+ LOGIN_METHOD_USOS_API -> {
+ data.startProgress(R.string.edziennik_progress_login_usos_api)
+ UsosLoginApi(data) { onSuccess(loginMethodId) }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt
new file mode 100644
index 00000000..06651477
--- /dev/null
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) Kuba Szczodrzyński 2022-10-11.
+ */
+
+package pl.szczodrzynski.edziennik.data.api.edziennik.usos.login
+
+import pl.szczodrzynski.edziennik.R
+import pl.szczodrzynski.edziennik.data.api.*
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
+import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi
+import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
+import pl.szczodrzynski.edziennik.data.api.models.ApiError
+import pl.szczodrzynski.edziennik.ext.*
+import pl.szczodrzynski.edziennik.utils.Utils.d
+
+class UsosLoginApi(val data: DataUsos, val onSuccess: () -> Unit) {
+ companion object {
+ private const val TAG = "UsosLoginApi"
+ }
+
+ private val api = UsosApi(data, null)
+
+ init {
+ run {
+ data.arguments?.getString("oauthLoginResponse")?.let {
+ data.oauthLoginResponse = it
+ }
+ if (data.isApiLoginValid()) {
+ onSuccess()
+ } else if (data.oauthLoginResponse != null) {
+ login()
+ } else {
+ authorize()
+ }
+ }
+ }
+
+ private fun authorize() {
+ data.oauthTokenKey = null
+ data.oauthTokenSecret = null
+ api.apiRequest(
+ tag = TAG,
+ service = "oauth/request_token",
+ params = mapOf(
+ "oauth_callback" to USOS_API_OAUTH_REDIRECT_URL,
+ "scopes" to USOS_API_SCOPES,
+ ),
+ responseType = UsosApi.ResponseType.PLAIN,
+ ) { text, _ ->
+ val authorizeData = text.fromQueryString()
+ data.oauthTokenKey = authorizeData["oauth_token"]
+ data.oauthTokenSecret = authorizeData["oauth_token_secret"]
+ data.oauthTokenIsUser = false
+
+ val authUrl = "${data.instanceUrl}services/oauth/authorize"
+ val authParams = mapOf(
+ "interactivity" to "confirm_user",
+ "oauth_token" to (data.oauthTokenKey ?: ""),
+ )
+ data.requireUserAction(
+ type = UserActionRequiredEvent.Type.OAUTH,
+ params = Bundle(
+ "authorizeUrl" to "$authUrl?${authParams.toQueryString()}",
+ "redirectUrl" to USOS_API_OAUTH_REDIRECT_URL,
+ "responseStoreKey" to "oauthLoginResponse",
+ "extras" to data.loginStore.data.toBundle(),
+ ),
+ errorText = R.string.notification_user_action_required_oauth_usos,
+ )
+ }
+ }
+
+ private fun login() {
+ d(TAG, "Login to ${data.schoolId} with ${data.oauthLoginResponse}")
+
+ val authorizeResponse = data.oauthLoginResponse?.fromQueryString()
+ ?: return // checked in init {}
+ if (authorizeResponse["oauth_token"] != data.oauthTokenKey) {
+ // got different token
+ data.error(ApiError(TAG, ERROR_USOS_OAUTH_GOT_DIFFERENT_TOKEN)
+ .withApiResponse(data.oauthLoginResponse))
+ return
+ }
+ val verifier = authorizeResponse["oauth_verifier"]
+ if (verifier.isNullOrBlank()) {
+ data.error(ApiError(TAG, ERROR_USOS_OAUTH_INCOMPLETE_RESPONSE)
+ .withApiResponse(data.oauthLoginResponse))
+ return
+ }
+
+ api.apiRequest(
+ tag = TAG,
+ service = "oauth/access_token",
+ params = mapOf(
+ "oauth_verifier" to verifier,
+ ),
+ responseType = UsosApi.ResponseType.PLAIN,
+ ) { text, response ->
+ val accessData = text.fromQueryString()
+ data.oauthTokenKey = accessData["oauth_token"]
+ data.oauthTokenSecret = accessData["oauth_token_secret"]
+ data.oauthTokenIsUser = data.oauthTokenKey != null && data.oauthTokenSecret != null
+ data.loginStore.removeLoginData("oauthLoginResponse")
+
+ if (!data.oauthTokenIsUser)
+ data.error(ApiError(TAG, ERROR_USOS_OAUTH_INCOMPLETE_RESPONSE)
+ .withApiResponse(text)
+ .withResponse(response))
+ else
+ onSuccess()
+ }
+ }
+}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/Vulcan.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/Vulcan.kt
index 64c87bf6..37af4b69 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/Vulcan.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/Vulcan.kt
@@ -17,6 +17,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLogin
import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent
import pl.szczodrzynski.edziennik.data.api.events.EventGetEvent
import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent
+import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError
@@ -179,6 +180,7 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va
private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback {
return object : EdziennikCallback {
override fun onCompleted() { callback.onCompleted() }
+ override fun onRequiresUserAction(event: UserActionRequiredEvent) { callback.onRequiresUserAction(event) }
override fun onProgress(step: Float) { callback.onProgress(step) }
override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) }
override fun onError(apiError: ApiError) {
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/UserActionRequiredEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/UserActionRequiredEvent.kt
index 49b510c2..3995842a 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/UserActionRequiredEvent.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/UserActionRequiredEvent.kt
@@ -4,11 +4,16 @@
package pl.szczodrzynski.edziennik.data.api.events
-data class UserActionRequiredEvent(val profileId: Int, val type: Int) {
- companion object {
- const val LOGIN_DATA_MOBIDZIENNIK = 101
- const val LOGIN_DATA_LIBRUS = 102
- const val LOGIN_DATA_VULCAN = 104
- const val CAPTCHA_LIBRUS = 202
+import android.os.Bundle
+
+data class UserActionRequiredEvent(
+ val profileId: Int?,
+ val type: Type,
+ val params: Bundle,
+ val errorText: Int,
+) {
+ enum class Type {
+ RECAPTCHA,
+ OAUTH,
}
}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikCallback.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikCallback.kt
index c1c878b4..4943ff1e 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikCallback.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikCallback.kt
@@ -4,6 +4,7 @@
package pl.szczodrzynski.edziennik.data.api.interfaces
+import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.models.Feature
import pl.szczodrzynski.edziennik.data.api.models.LoginMethod
@@ -14,4 +15,5 @@ import pl.szczodrzynski.edziennik.data.api.models.LoginMethod
*/
interface EdziennikCallback : EndpointCallback {
fun onCompleted()
+ fun onRequiresUserAction(event: UserActionRequiredEvent)
}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/ApiError.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/ApiError.kt
index bb5d20cc..b631ed7d 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/ApiError.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/ApiError.kt
@@ -5,6 +5,7 @@
package pl.szczodrzynski.edziennik.data.api.models
import android.content.Context
+import android.os.Bundle
import com.google.gson.JsonObject
import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
@@ -30,6 +31,7 @@ class ApiError(val tag: String, var errorCode: Int) {
var request: Request? = null
var response: Response? = null
var isCritical = true
+ var params: Bundle? = null
fun withThrowable(throwable: Throwable?): ApiError {
this.throwable = throwable
@@ -58,6 +60,11 @@ class ApiError(val tag: String, var errorCode: Int) {
return this
}
+ fun withParams(bundle: Bundle): ApiError {
+ this.params = bundle
+ return this
+ }
+
fun getStringText(context: Context): String {
return context.resources.getIdentifier("error_${errorCode}", "string", context.packageName).let {
if (it != 0)
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt
index ea53112a..9b7137fc 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt
@@ -1,5 +1,6 @@
package pl.szczodrzynski.edziennik.data.api.models
+import android.os.Bundle
import android.util.LongSparseArray
import android.util.SparseArray
import androidx.core.util.set
@@ -12,7 +13,8 @@ import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.ERROR_REQUEST_FAILURE
import pl.szczodrzynski.edziennik.data.api.Regexes.MESSAGE_META
-import pl.szczodrzynski.edziennik.data.api.interfaces.EndpointCallback
+import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
+import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.db.AppDb
import pl.szczodrzynski.edziennik.data.db.entity.*
import pl.szczodrzynski.edziennik.ext.*
@@ -37,7 +39,7 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
/**
* A callback passed to all [Feature]s and [LoginMethod]s
*/
- lateinit var callback: EndpointCallback
+ lateinit var callback: EdziennikCallback
/**
* A list of [LoginMethod]s *already fulfilled* during this sync.
@@ -374,6 +376,15 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
callback.onError(apiError)
}
+ fun requireUserAction(type: UserActionRequiredEvent.Type, params: Bundle, errorText: Int) {
+ callback.onRequiresUserAction(UserActionRequiredEvent(
+ profileId = profile?.id,
+ type = type,
+ params = params,
+ errorText = errorText,
+ ))
+ }
+
fun progress(step: Float) {
callback.onProgress(step)
}
@@ -438,14 +449,14 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
return team
}
- fun getTeacher(firstName: String, lastName: String, loginId: String? = null): Teacher {
+ fun getTeacher(firstName: String, lastName: String, loginId: String? = null, id: Long? = null): Teacher {
val teacher = teacherList.singleOrNull { it.fullName == "$firstName $lastName" }
- return validateTeacher(teacher, firstName, lastName, loginId)
+ return validateTeacher(teacher, firstName, lastName, loginId, id)
}
fun getTeacher(firstNameChar: Char, lastName: String, loginId: String? = null): Teacher {
val teacher = teacherList.singleOrNull { it.shortName == "$firstNameChar.$lastName" }
- return validateTeacher(teacher, firstNameChar.toString(), lastName, loginId)
+ return validateTeacher(teacher, firstNameChar.toString(), lastName, loginId, null)
}
fun getTeacherByLastFirst(nameLastFirst: String, loginId: String? = null): Teacher {
@@ -453,9 +464,9 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
val teacher = teacherList.singleOrNull { it.fullNameLastFirst == nameLastFirst }
val nameParts = nameLastFirst.split(" ", limit = 2)
return if (nameParts.size == 1)
- validateTeacher(teacher, nameParts[0], "", loginId)
+ validateTeacher(teacher, nameParts[0], "", loginId, null)
else
- validateTeacher(teacher, nameParts[1], nameParts[0], loginId)
+ validateTeacher(teacher, nameParts[1], nameParts[0], loginId, null)
}
fun getTeacherByFirstLast(nameFirstLast: String, loginId: String? = null): Teacher {
@@ -463,9 +474,9 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
val teacher = teacherList.singleOrNull { it.fullName == nameFirstLast }
val nameParts = nameFirstLast.split(" ", limit = 2)
return if (nameParts.size == 1)
- validateTeacher(teacher, nameParts[0], "", loginId)
+ validateTeacher(teacher, nameParts[0], "", loginId, null)
else
- validateTeacher(teacher, nameParts[0], nameParts[1], loginId)
+ validateTeacher(teacher, nameParts[0], nameParts[1], loginId, null)
}
fun getTeacherByFDotLast(nameFDotLast: String, loginId: String? = null): Teacher {
@@ -484,10 +495,16 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
getTeacher(nameParts[0][0], nameParts[1], loginId)
}
- private fun validateTeacher(teacher: Teacher?, firstName: String, lastName: String, loginId: String?): Teacher {
- val obj = teacher ?: Teacher(profileId, -1, firstName, lastName, loginId).apply {
- id = fullName.crc32()
- teacherList[id] = this
+ private fun validateTeacher(
+ teacher: Teacher?,
+ firstName: String,
+ lastName: String,
+ loginId: String?,
+ id: Long?
+ ): Teacher {
+ val obj = teacher ?: Teacher(profileId, -1, firstName, lastName, loginId).also {
+ it.id = id ?: it.fullName.crc32()
+ teacherList[it.id] = it
}
return obj.also {
if (loginId != null)
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt
index c86ae235..a9f6a69f 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt
@@ -451,9 +451,9 @@ class SzkolnyApi(val app: App) : CoroutineScope {
@Throws(Exception::class)
fun getRealms(registerName: String): List {
- val response = api.fsLoginRealms(registerName).execute()
+ val response = api.platforms(registerName).execute()
if (response.isSuccessful && response.body() != null) {
- return response.body()!!
+ return parseResponse(response)
}
throw SzkolnyApiException(null)
}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyService.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyService.kt
index 5c60c3ff..cca9475a 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyService.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyService.kt
@@ -45,6 +45,6 @@ interface SzkolnyService {
@GET("registerAvailability")
fun registerAvailability(): Call>>
- @GET("https://szkolny-eu.github.io/FSLogin/realms/{registerName}.json")
- fun fsLoginRealms(@Path("registerName") registerName: String): Call>
+ @GET("platforms/{registerName}")
+ fun platforms(@Path("registerName") registerName: String): Call>>
}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt
index 93758366..b7f6ccf2 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt
@@ -44,7 +44,7 @@ import pl.szczodrzynski.edziennik.data.db.migration.*
TimetableManual::class,
Note::class,
Metadata::class
-], version = 97)
+], version = 98)
@TypeConverters(
ConverterTime::class,
ConverterDate::class,
@@ -185,6 +185,7 @@ abstract class AppDb : RoomDatabase() {
Migration95(),
Migration96(),
Migration97(),
+ Migration98(),
).allowMainThreadQueries().build()
}
}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Lesson.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Lesson.kt
index 5123a2f5..37b7f6f3 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Lesson.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Lesson.kt
@@ -74,6 +74,8 @@ open class Lesson(
@Ignore
var showAsUnseen = false
+ var color: Int? = null
+
override fun toString(): String {
return "Lesson(profileId=$profileId, " +
"id=$id, " +
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt
index 9fc0521a..b1615fbc 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt
@@ -233,6 +233,10 @@ open class Profile(
MainActivity.DRAWER_ITEM_GRADES,
MainActivity.DRAWER_ITEM_HOMEWORK
)
+ LOGIN_TYPE_USOS -> listOf(
+ MainActivity.DRAWER_ITEM_TIMETABLE,
+ MainActivity.DRAWER_ITEM_AGENDA
+ )
else -> listOf(
MainActivity.DRAWER_ITEM_TIMETABLE,
MainActivity.DRAWER_ITEM_AGENDA,
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration98.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration98.kt
new file mode 100644
index 00000000..91003fd9
--- /dev/null
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration98.kt
@@ -0,0 +1,15 @@
+/*
+ * Copyright (c) Kuba Szczodrzyński 2022-10-16.
+ */
+
+package pl.szczodrzynski.edziennik.data.db.migration
+
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+
+class Migration98 : Migration(97, 98) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ // timetable colors - override color in lesson object
+ database.execSQL("ALTER TABLE timetable ADD COLUMN color INT DEFAULT NULL;")
+ }
+}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/BundleExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/BundleExtensions.kt
index cc6488bd..037d78f5 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ext/BundleExtensions.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/BundleExtensions.kt
@@ -22,6 +22,15 @@ fun Bundle?.getFloat(key: String, defaultValue: Float): Float {
fun Bundle?.getString(key: String, defaultValue: String): String {
return this?.getString(key, defaultValue) ?: defaultValue
}
+inline fun > Bundle?.getEnum(key: String): E? {
+ return this?.getString(key)?.let {
+ try {
+ enumValueOf(it)
+ } catch (e: Exception) {
+ null
+ }
+ }
+}
fun Bundle?.getIntOrNull(key: String): Int? {
return this?.get(key) as? Int
@@ -48,6 +57,7 @@ fun Bundle(vararg properties: Pair): Bundle {
is Bundle -> putBundle(property.first, property.second as Bundle)
is Parcelable -> putParcelable(property.first, property.second as Parcelable)
is Array<*> -> putParcelableArray(property.first, property.second as Array)
+ is Enum<*> -> putString(property.first, (property.second as Enum<*>).name)
}
}
}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/JsonExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/JsonExtensions.kt
index 9f0db8ad..14d25b48 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ext/JsonExtensions.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/JsonExtensions.kt
@@ -10,6 +10,8 @@ import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.google.gson.JsonPrimitive
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
fun JsonObject?.get(key: String): JsonElement? = this?.get(key)
@@ -93,7 +95,13 @@ fun JsonArray(vararg properties: Any?): JsonArray {
}
}
-fun JsonArray?.isNullOrEmpty(): Boolean = (this?.size() ?: 0) == 0
+@OptIn(ExperimentalContracts::class)
+fun JsonArray?.isNullOrEmpty(): Boolean {
+ contract {
+ returns(false) implies (this@isNullOrEmpty != null)
+ }
+ return this == null || this.isEmpty
+}
operator fun JsonArray.plusAssign(o: JsonElement) = this.add(o)
operator fun JsonArray.plusAssign(o: String) = this.add(o)
operator fun JsonArray.plusAssign(o: Char) = this.add(o)
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt
index ebfb1410..adb6fe55 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt
@@ -18,6 +18,8 @@ import android.text.style.StyleSpan
import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
import com.mikepenz.materialdrawer.holder.StringHolder
+import java.net.URLDecoder
+import java.net.URLEncoder
fun CharSequence?.isNotNullNorEmpty(): Boolean {
return this != null && this.isNotEmpty()
@@ -343,3 +345,17 @@ fun Int.toStringHolder() = StringHolder(this)
fun CharSequence.toStringHolder() = StringHolder(this)
fun @receiver:StringRes Int.resolveString(context: Context) = context.getString(this)
+
+fun String.urlEncode(): String = URLEncoder.encode(this, "UTF-8").replace("+", "%20")
+fun String.urlDecode(): String = URLDecoder.decode(this, "UTF-8")
+
+fun Map.toQueryString() = this
+ .map { it.key.urlEncode() to it.value.urlEncode() }
+ .sortedBy { it.first }
+ .joinToString("&") { "${it.first}=${it.second}" }
+
+fun String.fromQueryString() = this
+ .substringAfter('?')
+ .split("&")
+ .map { it.split("=") }
+ .associate { it[0].urlDecode() to it[1].urlDecode() }
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/TimeExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/TimeExtensions.kt
index 114a5a40..31bd952c 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ext/TimeExtensions.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/TimeExtensions.kt
@@ -7,9 +7,10 @@ package pl.szczodrzynski.edziennik.ext
import android.content.Context
import im.wangchao.mhttp.Response
import pl.szczodrzynski.edziennik.R
+import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import java.text.SimpleDateFormat
-import java.util.*
+import java.util.Locale
const val MINUTE = 60L
const val HOUR = 60L*MINUTE
@@ -115,3 +116,11 @@ fun Context.getSyncInterval(interval: Int): String {
""
return hoursText?.plus(" $minutesText") ?: minutesText
}
+
+fun ClosedRange.asSequence(): Sequence = sequence {
+ val date = this@asSequence.start.clone()
+ while (date in this@asSequence) {
+ yield(date.clone())
+ date.stepForward(0, 0, 1)
+ }
+}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/LibrusCaptchaDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaPromptDialog.kt
similarity index 88%
rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/LibrusCaptchaDialog.kt
rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaPromptDialog.kt
index 12ad0e37..a927347d 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/LibrusCaptchaDialog.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaPromptDialog.kt
@@ -13,14 +13,16 @@ import pl.szczodrzynski.edziennik.databinding.RecaptchaViewBinding
import pl.szczodrzynski.edziennik.ext.onClick
import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog
-class LibrusCaptchaDialog(
+class RecaptchaPromptDialog(
activity: AppCompatActivity,
+ private val siteKey: String,
+ private val referer: String,
private val onSuccess: (recaptchaCode: String) -> Unit,
- private val onFailure: (() -> Unit)?,
+ private val onCancel: (() -> Unit)?,
onShowListener: ((tag: String) -> Unit)? = null,
onDismissListener: ((tag: String) -> Unit)? = null,
) : BindingDialog(activity, onShowListener, onDismissListener) {
- override val TAG = "LibrusCaptchaDialog"
+ override val TAG = "RecaptchaPromptDialog"
override fun getTitleRes(): Int? = null
override fun inflate(layoutInflater: LayoutInflater) =
@@ -46,8 +48,8 @@ class LibrusCaptchaDialog(
b.progress.visibility = View.VISIBLE
RecaptchaDialog(
activity,
- siteKey = "6Lf48moUAAAAAB9ClhdvHr46gRWR-CN31CXQPG2U",
- referer = "https://portal.librus.pl/rodzina/login",
+ siteKey = siteKey,
+ referer = referer,
onSuccess = { recaptchaCode ->
b.checkbox.background = checkboxBackground
b.checkbox.foreground = checkboxForeground
@@ -67,6 +69,6 @@ class LibrusCaptchaDialog(
override fun onDismiss() {
if (!success)
- onFailure?.invoke()
+ onCancel?.invoke()
}
}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeDebugCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeDebugCard.kt
index 329a677a..6947b74b 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeDebugCard.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeDebugCard.kt
@@ -27,7 +27,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.databinding.CardHomeDebugBinding
import pl.szczodrzynski.edziennik.ext.dp
import pl.szczodrzynski.edziennik.ext.onClick
-import pl.szczodrzynski.edziennik.ui.captcha.LibrusCaptchaDialog
+import pl.szczodrzynski.edziennik.ui.captcha.RecaptchaPromptDialog
import pl.szczodrzynski.edziennik.ui.home.HomeCard
import pl.szczodrzynski.edziennik.ui.home.HomeCardAdapter
import pl.szczodrzynski.edziennik.ui.home.HomeFragment
@@ -85,11 +85,6 @@ class HomeDebugCard(
app.startActivity(Chucker.getLaunchIntent(activity, 1));
}
- b.librusCaptchaButton.onClick {
- //app.startActivity(Intent(activity, LoginLibrusCaptchaActivity::class.java))
- LibrusCaptchaDialog(activity, onSuccess = {}, onFailure = {}).show()
- }
-
b.getLogs.onClick {
val logs = HyperLog.getDeviceLogsInFile(activity, true)
val intent = Intent(Intent.ACTION_SEND)
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginActivity.kt
index 61aa4520..08d4917d 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginActivity.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginActivity.kt
@@ -31,6 +31,7 @@ class LoginActivity : AppCompatActivity(), CoroutineScope {
private val app: App by lazy { applicationContext as App }
private lateinit var b: LoginActivityBinding
lateinit var navOptions: NavOptions
+ lateinit var navOptionsBuilder: NavOptions.Builder
val nav by lazy { Navigation.findNavController(this, R.id.nav_host_fragment) }
val errorSnackbar: ErrorSnackbar by lazy { ErrorSnackbar(this) }
val swipeRefreshLayout: SwipeRefreshLayoutNoTouch by lazy { b.swipeRefreshLayout }
@@ -87,12 +88,12 @@ class LoginActivity : AppCompatActivity(), CoroutineScope {
super.onCreate(savedInstanceState)
setTheme(R.style.AppTheme_Light)
- navOptions = NavOptions.Builder()
+ navOptionsBuilder = NavOptions.Builder()
.setEnterAnim(R.anim.slide_in_right)
.setExitAnim(R.anim.slide_out_left)
.setPopEnterAnim(R.anim.slide_in_left)
.setPopExitAnim(R.anim.slide_out_right)
- .build()
+ navOptions = navOptionsBuilder.build()
b = LoginActivityBinding.inflate(layoutInflater)
setContentView(b.root)
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginFormFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginFormFragment.kt
index 3c5a26ca..12daadcd 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginFormFragment.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginFormFragment.kt
@@ -14,6 +14,8 @@ import android.widget.Toast
import androidx.core.view.isVisible
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
+import androidx.navigation.NavOptions
+import androidx.navigation.navOptions
import androidx.viewbinding.ViewBinding
import com.google.android.material.textfield.TextInputLayout
import com.mikepenz.iconics.IconicsDrawable
@@ -33,7 +35,6 @@ import pl.szczodrzynski.edziennik.ui.login.LoginInfo.BaseCredential
import pl.szczodrzynski.edziennik.ui.login.LoginInfo.FormCheckbox
import pl.szczodrzynski.edziennik.ui.login.LoginInfo.FormField
import pl.szczodrzynski.navlib.colorAttr
-import java.util.*
import kotlin.coroutines.CoroutineContext
class LoginFormFragment : Fragment(), CoroutineScope {
@@ -63,8 +64,10 @@ class LoginFormFragment : Fragment(), CoroutineScope {
get() = arguments?.getString("platformDescription")
private val platformFormFields
get() = arguments?.getString("platformFormFields")?.split(";")
- private val platformRealmData
- get() = arguments?.getString("platformRealmData")?.toJsonObject()
+ private val platformData
+ get() = arguments?.getString("platformData")?.toJsonObject()
+ private val platformStoreKey
+ get() = arguments?.getString("platformStoreKey")
override fun onCreateView(
inflater: LayoutInflater,
@@ -90,6 +93,11 @@ class LoginFormFragment : Fragment(), CoroutineScope {
val loginMode = arguments?.getInt("loginMode") ?: return
val mode = register.loginModes.firstOrNull { it.loginMode == loginMode } ?: return
+ if (mode.credentials.isEmpty()) {
+ login(loginType, loginMode)
+ return
+ }
+
b.title.setText(R.string.login_form_title_format, app.getString(register.registerName))
b.subTitle.text = platformName ?: app.getString(mode.name)
b.text.text = platformGuideText ?: app.getString(mode.guideText)
@@ -250,7 +258,10 @@ class LoginFormFragment : Fragment(), CoroutineScope {
payload.putBoolean("fakeLogin", true)
}
- payload.putBundle("webRealmData", platformRealmData?.toBundle())
+ if (platformStoreKey == null)
+ payload.putAll(platformData?.toBundle() ?: Bundle())
+ else
+ payload.putBundle(platformStoreKey, platformData?.toBundle())
var hasErrors = false
credentials.forEach { (credential, b) ->
@@ -295,6 +306,14 @@ class LoginFormFragment : Fragment(), CoroutineScope {
if (hasErrors)
return
- nav.navigate(R.id.loginProgressFragment, payload, activity.navOptions)
+ val navOptions =
+ if (credentials.isEmpty())
+ activity.navOptionsBuilder
+ .setPopUpTo(R.id.loginPlatformListFragment, inclusive = false)
+ .build()
+ else
+ activity.navOptions
+
+ nav.navigate(R.id.loginProgressFragment, payload, navOptions)
}
}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginInfo.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginInfo.kt
index 0448b514..774530f1 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginInfo.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginInfo.kt
@@ -6,6 +6,7 @@ package pl.szczodrzynski.edziennik.ui.login
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
+import com.google.gson.JsonObject
import com.mikepenz.iconics.typeface.IIcon
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import pl.szczodrzynski.edziennik.R
@@ -64,7 +65,6 @@ object LoginInfo {
errorCodes = mapOf(
ERROR_LOGIN_LIBRUS_PORTAL_NOT_ACTIVATED to R.string.login_error_account_not_activated,
ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN to R.string.login_error_incorrect_login_or_password,
- ERROR_CAPTCHA_LIBRUS_PORTAL to R.string.error_3001_reason
)
),
/*Mode(
@@ -313,7 +313,24 @@ object LoginInfo {
errorCodes = mapOf()
)
)
- )
+ ),
+ Register(
+ loginType = LOGIN_TYPE_USOS,
+ internalName = "usos",
+ registerName = R.string.login_type_usos,
+ registerLogo = R.drawable.login_logo_usos,
+ loginModes = listOf(
+ Mode(
+ loginMode = LOGIN_MODE_USOS_OAUTH,
+ name = R.string.login_mode_usos_oauth,
+ icon = R.drawable.login_mode_usos_api,
+ guideText = R.string.login_mode_usos_oauth_guide,
+ isPlatformSelection = true,
+ credentials = listOf(),
+ errorCodes = mapOf(),
+ ),
+ ),
+ ),
)
}
@@ -357,7 +374,8 @@ object LoginInfo {
val icon: String,
val screenshot: String?,
val formFields: List,
- val realmData: RealmData
+ val data: JsonObject,
+ val storeKey: String?,
)
open class BaseCredential(
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginPlatformListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginPlatformListFragment.kt
index 8a2bb25d..5629487f 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginPlatformListFragment.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginPlatformListFragment.kt
@@ -68,7 +68,8 @@ class LoginPlatformListFragment : Fragment(), CoroutineScope {
"platformName" to platform.name,
"platformDescription" to platform.description,
"platformFormFields" to platform.formFields.joinToString(";"),
- "platformRealmData" to app.gson.toJson(platform.realmData)
+ "platformData" to platform.data.toString(),
+ "platformStoreKey" to platform.storeKey,
), activity.navOptions)
}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginProgressFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginProgressFragment.kt
index 03abbc2b..87a01008 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginProgressFragment.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginProgressFragment.kt
@@ -19,7 +19,7 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
-import pl.szczodrzynski.edziennik.data.api.ERROR_CAPTCHA_NEEDED
+import pl.szczodrzynski.edziennik.data.api.ERROR_REQUIRES_USER_ACTION
import pl.szczodrzynski.edziennik.data.api.LOGIN_NO_ARGUMENTS
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.events.ApiTaskErrorEvent
@@ -29,6 +29,7 @@ import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.databinding.LoginProgressFragmentBinding
import pl.szczodrzynski.edziennik.ext.joinNotNullStrings
+import pl.szczodrzynski.edziennik.utils.managers.UserActionManager
import kotlin.coroutines.CoroutineContext
import kotlin.math.max
@@ -137,14 +138,21 @@ class LoginProgressFragment : Fragment(), CoroutineScope {
return
}
- app.userActionManager.execute(activity, event.profileId, event.type, onSuccess = { code ->
- args.putString("recaptchaCode", code)
- args.putLong("recaptchaTime", System.currentTimeMillis())
- doFirstLogin(args)
- }, onFailure = {
- activity.error(ApiError(TAG, ERROR_CAPTCHA_NEEDED))
- nav.navigateUp()
- })
+ val callback = UserActionManager.UserActionCallback(
+ onSuccess = { data ->
+ args.putAll(data)
+ doFirstLogin(args)
+ },
+ onFailure = {
+ activity.error(ApiError(TAG, ERROR_REQUIRES_USER_ACTION))
+ nav.navigateUp()
+ },
+ onCancel = {
+ nav.navigateUp()
+ },
+ )
+
+ app.userActionManager.execute(activity, event, callback)
}
override fun onStart() {
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/oauth/OAuthLoginActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/oauth/OAuthLoginActivity.kt
new file mode 100644
index 00000000..c80c3d82
--- /dev/null
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/oauth/OAuthLoginActivity.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) Kuba Szczodrzyński 2022-10-15.
+ */
+
+package pl.szczodrzynski.edziennik.ui.login.oauth
+
+import android.annotation.SuppressLint
+import android.graphics.Bitmap
+import android.os.Bundle
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import android.widget.FrameLayout
+import androidx.appcompat.app.AppCompatActivity
+import org.greenrobot.eventbus.EventBus
+import pl.szczodrzynski.edziennik.R
+import pl.szczodrzynski.edziennik.utils.Utils.d
+
+class OAuthLoginActivity : AppCompatActivity() {
+ companion object {
+ private const val TAG = "OAuthLoginActivity"
+ }
+
+ private var isSuccessful = false
+
+ @SuppressLint("SetJavaScriptEnabled")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setTitle(R.string.oauth_dialog_title)
+
+ val authorizeUrl = intent.getStringExtra("authorizeUrl") ?: return
+ val redirectUrl = intent.getStringExtra("redirectUrl") ?: return
+
+ val webView = WebView(this)
+ webView.webViewClient = object : WebViewClient() {
+ override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
+ d(TAG, "Navigating to $url")
+ if (url.startsWith(redirectUrl)) {
+ isSuccessful = true
+ EventBus.getDefault().post(OAuthLoginResult(
+ isError = false,
+ responseUrl = url,
+ ))
+ finish()
+ }
+ }
+ }
+ webView.settings.javaScriptEnabled = true
+ webView.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
+ setContentView(webView)
+
+ webView.loadUrl(authorizeUrl)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ if (!isSuccessful)
+ EventBus.getDefault().post(OAuthLoginResult(
+ isError = false,
+ responseUrl = null,
+ ))
+ }
+}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/oauth/OAuthLoginResult.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/oauth/OAuthLoginResult.kt
new file mode 100644
index 00000000..108c0c8e
--- /dev/null
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/oauth/OAuthLoginResult.kt
@@ -0,0 +1,10 @@
+/*
+ * Copyright (c) Kuba Szczodrzyński 2022-10-15.
+ */
+
+package pl.szczodrzynski.edziennik.ui.login.oauth
+
+data class OAuthLoginResult(
+ val isError: Boolean,
+ val responseUrl: String?,
+)
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableDayFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableDayFragment.kt
index 37e82a27..55250151 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableDayFragment.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableDayFragment.kt
@@ -21,8 +21,11 @@ import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import kotlinx.coroutines.*
-import pl.szczodrzynski.edziennik.*
+import pl.szczodrzynski.edziennik.App
+import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_TIMETABLE
+import pl.szczodrzynski.edziennik.R
+import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_USOS
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.db.entity.Lesson
import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull
@@ -40,8 +43,8 @@ import pl.szczodrzynski.edziennik.utils.managers.NoteManager
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.edziennik.utils.mutableLazy
-import java.util.*
import kotlin.coroutines.CoroutineContext
+import kotlin.math.max
import kotlin.math.min
class TimetableDayFragment : LazyFragment(), CoroutineScope {
@@ -82,7 +85,7 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
startHour = startHour,
endHour = endHour,
dividerHeight = 1.dp,
- halfHourHeight = 60.dp,
+ halfHourHeight = if (app.profile.loginStoreType == LOGIN_TYPE_USOS) 45.dp else 30.dp,
hourDividerColor = R.attr.hourDividerColor.resolveAttr(context),
halfHourDividerColor = R.attr.halfHourDividerColor.resolveAttr(context),
hourLabelWidth = 40.dp,
@@ -184,11 +187,18 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
val lessonsActual = lessons.filter { it.type != Lesson.TYPE_NO_LESSONS }
+ val minStartHour = lessonsActual.minOf { it.displayStartTime?.hour ?: DEFAULT_END_HOUR }
+ val maxEndHour = lessonsActual.maxOf { it.displayEndTime?.hour?.plus(1) ?: DEFAULT_START_HOUR }
+
if (profileConfig.timetableTrimHourRange) {
dayViewDelegate.deinitialize()
// end/start defaults are swapped on purpose
- startHour = lessonsActual.minOf { it.displayStartTime?.hour ?: DEFAULT_END_HOUR }
- endHour = lessonsActual.maxOf { it.displayEndTime?.hour?.plus(1) ?: DEFAULT_START_HOUR }
+ startHour = minStartHour
+ endHour = maxEndHour
+ } else if (startHour > minStartHour || endHour < maxEndHour) {
+ dayViewDelegate.deinitialize()
+ startHour = min(startHour, minStartHour)
+ endHour = max(endHour, maxEndHour)
}
b.scrollView.isVisible = true
@@ -318,7 +328,7 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
lesson.getNoteSubstituteText(showNotes = true) ?: lesson.displaySubjectName
val (subjectTextPrimary, subjectTextSecondary) = if (profileConfig.timetableColorSubjectName) {
- val subjectColor = Colors.stringToMaterialColorCRC(lessonText?.toString() ?: "")
+ val subjectColor = lesson.color ?: Colors.stringToMaterialColorCRC(lessonText?.toString() ?: "")
if (lb.annotationVisible) {
lb.subjectContainer.background = ColorDrawable(subjectColor)
} else {
@@ -377,9 +387,8 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
// The day view needs the event time ranges in the start minute/end minute format,
// so calculate those here
- val startMinute = 60 * (lesson.displayStartTime?.hour
- ?: 0) + (lesson.displayStartTime?.minute ?: 0)
- val endMinute = startMinute + 45
+ val startMinute = 60 * startTime.hour + startTime.minute
+ val endMinute = 60 * endTime.hour + endTime.minute
eventTimeRanges.add(DayView.EventTimeRange(startMinute, endMinute))
}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt
index dbe05325..3e8761c8 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt
@@ -7,58 +7,53 @@ package pl.szczodrzynski.edziennik.utils.managers
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
+import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationCompat
import org.greenrobot.eventbus.EventBus
+import org.greenrobot.eventbus.Subscribe
+import org.greenrobot.eventbus.ThreadMode
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
-import pl.szczodrzynski.edziennik.data.api.ERROR_CAPTCHA_LIBRUS_PORTAL
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
-import pl.szczodrzynski.edziennik.data.api.models.ApiError
-import pl.szczodrzynski.edziennik.ext.Intent
-import pl.szczodrzynski.edziennik.ext.JsonObject
-import pl.szczodrzynski.edziennik.ext.pendingIntentFlag
-import pl.szczodrzynski.edziennik.ui.captcha.LibrusCaptchaDialog
+import pl.szczodrzynski.edziennik.ext.*
+import pl.szczodrzynski.edziennik.ui.captcha.RecaptchaPromptDialog
+import pl.szczodrzynski.edziennik.ui.login.oauth.OAuthLoginActivity
+import pl.szczodrzynski.edziennik.ui.login.oauth.OAuthLoginResult
+import pl.szczodrzynski.edziennik.utils.Utils.d
class UserActionManager(val app: App) {
companion object {
private const val TAG = "UserActionManager"
}
- fun requiresUserAction(apiError: ApiError): Boolean {
- return apiError.errorCode == ERROR_CAPTCHA_LIBRUS_PORTAL
- }
-
- fun sendToUser(apiError: ApiError) {
- val type = when (apiError.errorCode) {
- ERROR_CAPTCHA_LIBRUS_PORTAL -> UserActionRequiredEvent.CAPTCHA_LIBRUS
- else -> 0
- }
-
+ fun sendToUser(event: UserActionRequiredEvent) {
if (EventBus.getDefault().hasSubscriberForEvent(UserActionRequiredEvent::class.java)) {
- EventBus.getDefault().post(UserActionRequiredEvent(apiError.profileId ?: -1, type))
+ EventBus.getDefault().post(event)
return
}
val manager = app.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
-
- val text = app.getString(when (type) {
- UserActionRequiredEvent.CAPTCHA_LIBRUS -> R.string.notification_user_action_required_captcha_librus
- else -> R.string.notification_user_action_required_text
- }, apiError.profileId)
-
+ val text = app.getString(event.errorText, event.profileId)
val intent = Intent(
- app,
- MainActivity::class.java,
- "action" to "userActionRequired",
- "profileId" to (apiError.profileId ?: -1),
- "type" to type
+ app,
+ MainActivity::class.java,
+ "action" to "userActionRequired",
+ "profileId" to event.profileId,
+ "type" to event.type,
+ "params" to event.params,
+ )
+ val pendingIntent = PendingIntent.getActivity(
+ app,
+ System.currentTimeMillis().toInt(),
+ intent,
+ PendingIntent.FLAG_ONE_SHOT or pendingIntentFlag(),
)
- val pendingIntent = PendingIntent.getActivity(app, System.currentTimeMillis().toInt(), intent, PendingIntent.FLAG_ONE_SHOT or pendingIntentFlag())
- val notification = NotificationCompat.Builder(app, app.notificationChannelsManager.userAttention.key)
+ val notification =
+ NotificationCompat.Builder(app, app.notificationChannelsManager.userAttention.key)
.setContentTitle(app.getString(R.string.notification_user_action_required_title))
.setContentText(text)
.setSmallIcon(R.drawable.ic_error_outline)
@@ -74,29 +69,98 @@ class UserActionManager(val app: App) {
manager.notify(System.currentTimeMillis().toInt(), notification)
}
- fun execute(
- activity: AppCompatActivity,
- profileId: Int?,
- type: Int,
- onSuccess: ((code: String) -> Unit)? = null,
- onFailure: (() -> Unit)? = null
- ) {
- if (type != UserActionRequiredEvent.CAPTCHA_LIBRUS)
- return
+ class UserActionCallback(
+ val onSuccess: ((data: Bundle) -> Unit)? = null,
+ val onFailure: (() -> Unit)? = null,
+ val onCancel: (() -> Unit)? = null,
+ )
- if (profileId == null)
- return
- // show captcha dialog
- // use passed onSuccess listener, else sync profile
- LibrusCaptchaDialog(
+ fun execute(
+ activity: AppCompatActivity,
+ event: UserActionRequiredEvent,
+ callback: UserActionCallback,
+ ) {
+ d(TAG, "Running user action (${event.type}) with params: ${event.params}")
+ val isSuccessful = when (event.type) {
+ UserActionRequiredEvent.Type.RECAPTCHA -> executeRecaptcha(activity, event, callback)
+ UserActionRequiredEvent.Type.OAUTH -> executeOauth(activity, event, callback)
+ }
+ if (!isSuccessful)
+ callback.onFailure?.invoke()
+ }
+
+ private fun executeRecaptcha(
+ activity: AppCompatActivity,
+ event: UserActionRequiredEvent,
+ callback: UserActionCallback,
+ ): Boolean {
+ val siteKey = event.params.getString("siteKey") ?: return false
+ val referer = event.params.getString("referer") ?: return false
+ RecaptchaPromptDialog(
activity = activity,
- onSuccess = onSuccess ?: { code ->
- EdziennikTask.syncProfile(profileId, arguments = JsonObject(
+ siteKey = siteKey,
+ referer = referer,
+ onSuccess = { code ->
+ finishAction(activity, event, callback, Bundle(
"recaptchaCode" to code,
- "recaptchaTime" to System.currentTimeMillis()
- )).enqueue(activity)
+ "recaptchaTime" to System.currentTimeMillis(),
+ ))
},
- onFailure = onFailure
+ onCancel = callback.onCancel,
).show()
+ return true
+ }
+
+ private fun executeOauth(
+ activity: AppCompatActivity,
+ event: UserActionRequiredEvent,
+ callback: UserActionCallback,
+ ): Boolean {
+ val storeKey = event.params.getString("responseStoreKey") ?: return false
+ event.params.getString("authorizeUrl") ?: return false
+ event.params.getString("redirectUrl") ?: return false
+
+ var listener: Any? = null
+ listener = object {
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ fun onOAuthLoginResult(result: OAuthLoginResult) {
+ EventBus.getDefault().unregister(listener)
+ when {
+ result.isError -> callback.onFailure?.invoke()
+ result.responseUrl != null -> {
+ finishAction(activity, event, callback, Bundle(
+ storeKey to result.responseUrl,
+ ))
+ }
+ else -> callback.onCancel?.invoke()
+ }
+ }
+ }
+ EventBus.getDefault().register(listener)
+
+ val intent = Intent(activity, OAuthLoginActivity::class.java).putExtras(event.params)
+ activity.startActivity(intent)
+ return true
+ }
+
+ private fun finishAction(
+ activity: AppCompatActivity,
+ event: UserActionRequiredEvent,
+ callback: UserActionCallback,
+ data: Bundle,
+ ) {
+ val extras = event.params.getBundle("extras")
+ if (extras != null)
+ data.putAll(extras)
+
+ if (callback.onSuccess != null)
+ callback.onSuccess.invoke(data)
+ else if (event.profileId != null)
+ EdziennikTask.syncProfile(
+ profileId = event.profileId,
+ arguments = data.toJsonObject(),
+ ).enqueue(activity)
+ else
+ callback.onFailure?.invoke()
}
}
diff --git a/app/src/main/res/drawable/login_logo_usos.png b/app/src/main/res/drawable/login_logo_usos.png
new file mode 100644
index 00000000..98d95b34
Binary files /dev/null and b/app/src/main/res/drawable/login_logo_usos.png differ
diff --git a/app/src/main/res/drawable/login_mode_usos_api.png b/app/src/main/res/drawable/login_mode_usos_api.png
new file mode 100644
index 00000000..6ec32631
Binary files /dev/null and b/app/src/main/res/drawable/login_mode_usos_api.png differ
diff --git a/app/src/main/res/layout/card_home_debug.xml b/app/src/main/res/layout/card_home_debug.xml
index 18d21c02..4bcca808 100644
--- a/app/src/main/res/layout/card_home_debug.xml
+++ b/app/src/main/res/layout/card_home_debug.xml
@@ -25,13 +25,6 @@
android:layout_height="wrap_content"
android:text="Save Debug Logs" />
-
-
ERROR_LOGIN_METHOD_NOT_SATISFIED
ERROR_NOT_IMPLEMENTED
ERROR_FILE_DOWNLOAD
-
- ERROR_NO_STUDENTS_IN_ACCOUNT
-
- ERROR_CAPTCHA_NEEDED
- ERROR_CAPTCHA_LIBRUS_PORTAL
+ ERROR_REQUIRES_USER_ACTION
ERROR_API_PDO_ERROR
ERROR_API_INVALID_CLIENT
@@ -174,6 +170,12 @@
ERROR_PODLASIE_API_OTHER
ERROR_PODLASIE_API_DATA_MISSING
+ ERROR_USOS_OAUTH_GOT_DIFFERENT_TOKEN
+ ERROR_USOS_OAUTH_INCOMPLETE_RESPONSE
+ ERROR_USOS_NO_STUDENT_PROGRAMMES
+ ERROR_USOS_API_INCOMPLETE_RESPONSE
+ ERROR_USOS_API_MISSING_RESPONSE
+
ERROR_TEMPLATE_WEB_OTHER
EXCEPTION_API_TASK
@@ -220,11 +222,7 @@
Nie można wywołać metody logowania. Skontaktuj się z twórcą aplikacji.
Nie zaimplementowano
Wystąpił błąd podczas pobierania pliku. Dziennik może być przeciążony lub mieć przerwę techniczną.
-
- Brak uczniów przypisanych do konta
-
- Wymagane rozwiązanie zadania Captcha
- LIBRUS®️: wymagane rozwiązanie zadania Captcha
+ Wymagana akcja w aplikacji
ERROR_API_PDO_ERROR
Nieprawidłowy ID klienta API
@@ -364,6 +362,12 @@
ERROR_PODLASIE_API_OTHER
Brak danych. Zgłoś błąd programiście.
+ Błąd logowania: otrzymano nieprawidłowy token
+ Błąd logowania: niekompletna odpowiedź serwera
+ Student nie jest zapisany na żaden kierunek
+ Brakujące dane w odpowiedzi serwera
+ Brakująca odpowiedź serwera
+
ERROR_TEMPLATE_WEB_OTHER
Błąd synchronizacji. Upewnij się, że masz połączenie z internetem, a następnie zgłoś błąd.
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b59691c9..3cc2451d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1542,4 +1542,10 @@
Pokazuj rodzaj obecności na lekcji
Koloruj nazwę przedmiotu
Nie pokazuj godzin bez lekcji
+ Logowanie do USOS API...
+ USOS
+ Logowanie z użyciem przeglądarki
+ TODO
+ USOS - wymagane logowanie z użyciem przeglądarki
+ Zaloguj się