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,9 +38,8 @@ 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
@ -126,6 +125,4 @@ class TimetableController {
timetable[0].days[id[0].toInt()].timeslots[id[1].toInt()].add(lesson) 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,18 +128,30 @@ 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) {
val addGradesTime = measureTimeMillis {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
refreshLayout_Grades.isRefreshing = true refreshLayout_Grades.isRefreshing = true
} }
val grades = parser.parseGrades() // 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) { withContext(Dispatchers.Main) {
linLayout_Grades.removeAllViews() // clear layout linLayout_Grades.removeAllViews() // clear layout
} }
// TODO this loop takes 3/4 of the time
// for each semester add a new card // for each semester add a new card
grades.forEach { semester -> grades.forEach { semester ->
val usedSubjects = ArrayList<String>() val usedSubjects = ArrayList<String>()
@ -183,8 +198,6 @@ class GradesFragment : Fragment() {
} }
} }
val txtViewLegal = TextView(context).apply { val txtViewLegal = TextView(context).apply {
text = resources.getString(R.string.without_guarantee) text = resources.getString(R.string.without_guarantee)
textAlignment = View.TEXT_ALIGNMENT_CENTER textAlignment = View.TEXT_ALIGNMENT_CENTER
@ -195,6 +208,9 @@ class GradesFragment : Fragment() {
linLayout_Grades.addView(txtViewLegal) linLayout_Grades.addView(txtViewLegal)
refreshLayout_Grades.isRefreshing = false refreshLayout_Grades.isRefreshing = false
} }
}
println("startup completed in $addGradesTime ms")
} }
} }

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>