save grades to a encrypted cache file, use cache if not older than 24hr
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
This commit is contained in:
parent
313ff4741a
commit
d94f38de93
|
@ -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'
|
||||
}
|
||||
|
||||
|
|
|
@ -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<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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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<TimetableWeek>()
|
||||
val lessonMap = HashMap<String, Lesson>() // the key is courseName-subject-lessonID
|
||||
val subjectMap = HashMap<String, ArrayList<String>>() // the key is courseName
|
||||
val timetable = ArrayList<TimetableWeek>()
|
||||
val lessonMap = HashMap<String, Lesson>() // the key is courseName-subject-lessonID
|
||||
val subjectMap = HashMap<String, ArrayList<String>>() // the key is courseName
|
||||
|
||||
/**
|
||||
* update the main timetable and all additional subjects, async
|
||||
*/
|
||||
fun update(context: Context): List<Job> {
|
||||
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<Job> {
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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<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)
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -127,6 +127,7 @@
|
|||
<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_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_password" translatable="false">org.mosad.seil0.projectlaogai.user_password</string>
|
||||
|
||||
|
|
Loading…
Reference in New Issue