/** * ProjectLaogai * * Copyright 2019-2020 * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ package org.mosad.seil0.projectlaogai.controller.cache import android.content.Context import android.util.Log import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.JsonParser import com.google.gson.reflect.TypeToken import kotlinx.coroutines.* import org.mosad.seil0.projectlaogai.controller.PreferencesController import org.mosad.seil0.projectlaogai.controller.PreferencesController.Companion.cCourse import org.mosad.seil0.projectlaogai.controller.PreferencesController.Companion.coursesCacheTime import org.mosad.seil0.projectlaogai.controller.PreferencesController.Companion.mensaCacheTime import org.mosad.seil0.projectlaogai.controller.PreferencesController.Companion.timetableCacheTime import org.mosad.seil0.projectlaogai.controller.TCoRAPIController import org.mosad.seil0.projectlaogai.util.* import java.io.* import java.util.* import kotlin.collections.ArrayList /** * The cacheController reads and updates the cache files. * It contains the courseList and mensaMenu object, all timetable objects * are located in TimetableController. */ class CacheController(cont: Context) { private val className = "CacheController" private val context = cont init { val cal = Calendar.getInstance() val currentDay = Calendar.getInstance().get(Calendar.DAY_OF_WEEK) val currentTime = System.currentTimeMillis() / 1000 // check if we need to update the mensa data before displaying it cal.time = Date(mensaCacheTime * 1000) // if a) it's monday and the last cache update was on sunday or b) the cache is older than 24hr, update blocking if ((currentDay == Calendar.MONDAY && cal.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY) || (currentTime - mensaMenu.meta.updateTime) > 86400) { Log.i(className, "update mensa blocking") GlobalScope.launch(Dispatchers.Default) { updateMensaMenu(context).join() } } // check if we need to update the timetable before displaying it cal.time = Date(timetableCacheTime * 1000) // if a) it`s monday and the last cache update was not on a sunday or b) the cache is older than 24hr, update blocking if ((currentDay == Calendar.MONDAY && cal.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY) || (currentTime - timetableCacheTime) > 86400) { Log.i(className, "updating timetable after sunday!") GlobalScope.launch(Dispatchers.Default) { val threads = listOf( updateTimetable(cCourse.courseName, 0, context), updateTimetable(cCourse.courseName, 1, context) ) threads.joinAll() } } updateCourseList(context) readStartCache(cCourse.courseName) // initially read values from cache // check if an update is necessary, not blocking if (currentTime - coursesCacheTime > 86400) updateCourseList(context) if (currentTime - mensaCacheTime > 10800) updateMensaMenu(context) if (currentTime - timetableCacheTime > 10800) { TimetableController.update(context) } } companion object { private const val className = "CacheController" var coursesList = ArrayList() var mensaMenu = MensaMenu() /** * update the course list, async */ fun updateCourseList(context: Context): Job { val file = File(context.filesDir, "courses.json") var courseListUp = CoursesList() return GlobalScope.launch(Dispatchers.IO) { try { courseListUp = TCoRAPIController.getCourseListNEW() } catch (ex: Exception) { Log.e(className, "could not load course list from tcor", ex) } // update coursesList array list coursesList = courseListUp.courses // save cache file and update time save(file, Gson().toJson(courseListUp)) coursesCacheTime = System.currentTimeMillis() / 1000 PreferencesController.save(context) } } /** * update the mensa menu, async */ fun updateMensaMenu(context: Context): Job { val file = File(context.filesDir, "mensa.json") return GlobalScope.launch(Dispatchers.IO) { try { mensaMenu = TCoRAPIController.getMensaMenu() } catch (ex: Exception) { Log.e(className, "could not load mensa menu from tcor", ex) } // save cache file and update time save(file, Gson().toJson(mensaMenu)) mensaCacheTime = System.currentTimeMillis() / 1000 PreferencesController.save(context) } } /** * update the timetable for a week, async * @param courseName the course name (e.g AI1) * @param week the week to update (0 for the current and so on) */ fun updateTimetable(courseName: String, week: Int, context: Context): Job { val file = File(context.filesDir, "timetable-$courseName-$week.json") var timetable = TimetableWeek() // try to update timetable from tcor, async return GlobalScope.launch(Dispatchers.IO) { try { timetable = TCoRAPIController.getTimetable(courseName, week) } catch (ex: Exception) { Log.e(className, "could not load timetable $courseName[$week] from tcor", ex) } // update timetable in TTC if (TimetableController.timetable.size > week) { TimetableController.timetable[week] = timetable } else { TimetableController.timetable.add(timetable) } // save cache file and update time save(file, Gson().toJson(timetable)) timetableCacheTime = System.currentTimeMillis() / 1000 PreferencesController.save(context) } } /** * update all additional subject lessons, async */ fun updateAdditionalLessons(context: Context): Job { val fileLessons = File(context.filesDir, "additional_lessons.json") return GlobalScope.launch(Dispatchers.IO) { TimetableController.subjectMap.forEach { (courseName, subjects) -> // update all subjects for a course subjects.forEach {subject -> try { TCoRAPIController.getLessons(courseName, subject, 0).forEach { lesson -> TimetableController.addLesson(courseName, subject, lesson) } } catch (ex: Exception) { Log.e(className, "could not load $courseName: $subject", ex) } } } save(fileLessons, Gson().toJson(TimetableController.lessonMap)) } } /** * save changes in lessonMap and subjectMap, * called on addSubject or removeSubject */ fun saveAdditionalSubjects(context: Context) { val fileLessons = File(context.filesDir, "additional_lessons.json") val fileSubjects = File(context.filesDir, "additional_subjects.json") save(fileLessons, Gson().toJson(TimetableController.lessonMap)) save(fileSubjects, Gson().toJson(TimetableController.subjectMap)) } private fun save(file: File, text: String) { try { val writer = BufferedWriter(FileWriter(file)) writer.write(text) writer.close() } catch (ex: Exception) { Log.e(className, "failed to write file \"${file.absoluteFile}\"", ex) } } } /** * read coursesList, mensa (current and next week), timetable (current and next week) * @param courseName the course name (e.g AI1) */ private fun readStartCache(courseName: String) { try { readCoursesList() } catch (ex : Exception) { Log.e(className, "Error while reading the course list", ex) } try { readMensa() } catch (ex : Exception) { Log.e(className, "Error while reading the mensa menu", ex) } try { readTimetable(courseName, 0) } catch (ex : Exception) { Log.e(className, "Error while reading timetable week 0", ex) } try { readTimetable(courseName, 1) } catch (ex : Exception) { Log.e(className, "Error while reading timetable week 1", ex) } try { readAdditionalSubjects() } catch (ex : Exception) { Log.e(className, "Error while reading additional subjects", ex) } } /** * read the courses list from the cached file * add them to the coursesList object */ private fun readCoursesList() { val file = File(context.filesDir, "courses.json") // make sure the file exists if (!file.exists()) { runBlocking { updateCourseList(context).join() } return } coursesList = FileReader(file).use { GsonBuilder().create().fromJson(BufferedReader(it).readLine(), CoursesList().javaClass).courses } } /** * get the MensaMenu object from the cached json, * if cache is empty create the cache file */ private fun readMensa() { val file = File(context.filesDir, "mensa.json") // make sure the file exists if (!file.exists()) { runBlocking { updateMensaMenu(context).join() } return } mensaMenu = FileReader(file).use { GsonBuilder().create().fromJson(BufferedReader(it).readLine(), MensaMenu().javaClass) } } /** * read the weeks timetable from the cached file * @param courseName the course name (e.g AI1) * @param week the week to read (0 for the current and so on) */ private fun readTimetable(courseName: String, week: Int) { val file = File(context.filesDir, "timetable-$courseName-$week.json") // if the file does not exist, call updateTimetable blocking and return if (!file.exists()) { runBlocking { updateTimetable(courseName, week, context).join() } return } val timetableObject = FileReader(file).use { JsonParser.parseString(BufferedReader(it).readLine()).asJsonObject } // if its a TimetableCourseWeek object migrate to TimetableWeek TODO remove converting at version 0.8 val timetable = if(timetableObject.has("meta")) { Log.i(Companion.className, "trying to migrate TimetableCourseWeek to TimetableWeek") val timetableWC = Gson().fromJson(timetableObject, TimetableCourseWeek().javaClass) save(file, Gson().toJson(TimetableWeek( timetableWC.meta.weekIndex, timetableWC.meta.weekNumberYear, timetableWC.timetable.days ))) TimetableWeek(timetableWC.meta.weekIndex, timetableWC.meta.weekNumberYear, timetableWC.timetable.days) } else { Gson().fromJson(timetableObject, TimetableWeek().javaClass) } // update timetable in TTC if (TimetableController.timetable.size > week) { TimetableController.timetable[week] = timetable } else { TimetableController.timetable.add(timetable) } } private fun readAdditionalSubjects() { val fileLessons = File(context.filesDir, "additional_lessons.json") val fileSubjects = File(context.filesDir, "additional_subjects.json") // make sure the file exists if (!fileLessons.exists() || !fileSubjects.exists()) { return } // clear the maps before loading, just to be save TimetableController.lessonMap.clear() TimetableController.subjectMap.clear() // read subjects and lessons from cache file FileReader(fileLessons).use { TimetableController.lessonMap.putAll( GsonBuilder().create() .fromJson(BufferedReader(it).readLine(), object : TypeToken>() {}.type) ) } FileReader(fileSubjects).use { TimetableController.subjectMap.putAll( GsonBuilder().create() .fromJson(BufferedReader(it).readLine(), HashMap>().javaClass) ) } // add lessons to timetable TimetableController.lessonMap.forEach { (_, lesson) -> TimetableController.addLessonToTimetable(lesson) } } }