save grades to a encrypted cache file, use cache if not older than 24hr
continuous-integration/drone/push Build is passing Details

This commit is contained in:
Jannik 2020-08-28 21:38:44 +02:00
parent 313ff4741a
commit d94f38de93
Signed by: Seil0
GPG Key ID: E8459F3723C52C24
7 changed files with 243 additions and 146 deletions

View File

@ -51,7 +51,7 @@ dependencies {
implementation 'androidx.core:core:1.3.1' implementation 'androidx.core:core:1.3.1'
implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.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.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.security:security-crypto:1.1.0-alpha02' implementation 'androidx.security:security-crypto:1.1.0-alpha02'
implementation 'com.google.android.material:material:1.2.0' implementation 'com.google.android.material:material:1.2.0'
@ -66,7 +66,7 @@ dependencies {
implementation 'org.jsoup:jsoup:1.13.1' implementation 'org.jsoup:jsoup:1.13.1'
testImplementation 'junit:junit:4.13' 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' androidTestImplementation 'androidx.test.ext:junit:1.1.1'
} }

View File

@ -24,21 +24,26 @@ package org.mosad.seil0.projectlaogai.controller.cache
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import androidx.security.crypto.EncryptedFile
import androidx.security.crypto.MasterKey
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.google.gson.JsonParser import com.google.gson.JsonParser
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.mosad.seil0.projectlaogai.controller.QISPOSParser
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences import org.mosad.seil0.projectlaogai.controller.preferences.Preferences
import org.mosad.seil0.projectlaogai.controller.TCoRAPIController import org.mosad.seil0.projectlaogai.controller.TCoRAPIController
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.cCourse 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.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.mensaCacheTime
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.timetableCacheTime import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.timetableCacheTime
import org.mosad.seil0.projectlaogai.util.* import org.mosad.seil0.projectlaogai.util.*
import java.io.* import java.io.*
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.collections.HashMap
/** /**
* The cacheController reads and updates the cache files. * 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, * save changes in lessonMap and subjectMap,
* called on addSubject or removeSubject * 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<String, ArrayList<GradeSubject>> {
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<HashMap<String, ArrayList<GradeSubject>>>() {}.type
)
}
}
return HashMap()
}
} }

View File

@ -38,94 +38,91 @@ import org.mosad.seil0.projectlaogai.util.TimetableWeek
* * add second week * * add second week
* * add configurable week to addSubject() and removeSubject(), updateAdditionalLessons() * * add configurable week to addSubject() and removeSubject(), updateAdditionalLessons()
*/ */
class TimetableController { object TimetableController {
companion object { val timetable = ArrayList<TimetableWeek>()
val timetable = ArrayList<TimetableWeek>() val lessonMap = HashMap<String, Lesson>() // the key is courseName-subject-lessonID
val lessonMap = HashMap<String, Lesson>() // the key is courseName-subject-lessonID val subjectMap = HashMap<String, ArrayList<String>>() // the key is courseName
val subjectMap = HashMap<String, ArrayList<String>>() // the key is courseName
/** /**
* update the main timetable and all additional subjects, async * update the main timetable and all additional subjects, async
*/ */
fun update(context: Context): List<Job> { fun update(context: Context): List<Job> {
return listOf( return listOf(
CacheController.updateTimetable(cCourse.courseName, 0, context), CacheController.updateTimetable(cCourse.courseName, 0, context),
CacheController.updateTimetable(cCourse.courseName, 1, context), CacheController.updateTimetable(cCourse.courseName, 1, context),
CacheController.updateAdditionalLessons(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 concrete lessons
* add a subject to the subjectMap and all it's lessons TCoRAPIController.getLessons(courseName, subject, 0).forEach { lesson ->
* to the lessonMap addLesson(courseName, subject, lesson)
* @param courseName course to which the subject belongs }
* @param subject the subjects name
*/ CacheController.saveAdditionalSubjects(context)
fun addSubject(courseName: String, subject: String, context: Context) { }
// add subject
if (subjectMap.containsKey(courseName)) { /**
subjectMap[courseName]?.add(subject) * remove a subject from the subjectMap and all it's lessons
} else { * from the lessonMap
subjectMap[courseName] = arrayListOf(subject) * @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)
} }
/** 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() * add a lesson to the lessonMap, also add it to the timetable
while (iterator.hasNext()) { */
val it = iterator.next() fun addLesson(courseName: String, subject: String, lesson: Lesson) {
if(it.key.contains("$courseName-$subject")) { //the courseName, subject and lessonID, separator: -
// remove the lesson from the lessons list val key = "$courseName-$subject-${lesson.lessonID}"
iterator.remove() // use iterator to remove, otherwise ConcurrentModificationException lessonMap[key] = lesson
// remove the lesson from the timetable addLessonToTimetable(lesson)
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)
}
/**
* 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)
} }
} }

View File

@ -36,6 +36,7 @@ object Preferences {
var coursesCacheTime: Long = 0 var coursesCacheTime: Long = 0
var mensaCacheTime: Long = 0 var mensaCacheTime: Long = 0
var timetableCacheTime: Long = 0 var timetableCacheTime: Long = 0
var gradesCacheTime: Long = 0
var cColorPrimary: Int = Color.parseColor("#009688") var cColorPrimary: Int = Color.parseColor("#009688")
var cColorAccent: Int = Color.parseColor("#0096ff") var cColorAccent: Int = Color.parseColor("#0096ff")
var cCourse = Course( var cCourse = Course(
@ -67,6 +68,9 @@ object Preferences {
putLong(context.getString(R.string.save_key_timetableCacheTime), putLong(context.getString(R.string.save_key_timetableCacheTime),
timetableCacheTime timetableCacheTime
) )
putLong(context.getString(R.string.save_key_gradesCacheTime),
gradesCacheTime
)
apply() apply()
} }
@ -166,6 +170,9 @@ object Preferences {
timetableCacheTime = sharedPref.getLong(context.getString( timetableCacheTime = sharedPref.getLong(context.getString(
R.string.save_key_timetableCacheTime R.string.save_key_timetableCacheTime
), 0) ), 0)
gradesCacheTime = sharedPref.getLong(context.getString(
R.string.save_key_gradesCacheTime
), 0)
// load saved course // load saved course
cCourse = Course( cCourse = Course(

View File

@ -35,11 +35,13 @@ import kotlinx.android.synthetic.main.fragment_grades.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.mosad.seil0.projectlaogai.R import org.mosad.seil0.projectlaogai.R
import org.mosad.seil0.projectlaogai.controller.QISPOSParser 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.EncryptedPreferences
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences import org.mosad.seil0.projectlaogai.controller.preferences.Preferences
import org.mosad.seil0.projectlaogai.uicomponents.DayCardView import org.mosad.seil0.projectlaogai.uicomponents.DayCardView
import org.mosad.seil0.projectlaogai.uicomponents.GradeLinearLayout import org.mosad.seil0.projectlaogai.uicomponents.GradeLinearLayout
import org.mosad.seil0.projectlaogai.uicomponents.dialogs.LoginDialog import org.mosad.seil0.projectlaogai.uicomponents.dialogs.LoginDialog
import kotlin.system.measureTimeMillis
/** /**
* The grades fragment class * The grades fragment class
@ -64,6 +66,7 @@ class GradesFragment : Fragment() {
parser = QISPOSParser(context!!)// init the parser parser = QISPOSParser(context!!)// init the parser
// TODO if loading from cache, don't check Qispos state
if (checkCredentials() && checkQisposStatus()) { if (checkCredentials() && checkQisposStatus()) {
GlobalScope.launch(Dispatchers.Default) { GlobalScope.launch(Dispatchers.Default) {
addGrades() addGrades()
@ -125,76 +128,89 @@ class GradesFragment : Fragment() {
return statusCode == 200 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) { private fun addGrades() = GlobalScope.launch(Dispatchers.Default) {
withContext(Dispatchers.Main) { val addGradesTime = measureTimeMillis {
refreshLayout_Grades.isRefreshing = true
}
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<String>()
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) { 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<String>()
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
}
} }
println("startup completed in $addGradesTime ms")
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
}
} }
} }

View File

@ -33,7 +33,7 @@ import kotlinx.android.synthetic.main.fragment_timetable.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.mosad.seil0.projectlaogai.R import org.mosad.seil0.projectlaogai.R
import org.mosad.seil0.projectlaogai.controller.cache.TimetableController 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.controller.preferences.Preferences
import org.mosad.seil0.projectlaogai.uicomponents.dialogs.AddSubjectDialog import org.mosad.seil0.projectlaogai.uicomponents.dialogs.AddSubjectDialog
import org.mosad.seil0.projectlaogai.uicomponents.DayCardView import org.mosad.seil0.projectlaogai.uicomponents.DayCardView

View File

@ -127,6 +127,7 @@
<string name="save_key_coursesCacheTime" translatable="false">org.mosad.seil0.projectlaogai.coursesCacheTime</string> <string name="save_key_coursesCacheTime" translatable="false">org.mosad.seil0.projectlaogai.coursesCacheTime</string>
<string name="save_key_mensaCacheTime" translatable="false">org.mosad.seil0.projectlaogai.mensaCacheTime</string> <string name="save_key_mensaCacheTime" translatable="false">org.mosad.seil0.projectlaogai.mensaCacheTime</string>
<string name="save_key_timetableCacheTime" translatable="false">org.mosad.seil0.projectlaogai.timetableCacheTime</string> <string name="save_key_timetableCacheTime" translatable="false">org.mosad.seil0.projectlaogai.timetableCacheTime</string>
<string name="save_key_gradesCacheTime" translatable="false">org.mosad.seil0.projectlaogai.gradesCacheTime</string>
<string name="save_key_user_email" translatable="false">org.mosad.seil0.projectlaogai.user_email</string> <string name="save_key_user_email" translatable="false">org.mosad.seil0.projectlaogai.user_email</string>
<string name="save_key_user_password" translatable="false">org.mosad.seil0.projectlaogai.user_password</string> <string name="save_key_user_password" translatable="false">org.mosad.seil0.projectlaogai.user_password</string>