From 4de066bf5facab8e303dedb747fc6cb8c391f3a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sun, 16 Oct 2022 17:21:36 +0200 Subject: [PATCH] [API/Usos] Implement Timetable. --- .../data/api/edziennik/usos/UsosFeatures.kt | 24 +++- .../data/api/edziennik/usos/data/UsosData.kt | 10 +- .../edziennik/usos/data/api/UsosApiCourses.kt | 10 +- .../edziennik/usos/data/api/UsosApiTerms.kt | 3 +- .../usos/data/api/UsosApiTimetable.kt | 132 ++++++++++++++++++ .../edziennik/ext/TimeExtensions.kt | 11 +- .../ui/timetable/TimetableDayFragment.kt | 25 ++-- 7 files changed, 188 insertions(+), 27 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTimetable.kt 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 index bdcf9c78..50c15582 100644 --- 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 @@ -5,25 +5,35 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.usos import pl.szczodrzynski.edziennik.data.api.* -import pl.szczodrzynski.edziennik.data.api.FEATURE_ALWAYS_NEEDED -import pl.szczodrzynski.edziennik.data.api.FEATURE_STUDENT_INFO -import pl.szczodrzynski.edziennik.data.api.FEATURE_TEAM_INFO 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_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/UsosData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt index 5ea6ab66..6f3b6039 100644 --- 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 @@ -6,12 +6,10 @@ 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.DataUsos -import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_COURSES -import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_TERMS -import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_USER +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.utils.Utils.d class UsosData(val data: DataUsos, val onSuccess: () -> Unit) { @@ -55,6 +53,10 @@ class UsosData(val data: DataUsos, val onSuccess: () -> Unit) { 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 index 8f7359b5..613d9f08 100644 --- 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 @@ -29,12 +29,12 @@ class UsosApiCourses( // "terms" to listOf("id", "name", "start_date", "end_date"), "course_editions" to listOf( "course_id", - // "course_name", + "course_name", // "term_id", "user_groups" to listOf( "course_unit_id", "group_number", - "class_type", + // "class_type", "class_type_id", // "lecturers", ), @@ -62,18 +62,18 @@ class UsosApiCourses( var hasValidTeam = false for (courseEdition in courseEditions) { val courseId = courseEdition.getString("course_id") ?: continue - // val courseName = courseEdition.getLangString("course_name") ?: 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 classType = userGroup.getLangString("class_type") ?: continue val classTypeId = userGroup.getString("class_type_id") ?: continue data.teamList.put(courseUnitId, Team( profileId, courseUnitId, - "$classType $groupNumber ($courseId)", + "${profile?.studentClassName} $classTypeId$groupNumber - $courseName", 2, "${data.schoolId}:${courseId} $classTypeId$groupNumber", -1, 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 index 4a17bffa..1bf47288 100644 --- 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 @@ -5,7 +5,6 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api import com.google.gson.JsonArray -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_TERMS @@ -32,7 +31,7 @@ class UsosApiTerms( responseType = ResponseType.ARRAY, ) { json, response -> if (!processResponse(json)) { - data.error(UsosApiCourses.TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response) + data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response) return@apiRequest } 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..355731a8 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTimetable.kt @@ -0,0 +1,132 @@ +/* + * 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() + } + 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/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/timetable/TimetableDayFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableDayFragment.kt index 37e82a27..1e4d9773 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 @@ -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)) }