From d94f38de93415074c8673e532d8d59ff72878583 Mon Sep 17 00:00:00 2001 From: Jannik Seiler Date: Fri, 28 Aug 2020 21:38:44 +0200 Subject: [PATCH] save grades to a encrypted cache file, use cache if not older than 24hr --- app/build.gradle | 4 +- .../controller/cache/CacheController.kt | 76 +++++++++ .../controller/cache/TimetableController.kt | 153 +++++++++--------- .../controller/preferences/Preferences.kt | 7 + .../projectlaogai/fragments/GradesFragment.kt | 146 +++++++++-------- .../fragments/TimeTableFragment.kt | 2 +- app/src/main/res/values/strings.xml | 1 + 7 files changed, 243 insertions(+), 146 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 679992e..9191017 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -51,7 +51,7 @@ dependencies { implementation 'androidx.core:core:1.3.1' implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'androidx.constraintlayout:constraintlayout:2.0.0' + implementation 'androidx.constraintlayout:constraintlayout:2.0.1' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.security:security-crypto:1.1.0-alpha02' implementation 'com.google.android.material:material:1.2.0' @@ -66,7 +66,7 @@ dependencies { implementation 'org.jsoup:jsoup:1.13.1' testImplementation 'junit:junit:4.13' - androidTestImplementation 'androidx.test:core:1.2.0' + androidTestImplementation 'androidx.test:core:1.3.0' androidTestImplementation 'androidx.test.ext:junit:1.1.1' } diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/cache/CacheController.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/controller/cache/CacheController.kt index 3fc2581..530e8a1 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/cache/CacheController.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/controller/cache/CacheController.kt @@ -24,21 +24,26 @@ package org.mosad.seil0.projectlaogai.controller.cache import android.content.Context import android.util.Log +import androidx.security.crypto.EncryptedFile +import androidx.security.crypto.MasterKey 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.QISPOSParser import org.mosad.seil0.projectlaogai.controller.preferences.Preferences import org.mosad.seil0.projectlaogai.controller.TCoRAPIController import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.cCourse import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.coursesCacheTime +import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.gradesCacheTime import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.mensaCacheTime import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.timetableCacheTime import org.mosad.seil0.projectlaogai.util.* import java.io.* import java.util.* import kotlin.collections.ArrayList +import kotlin.collections.HashMap /** * The cacheController reads and updates the cache files. @@ -201,6 +206,25 @@ class CacheController(cont: Context) { } } + /** + * update the encrypted grades file + */ + fun updateGrades(context: Context): Job { + val file = File(context.filesDir, "grades_encrypted") + val parser = QISPOSParser(context) + + return GlobalScope.launch(Dispatchers.IO) { + if (parser.checkQISPOSStatus() == 200) { + if (file.exists()) { file.delete() } + + // save cache file and update time + saveEncrypted(context, file, Gson().toJson(parser.parseGrades())) + gradesCacheTime = System.currentTimeMillis() / 1000 + Preferences.save(context) + } + } + } + /** * save changes in lessonMap and subjectMap, * called on addSubject or removeSubject @@ -223,6 +247,22 @@ class CacheController(cont: Context) { } } + private fun saveEncrypted(context: Context, file: File, text: String) { + val masterKey = MasterKey.Builder(context) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + val encryptedFile = EncryptedFile.Builder( + context, + file, + masterKey, + EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB + ).build() + + encryptedFile.openFileOutput().bufferedWriter().use { + it.write(text) + } + } + } /** @@ -372,4 +412,40 @@ class CacheController(cont: Context) { } + /** + * read the encrypted grades file, don't keep them + * in CacheController for security reasons + * @return the grades as HashMap if the file exists, else a empty HashMap + */ + fun readGrades(): HashMap> { + val file = File(context.filesDir, "grades_encrypted") + + // if the file does not exit, try creating it + if (!file.exists()) { + runBlocking { updateGrades(context) } + } + + if (file.exists()) { + val masterKey = MasterKey.Builder(context) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + val encryptedFile = EncryptedFile.Builder( + context, + file, + masterKey, + EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB + ).build() + + return encryptedFile.openFileInput().bufferedReader().use { + GsonBuilder().create() + .fromJson( + it.readLine(), + object : TypeToken>>() {}.type + ) + } + } + + return HashMap() + } + } \ No newline at end of file diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/cache/TimetableController.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/controller/cache/TimetableController.kt index 2863bfe..48a6868 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/cache/TimetableController.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/controller/cache/TimetableController.kt @@ -38,94 +38,91 @@ import org.mosad.seil0.projectlaogai.util.TimetableWeek * * add second week * * add configurable week to addSubject() and removeSubject(), updateAdditionalLessons() */ -class TimetableController { +object TimetableController { - companion object { - val timetable = ArrayList() - val lessonMap = HashMap() // the key is courseName-subject-lessonID - val subjectMap = HashMap>() // the key is courseName + val timetable = ArrayList() + val lessonMap = HashMap() // the key is courseName-subject-lessonID + val subjectMap = HashMap>() // the key is courseName - /** - * update the main timetable and all additional subjects, async - */ - fun update(context: Context): List { - return listOf( - CacheController.updateTimetable(cCourse.courseName, 0, context), - CacheController.updateTimetable(cCourse.courseName, 1, context), - CacheController.updateAdditionalLessons(context) - ) + /** + * update the main timetable and all additional subjects, async + */ + fun update(context: Context): List { + return listOf( + CacheController.updateTimetable(cCourse.courseName, 0, context), + CacheController.updateTimetable(cCourse.courseName, 1, context), + CacheController.updateAdditionalLessons(context) + ) + } + + /** + * add a subject to the subjectMap and all it's lessons + * to the lessonMap + * @param courseName course to which the subject belongs + * @param subject the subjects name + */ + fun addSubject(courseName: String, subject: String, context: Context) { + // add subject + if (subjectMap.containsKey(courseName)) { + subjectMap[courseName]?.add(subject) + } else { + subjectMap[courseName] = arrayListOf(subject) } - /** - * add a subject to the subjectMap and all it's lessons - * to the lessonMap - * @param courseName course to which the subject belongs - * @param subject the subjects name - */ - fun addSubject(courseName: String, subject: String, context: Context) { - // add subject - if (subjectMap.containsKey(courseName)) { - subjectMap[courseName]?.add(subject) - } else { - subjectMap[courseName] = arrayListOf(subject) + // add concrete lessons + TCoRAPIController.getLessons(courseName, subject, 0).forEach { lesson -> + addLesson(courseName, subject, lesson) + } + + CacheController.saveAdditionalSubjects(context) + } + + /** + * remove a subject from the subjectMap and all it's lessons + * from the lessonMap + * @param courseName course to which the subject belongs + * @param subject the subjects name + */ + fun removeSubject(courseName: String, subject: String, context: Context) { + // remove subject + subjectMap[courseName]?.remove(subject) + + // remove concrete lessons + val iterator = lessonMap.iterator() + while (iterator.hasNext()) { + val it = iterator.next() + if(it.key.contains("$courseName-$subject")) { + // remove the lesson from the lessons list + iterator.remove() // use iterator to remove, otherwise ConcurrentModificationException + + // remove the lesson from the timetable + val id = it.value.lessonID.split(".") + if(id.size == 3) + timetable[0].days[id[0].toInt()].timeslots[id[1].toInt()].remove(it.value) } - - // add concrete lessons - TCoRAPIController.getLessons(courseName, subject, 0).forEach { lesson -> - addLesson(courseName, subject, lesson) - } - - CacheController.saveAdditionalSubjects(context) } - /** - * remove a subject from the subjectMap and all it's lessons - * from the lessonMap - * @param courseName course to which the subject belongs - * @param subject the subjects name - */ - fun removeSubject(courseName: String, subject: String, context: Context) { - // remove subject - subjectMap[courseName]?.remove(subject) + CacheController.saveAdditionalSubjects(context) + } - // remove concrete lessons - val iterator = lessonMap.iterator() - while (iterator.hasNext()) { - val it = iterator.next() - if(it.key.contains("$courseName-$subject")) { - // remove the lesson from the lessons list - iterator.remove() // use iterator to remove, otherwise ConcurrentModificationException + /** + * add a lesson to the lessonMap, also add it to the timetable + */ + fun addLesson(courseName: String, subject: String, lesson: Lesson) { + //the courseName, subject and lessonID, separator: - + val key = "$courseName-$subject-${lesson.lessonID}" + lessonMap[key] = lesson - // remove the lesson from the timetable - val id = it.value.lessonID.split(".") - if(id.size == 3) - timetable[0].days[id[0].toInt()].timeslots[id[1].toInt()].remove(it.value) - } - } - - CacheController.saveAdditionalSubjects(context) - } - - /** - * add a lesson to the lessonMap, also add it to the timetable - */ - fun addLesson(courseName: String, subject: String, lesson: Lesson) { - //the courseName, subject and lessonID, separator: - - val key = "$courseName-$subject-${lesson.lessonID}" - lessonMap[key] = lesson - - addLessonToTimetable(lesson) - } - - /** - * add a lesson to the timetable - */ - fun addLessonToTimetable(lesson: Lesson) { - val id = lesson.lessonID.split(".") - if(id.size == 3) - timetable[0].days[id[0].toInt()].timeslots[id[1].toInt()].add(lesson) - } + addLessonToTimetable(lesson) + } + /** + * add a lesson to the timetable + */ + fun addLessonToTimetable(lesson: Lesson) { + val id = lesson.lessonID.split(".") + if(id.size == 3) + timetable[0].days[id[0].toInt()].timeslots[id[1].toInt()].add(lesson) } } \ No newline at end of file diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/preferences/Preferences.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/controller/preferences/Preferences.kt index 86dadae..0cccc87 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/preferences/Preferences.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/controller/preferences/Preferences.kt @@ -36,6 +36,7 @@ object Preferences { var coursesCacheTime: Long = 0 var mensaCacheTime: Long = 0 var timetableCacheTime: Long = 0 + var gradesCacheTime: Long = 0 var cColorPrimary: Int = Color.parseColor("#009688") var cColorAccent: Int = Color.parseColor("#0096ff") var cCourse = Course( @@ -67,6 +68,9 @@ object Preferences { putLong(context.getString(R.string.save_key_timetableCacheTime), timetableCacheTime ) + putLong(context.getString(R.string.save_key_gradesCacheTime), + gradesCacheTime + ) apply() } @@ -166,6 +170,9 @@ object Preferences { timetableCacheTime = sharedPref.getLong(context.getString( R.string.save_key_timetableCacheTime ), 0) + gradesCacheTime = sharedPref.getLong(context.getString( + R.string.save_key_gradesCacheTime + ), 0) // load saved course cCourse = Course( diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/GradesFragment.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/GradesFragment.kt index 376be8d..7bdd378 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/GradesFragment.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/GradesFragment.kt @@ -35,11 +35,13 @@ import kotlinx.android.synthetic.main.fragment_grades.* import kotlinx.coroutines.* import org.mosad.seil0.projectlaogai.R import org.mosad.seil0.projectlaogai.controller.QISPOSParser +import org.mosad.seil0.projectlaogai.controller.cache.CacheController import org.mosad.seil0.projectlaogai.controller.preferences.EncryptedPreferences import org.mosad.seil0.projectlaogai.controller.preferences.Preferences import org.mosad.seil0.projectlaogai.uicomponents.DayCardView import org.mosad.seil0.projectlaogai.uicomponents.GradeLinearLayout import org.mosad.seil0.projectlaogai.uicomponents.dialogs.LoginDialog +import kotlin.system.measureTimeMillis /** * The grades fragment class @@ -64,6 +66,7 @@ class GradesFragment : Fragment() { parser = QISPOSParser(context!!)// init the parser + // TODO if loading from cache, don't check Qispos state if (checkCredentials() && checkQisposStatus()) { GlobalScope.launch(Dispatchers.Default) { addGrades() @@ -125,76 +128,89 @@ class GradesFragment : Fragment() { return statusCode == 200 } - // add the grades to the layout, async + /** + * add the grades to the layout, async + * TODO this is slow as hell + */ private fun addGrades() = GlobalScope.launch(Dispatchers.Default) { - withContext(Dispatchers.Main) { - refreshLayout_Grades.isRefreshing = true - } + val addGradesTime = measureTimeMillis { - val grades = parser.parseGrades() - - withContext(Dispatchers.Main) { - linLayout_Grades.removeAllViews() // clear layout - } - - // for each semester add a new card - grades.forEach { semester -> - val usedSubjects = ArrayList() - val semesterCard = DayCardView(context!!) - semesterCard.setDayHeading(semester.key) - - // for each subject add a new linLayout - semester.value.forEachIndexed { index, subject -> - if (usedSubjects.contains(subject.name)) { - return@forEachIndexed - } - - // get the first sub subjects - val subSubject = semester.value.firstOrNull { - it.name.contains(subject.name) && it.name != subject.name - } - - // if sub subject is not null, add it to used subjects - subSubject?.let { - usedSubjects.add(it.name) - } - - val subjectLayout = GradeLinearLayout(context).set { - subjectName = subject.name - grade = subject.grade - subSubjectName = subSubject?.name.toString() - subGrade = subSubject?.grade.toString() - } - - // disable sub-subject if not set - if (subSubject == null) - subjectLayout.disableSubSubject() - - // disable divider if last element - if (index == semester.value.lastIndex || semester.value.indexOf(subSubject) == semester.value.lastIndex) - subjectLayout.disableDivider() - - semesterCard.getLinLayoutDay().addView(subjectLayout) - } - - // without context we can't access the view withContext(Dispatchers.Main) { - linLayout_Grades.addView(semesterCard) + refreshLayout_Grades.isRefreshing = true } + + // if the cache is older than 24hr, update blocking + val currentTime = System.currentTimeMillis() / 1000 + if ((currentTime - Preferences.gradesCacheTime) > 86400) { + CacheController.updateGrades(context!!).join() + } + + val grades = CacheController(context!!).readGrades() + + withContext(Dispatchers.Main) { + linLayout_Grades.removeAllViews() // clear layout + } + + // TODO this loop takes 3/4 of the time + // for each semester add a new card + grades.forEach { semester -> + val usedSubjects = ArrayList() + val semesterCard = DayCardView(context!!) + semesterCard.setDayHeading(semester.key) + + // for each subject add a new linLayout + semester.value.forEachIndexed { index, subject -> + if (usedSubjects.contains(subject.name)) { + return@forEachIndexed + } + + // get the first sub subjects + val subSubject = semester.value.firstOrNull { + it.name.contains(subject.name) && it.name != subject.name + } + + // if sub subject is not null, add it to used subjects + subSubject?.let { + usedSubjects.add(it.name) + } + + val subjectLayout = GradeLinearLayout(context).set { + subjectName = subject.name + grade = subject.grade + subSubjectName = subSubject?.name.toString() + subGrade = subSubject?.grade.toString() + } + + // disable sub-subject if not set + if (subSubject == null) + subjectLayout.disableSubSubject() + + // disable divider if last element + if (index == semester.value.lastIndex || semester.value.indexOf(subSubject) == semester.value.lastIndex) + subjectLayout.disableDivider() + + semesterCard.getLinLayoutDay().addView(subjectLayout) + } + + // without context we can't access the view + withContext(Dispatchers.Main) { + linLayout_Grades.addView(semesterCard) + } + } + + val txtViewLegal = TextView(context).apply { + text = resources.getString(R.string.without_guarantee) + textAlignment = View.TEXT_ALIGNMENT_CENTER + } + + // stop refreshing and show legal warning + withContext(Dispatchers.Main) { + linLayout_Grades.addView(txtViewLegal) + refreshLayout_Grades.isRefreshing = false + } + } - - - - val txtViewLegal = TextView(context).apply { - text = resources.getString(R.string.without_guarantee) - textAlignment = View.TEXT_ALIGNMENT_CENTER - } - - // stop refreshing and show legal warning - withContext(Dispatchers.Main) { - linLayout_Grades.addView(txtViewLegal) - refreshLayout_Grades.isRefreshing = false - } + println("startup completed in $addGradesTime ms") } } \ No newline at end of file diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/TimeTableFragment.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/TimeTableFragment.kt index ce1736d..e1c21d6 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/TimeTableFragment.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/TimeTableFragment.kt @@ -33,7 +33,7 @@ import kotlinx.android.synthetic.main.fragment_timetable.* import kotlinx.coroutines.* import org.mosad.seil0.projectlaogai.R import org.mosad.seil0.projectlaogai.controller.cache.TimetableController -import org.mosad.seil0.projectlaogai.controller.cache.TimetableController.Companion.timetable +import org.mosad.seil0.projectlaogai.controller.cache.TimetableController.timetable import org.mosad.seil0.projectlaogai.controller.preferences.Preferences import org.mosad.seil0.projectlaogai.uicomponents.dialogs.AddSubjectDialog import org.mosad.seil0.projectlaogai.uicomponents.DayCardView diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aa46582..a5aefc8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -127,6 +127,7 @@ org.mosad.seil0.projectlaogai.coursesCacheTime org.mosad.seil0.projectlaogai.mensaCacheTime org.mosad.seil0.projectlaogai.timetableCacheTime + org.mosad.seil0.projectlaogai.gradesCacheTime org.mosad.seil0.projectlaogai.user_email org.mosad.seil0.projectlaogai.user_password