remove grades/qispos

This commit is contained in:
Jannik 2022-12-22 17:39:42 +01:00
parent 3e52061a20
commit 521045e0dd
Signed by: Seil0
GPG Key ID: E8459F3723C52C24
13 changed files with 2 additions and 1104 deletions

View File

@ -94,7 +94,6 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
getString(R.string.intent_action_mensaFragment) -> activeFragment = MensaFragment()
getString(R.string.intent_action_timetableFragment) -> activeFragment = TimetableFragment()
getString(R.string.intent_action_moodleFragment) -> activeFragment = MoodleFragment()
getString(R.string.intent_action_gradesFragment) -> activeFragment = GradesFragment()
else -> activeFragment = HomeFragment()
}
@ -155,7 +154,6 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
R.id.nav_mensa -> MensaFragment()
R.id.nav_timetable -> TimetableFragment()
R.id.nav_moodle -> MoodleFragment()
R.id.nav_grades -> GradesFragment()
R.id.nav_settings -> SettingsFragment()
else -> HomeFragment()
}

View File

@ -1,99 +0,0 @@
/**
* ProjectLaogai
*
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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
import android.content.Context
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences
import org.mosad.seil0.projectlaogai.util.GradeSubject
import org.mosad.seil0.projectlaogai.worker.GradesUpdateWorker
import java.util.concurrent.TimeUnit
class GradesController {
/**
* show the difference between 2 grades sets
*/
fun diffGrades(origMap: Map<String, ArrayList<GradeSubject>>, diffMap: Map<String, ArrayList<GradeSubject>>): ArrayList<GradeSubject> {
val diff = ArrayList<GradeSubject>()
diffMap.values.forEach { semester ->
// if it's the same, no need to compare
if (!origMap.containsValue(semester)) {
semester.forEach { gradeSubject ->
// for each of the grades, check if it differs from the origMap
if (origMap.containsKey(gradeSubject.semester)) {
// a new or changed subject
if (gradeSubject !in origMap[gradeSubject.semester]!!) {
diff.add(gradeSubject)
}
} else {
// a new semester
diff.add(gradeSubject)
}
}
}
}
return diff
}
companion object {
/**
* stop the grades background sync, if the sync interval is greater than 0
* start it again with the new interval
*/
fun updateBackgroundSync(context: Context) {
stopBackgroundSync(context)
// if interval is not 0, start background Sync
if (Preferences.gradesSyncInterval > 0)
startBackgroundSync(context)
}
/**
* start a new periodic worker GradesUpdateWorker
*/
fun startBackgroundSync(context: Context) {
val work = PeriodicWorkRequestBuilder<GradesUpdateWorker>(
Preferences.gradesSyncInterval.toLong(),
TimeUnit.HOURS
).build()
val workManager = WorkManager.getInstance(context)
workManager.enqueueUniquePeriodicWork("GradesUpdateWorker", ExistingPeriodicWorkPolicy.REPLACE, work)
}
/**
* cancel GradesUpdateWorker
*/
fun stopBackgroundSync(context: Context) {
WorkManager.getInstance(context).cancelUniqueWork("GradesUpdateWorker")
}
}
}

View File

@ -1,240 +0,0 @@
/**
* ProjectLaogai
*
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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
import android.content.Context
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.jsoup.HttpStatusException
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import org.mosad.seil0.projectlaogai.R
import org.mosad.seil0.projectlaogai.controller.preferences.EncryptedPreferences
import org.mosad.seil0.projectlaogai.util.GradeSubject
import java.security.KeyStore
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.util.*
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.TrustManagerFactory
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
/**
* Parse the qispos site the get all needed data for the grades fragment
*/
class QISPOSParser(val context: Context) {
private val className = this.javaClass.name
private val baseURL = "https://notenverwaltung.hs-offenburg.de"
private val loginPath = "/qispos/rds?state=user&type=1&category=auth.login&startpage=portal.vm&breadCrumbSource=portal"
/**
* check if qispos is available
* @return a http status code, 999 if no HttpStatusException supplied
*/
fun checkQISPOSStatus(): Int {
return runBlocking {
withContext(Dispatchers.IO) {
val socketFactory = createSSLSocketFactory()
try {
val res = Jsoup.connect(baseURL + loginPath)
.sslSocketFactory(socketFactory)
.followRedirects(true)
.referrer("https://notenverwaltung.hs-offenburg.de/qispos/rds?state=user&type=0")
.execute()
Log.i(className, "Qispos status is: ${res.statusCode()}")
res.statusCode()
} catch (exHttp: HttpStatusException) {
Log.w(className, "Qispos status is: ${exHttp.statusCode}")
exHttp.statusCode
} catch (ex: Exception) {
Log.e(className, "Error while checking status", ex)
999
}
}
}
}
/**
* parse the html from readGrades()
* @return a SortedMap, each entry is a semester, each semester has a ArrayList with subjects
*/
fun parseGrades(): SortedMap<String, ArrayList<GradeSubject>> {
val gradesMap = HashMap<String, ArrayList<GradeSubject>>()
val gradesListHtml = readGrades()
gradesListHtml?.select("table > tbody > tr")?.forEach {
val row = it.select("td.tabelle1_alignleft,td.tabelle1_aligncenter,td.tabelle1_alignright")
// only real subjects will be selected
if(row.size >= 6 && row[0].text().length >=7) {
val subject = GradeSubject(
id = row[0].text(),
name = row[1].text(),
semester = row[2].text(),
grade = if (row[3].text().isNotEmpty()) row[3].text() else row[4].text(),
credits = row[5].text()
)
if (gradesMap.containsKey(subject.semester)) {
gradesMap[subject.semester]!!.add(subject)
} else {
gradesMap[subject.semester] = arrayListOf(subject)
}
}
}
return gradesMap.toSortedMap(compareBy<String>{
val oText = it.substringAfter(" ")
// if WS, substring before / and add 0.5 to compareBy
if (oText.contains("/")) {
oText.substringBefore("/").toInt() + 0.5
} else {
oText.toDouble()
}
}.thenBy { it })
}
/**
* read the grades html from qispos
* @return the grades list as html element or null
*/
private fun readGrades(): Element?{
val credentials = EncryptedPreferences.readCredentials(context)
val username = credentials.first.substringBefore("@")
val password = credentials.second
return runBlocking {
withContext(Dispatchers.IO) {
try {
val socketFactory = createSSLSocketFactory()
// login, asdf = username, fdsa = password, wtf
val list = mapOf(
Pair("asdf", username),
Pair("fdsa", password),
Pair("submit", "Anmelden")
)
// login and get session cookies
val res = Jsoup.connect(baseURL + loginPath)
.sslSocketFactory(socketFactory)
.followRedirects(true)
.referrer("https://notenverwaltung.hs-offenburg.de/qispos/rds?state=user&type=0")
.data(list)
.postDataCharset("UTF-8")
.execute()
Log.i(className, "Login status is: ${res.statusCode()} (${res.statusMessage()})")
val loginCookies = res.cookies()
// grades root document and url
val rootHtml =Jsoup.parse(res.body())
val gradesRootLink =
rootHtml.select("li.menueListStyle > a.auflistung").last()!!.attr("abs:href")
// parse grades url
val gradesHtml = Jsoup.connect(gradesRootLink)
.sslSocketFactory(socketFactory)
.followRedirects(true)
.cookies(loginCookies)
.referrer("https://notenverwaltung.hs-offenburg.de/qispos/rds?state=user&type=0")
.get()
val gradesNextLink = gradesHtml.select("li.treelist > a.regular").attr("abs:href")
val gradesNextHtml = Jsoup.connect(gradesNextLink)
.sslSocketFactory(socketFactory)
.followRedirects(true)
.cookies(loginCookies)
.referrer("https://notenverwaltung.hs-offenburg.de/qispos/rds?state=user&type=0")
.get()
val gradesListLink = gradesNextHtml.selectFirst("li.treelist > ul > li")!!.selectFirst("a")!!.attr("abs:href")
// get the grades list
val gradesListHtml = Jsoup.connect(gradesListLink)
.sslSocketFactory(socketFactory)
.followRedirects(true)
.cookies(loginCookies)
.referrer("https://notenverwaltung.hs-offenburg.de/qispos/rds?state=user&type=0")
.get()
Log.i(className, "Read html length: ${gradesListHtml.body().html().length}")
gradesListHtml.body()
} catch (ex: Exception) {
Log.e(className, "Error while loading Qispos", ex)
null
}
}
}
}
/**
* since the HS has a fucked up tls setup we need to work around that
*/
private fun createSSLSocketFactory(): SSLSocketFactory {
// Load CAs from an InputStream
// (could be from a resource or ByteArrayInputStream or ...)
val cf: CertificateFactory = CertificateFactory.getInstance("X.509")
val caInput = context.resources.openRawResource(R.raw.notenverwaltung_hs_offenburg_de)
val ca = caInput.use {
cf.generateCertificate(it) as X509Certificate
}
// Create a KeyStore containing our trusted CAs
val keyStoreType = KeyStore.getDefaultType()
val keyStore = KeyStore.getInstance(keyStoreType).apply {
load(null, null)
setCertificateEntry("ca", ca)
}
// Create a TrustManager that trusts the CAs inputStream our KeyStore
val tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm()
val tmf = TrustManagerFactory.getInstance(tmfAlgorithm).apply {
init(keyStore)
}
// Create an SSLContext that uses our TrustManager
val sslContext = SSLContext.getInstance("TLS").apply {
init(null, tmf.trustManagers, null)
}
return sslContext.socketFactory
}
}

View File

@ -24,18 +24,14 @@ 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.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.*
@ -202,23 +198,6 @@ class CacheController(private val context: Context) {
}
}
/**
* update the encrypted grades file
*/
fun updateGrades(context: Context): Job {
val file = File(context.filesDir, "grades_encrypted")
val parser = QISPOSParser(context)
return CoroutineScope(Dispatchers.IO).launch {
if (parser.checkQISPOSStatus() == 200) {
// 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
@ -240,28 +219,6 @@ class CacheController(private val context: Context) {
Log.e(className, "failed to write file \"${file.absoluteFile}\"", ex)
}
}
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()
// only write one file at a time
synchronized(this) {
if (file.exists()) { file.delete() }
encryptedFile.openFileOutput().bufferedWriter().use {
it.write(text)
}
}
}
}
/**
@ -411,48 +368,4 @@ class CacheController(private val context: Context) {
}
/**
* read the encrypted grades file, don't keep them
* in CacheController for security reasons
* @return the grades as SortedMap if the file exists, else a empty SortedMap
*/
fun readGrades(): SortedMap<String, java.util.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()
val map: HashMap<String, ArrayList<GradeSubject>> = encryptedFile.openFileInput().bufferedReader().use {
GsonBuilder().create()
.fromJson(it, object : TypeToken<HashMap<String, ArrayList<GradeSubject>>>() {}.type)
}
// same sorting as qispos parser
return map.toSortedMap(compareBy<String>{
val oText = it.substringAfter(" ")
if (oText.contains("/")) {
oText.substringBefore("/").toInt() + 0.5
} else {
oText.toDouble()
}
}.thenBy { it })
}
return sortedMapOf()
}
}

View File

@ -1,228 +0,0 @@
/**
* ProjectLaogai
*
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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.fragments
import android.graphics.Rect
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
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.databinding.FragmentGradesBinding
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
* contains all needed parts to display and the grades screen
*/
class GradesFragment : Fragment() {
private lateinit var parser: QISPOSParser
private lateinit var binding: FragmentGradesBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentGradesBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.refreshLayoutGrades.setProgressBackgroundColorSchemeColor(Preferences.themeSecondary)
initActions()
parser = QISPOSParser(requireContext())// init the parser
if (checkCredentials()) {
lifecycleScope.launch(Dispatchers.Default) {
// if the cache is older than 24hr, update blocking
val currentTime = System.currentTimeMillis() / 1000
withContext(Dispatchers.Main) {
if ((currentTime - Preferences.gradesCacheTime) > 86400 && checkQisposStatus()) {
binding.refreshLayoutGrades.isRefreshing = true
CacheController.updateGrades(requireContext()).join()
}
}
addGrades()
}
}
}
/**
* initialize the actions
*/
private fun initActions() = lifecycleScope.launch(Dispatchers.Default) {
binding.refreshLayoutGrades.setOnRefreshListener {
binding.linLayoutGrades.removeAllViews() // clear layout
// TODO add loading textView
lifecycleScope.launch(Dispatchers.Default) {
CacheController.updateGrades(requireContext()).join()
addGrades()
}
}
}
/**
* check if the credentials are set, if not show a login dialog
*/
private fun checkCredentials(): Boolean {
val credentials = EncryptedPreferences.readCredentials(requireContext())
var credentialsPresent = false
// if there is no password set, show the login dialog
if (credentials.first.isEmpty() || credentials.second.isEmpty()) {
LoginDialog(this.requireContext())
.positiveButton {
EncryptedPreferences.saveCredentials(email, password, context)
addGrades()
}
.negativeButton {
binding.txtViewLoading.text = resources.getString(R.string.credentials_missing)
}
.show {
email = EncryptedPreferences.email
password = ""
}
} else {
credentialsPresent = true
}
return credentialsPresent
}
/**
* check if qispos is available, if not show an error
*/
private fun checkQisposStatus(): Boolean {
val statusCode = parser.checkQISPOSStatus()
// show error if the status code is not 200
if (statusCode != 200) {
val infoText = resources.getString(when(statusCode) {
503 -> R.string.qispos_unavailable
else -> R.string.qispos_generic_error
})
val img = ResourcesCompat.getDrawable(resources, R.drawable.ic_error_outline_black_24dp, null)?.apply {
bounds = Rect(0, 0, 75, 75)
}
binding.txtViewLoading.apply {
text = infoText
setCompoundDrawables(null, null, null, img)
}
}
return statusCode == 200
}
/**
* add the grades to the layout, async
* TODO this is slow as hell
*/
private fun addGrades() = lifecycleScope.launch(Dispatchers.Default) {
val addGradesTime = measureTimeMillis {
val grades = CacheController(requireContext()).readGrades()
withContext(Dispatchers.Main) {
binding.linLayoutGrades.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(requireContext())
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) {
binding.linLayoutGrades.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) {
binding.linLayoutGrades.addView(txtViewLegal)
binding.refreshLayoutGrades.isRefreshing = false
}
}
Log.i(javaClass.name, "added grades in $addGradesTime ms")
}
}

View File

@ -34,7 +34,6 @@ import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.WhichButton
import com.afollestad.materialdialogs.actions.getActionButton
import com.afollestad.materialdialogs.list.listItemsMultiChoice
import com.afollestad.materialdialogs.list.listItemsSingleChoice
import de.psdev.licensesdialog.LicensesDialog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.joinAll
@ -42,7 +41,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.mosad.seil0.projectlaogai.BuildConfig
import org.mosad.seil0.projectlaogai.R
import org.mosad.seil0.projectlaogai.controller.GradesController
import org.mosad.seil0.projectlaogai.controller.cache.CacheController
import org.mosad.seil0.projectlaogai.controller.cache.CacheController.Companion.coursesList
import org.mosad.seil0.projectlaogai.controller.cache.TimetableController
@ -80,11 +78,6 @@ class SettingsFragment : Fragment() {
binding.textUser.text = EncryptedPreferences.email.ifEmpty { resources.getString(R.string.sample_user) }
binding.textCourse.text = Preferences.course.courseName
binding.textAboutDesc.text = resources.getString(R.string.about_version, BuildConfig.VERSION_NAME, getString(R.string.build_time))
binding.textGradesSyncDesc.text = if (Preferences.gradesSyncInterval == 0) {
resources.getString(R.string.grades_sync_desc_never)
} else {
resources.getString(R.string.grades_sync_desc, Preferences.gradesSyncInterval)
}
binding.switchBuffet.isChecked = Preferences.showBuffet // init switch
val outValue = TypedValue()
@ -278,38 +271,6 @@ class SettingsFragment : Fragment() {
// }
// }
binding.linLayoutGradesSync.setOnClickListener {
val items = resources.getStringArray(R.array.syncInterval).toList()
val initial = when (Preferences.gradesSyncInterval) {
1 -> 1
3 -> 2
6 -> 3
12 -> 4
else -> 0
}
MaterialDialog(requireContext())
.title(R.string.grades_sync)
.listItemsSingleChoice(items = items, initialSelection = initial) { _, index, _ ->
val interval = when (index) {
1 -> 1
2 -> 3
3 -> 6
4 -> 12
else -> 0
}
Preferences.saveGradesSync(requireContext(), interval)
GradesController.updateBackgroundSync(requireContext())
binding.textGradesSyncDesc.text = if (Preferences.gradesSyncInterval == 0) {
resources.getString(R.string.grades_sync_desc_never)
} else {
resources.getString(R.string.grades_sync_desc, Preferences.gradesSyncInterval)
}
}.show()
}
binding.switchBuffet.setOnClickListener {
Preferences.saveShowBuffet(requireContext(), binding.switchBuffet.isChecked)
}

View File

@ -1,56 +0,0 @@
/**
* ProjectLaogai
*
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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.uicomponents
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import org.mosad.seil0.projectlaogai.databinding.LinearlayoutGradeBinding
class GradeLinearLayout(context: Context?): LinearLayout(context) {
private val binding = LinearlayoutGradeBinding.inflate(LayoutInflater.from(context), this, true)
var subjectName = ""
var grade = ""
var subSubjectName = ""
var subGrade = ""
fun set(func: GradeLinearLayout.() -> Unit): GradeLinearLayout = apply {
func()
binding.textSubject.text = subjectName
binding.textGrade.text = grade
binding.textSubSubject.text = subSubjectName
binding.textSubGrade.text = subGrade
}
fun disableDivider() {
binding.dividerGrade.visibility = View.GONE
}
fun disableSubSubject() {
binding.linearSubSubject.visibility = View.GONE
}
}

View File

@ -1,112 +0,0 @@
/**
* ProjectLaogai
*
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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.worker
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.work.Worker
import androidx.work.WorkerParameters
import kotlinx.coroutines.runBlocking
import org.mosad.seil0.projectlaogai.MainActivity
import org.mosad.seil0.projectlaogai.R
import org.mosad.seil0.projectlaogai.controller.GradesController
import org.mosad.seil0.projectlaogai.controller.cache.CacheController
import org.mosad.seil0.projectlaogai.controller.preferences.EncryptedPreferences
import org.mosad.seil0.projectlaogai.util.NotificationUtils
import org.mosad.seil0.projectlaogai.util.NotificationUtils.Companion.CHANNEL_ID_GRADES
class GradesUpdateWorker(val context: Context, params: WorkerParameters): Worker(context, params) {
override fun doWork(): Result {
// check if credentials are present, if not do nothing
val credentials = EncryptedPreferences.readCredentials(context)
if (credentials.first.isEmpty() || credentials.second.isEmpty()) {
return Result.success()
}
val notificationIdChecking = NotificationUtils.getId()
val builderChecking = NotificationCompat.Builder(context, CHANNEL_ID_GRADES)
.setSmallIcon(R.drawable.ic_grading_black_24dp)
.setContentTitle(context.getString(R.string.notification_grades))
.setContentText(context.getString(R.string.notification_grades_updating_desc))
.setNotificationSilent()
NotificationManagerCompat.from(context).apply {
notify(notificationIdChecking, builderChecking.build())
}
// get old grades
val oldGrades = CacheController(context).readGrades()
// get update from qispos
runBlocking { CacheController.updateGrades(context).join() }
val newGrades = CacheController(context).readGrades()
// check for changes
val diff = GradesController().diffGrades(oldGrades, newGrades)
// show message
if (diff.isNotEmpty()) {
val text = if (diff.size < 2) {
context.getString(R.string.notification_grades_single_desc, diff.first().name)
} else {
context.getString(R.string.notification_grades_multiple_desc, diff.first().name, (diff.size - 1))
}
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
action = context.getString(R.string.intent_action_gradesFragment)
}
val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
val builder = NotificationCompat.Builder(context, CHANNEL_ID_GRADES)
.setSmallIcon(R.drawable.ic_grading_black_24dp)
.setContentTitle(context.getString(R.string.notification_grades))
.setContentText(text)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
// if there are multiple subjects, use BigText
if (diff.size > 1)
builder.setStyle(NotificationCompat.BigTextStyle().bigText(text))
NotificationManagerCompat.from(context).apply {
notify(NotificationUtils.getId(), builder.build())
}
}
// remove scanning notification
NotificationManagerCompat.from(context).apply {
cancel(notificationIdChecking)
}
return Result.success()
}
}

View File

@ -1,70 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/linLayout_grade"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="7dp"
android:paddingTop="2dp"
android:paddingRight="7dp"
android:paddingBottom="3dp">
<LinearLayout
android:id="@+id/linear_subject"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:id="@+id/text_subject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/sample_subject"
android:textSize="15sp" />
<TextView
android:id="@+id/text_grade"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:text="@string/sample_grade"
android:textAlignment="textEnd"
android:textSize="15sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/linear_sub_subject"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:paddingStart="7dp"
android:paddingTop="3dp"
android:paddingEnd="0dp">
<TextView
android:id="@+id/text_sub_subject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/sample_sub_subject" />
<TextView
android:id="@+id/text_sub_grade"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:text="@string/sample_grade_state"
android:textAlignment="textEnd" />
</LinearLayout>
<View
android:id="@+id/divider_grade"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="3dp"
android:background="?dividerColor" />
</LinearLayout>

View File

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?themePrimary"
tools:context=".fragments.GradesFragment">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/refreshLayout_Grades"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:id="@+id/scrollView_Grades"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/linLayout_Grades"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/txtView_Loading"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="100dp"
android:padding="7dp"
android:text="@string/loading_from_hs"
android:textAlignment="center"
android:textSize="15sp" />
</LinearLayout>
</ScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</FrameLayout>

View File

@ -18,7 +18,8 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:orientation="vertical"
android:paddingBottom="11dp">
<androidx.cardview.widget.CardView
android:id="@+id/cardView_Info"
@ -340,35 +341,6 @@
android:layout_height="1dp"
android:background="?android:attr/listDivider" />
<LinearLayout
android:id="@+id/linLayout_GradesSync"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:selectableItemBackground"
android:orientation="vertical"
android:padding="7dp">
<TextView
android:id="@+id/text_grades_sync"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/grades_sync"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/text_grades_sync_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/grades_sync_desc" />
</LinearLayout>
<View
android:id="@+id/divider8"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switch_buffet"
android:layout_width="match_parent"

View File

@ -20,10 +20,6 @@
android:id="@+id/nav_moodle"
android:icon="@drawable/ic_school_black_24dp"
android:title="@string/moodle"/>
<item
android:id="@+id/nav_grades"
android:icon="@drawable/ic_grading_black_24dp"
android:title="@string/grades" />
<item
android:id="@+id/nav_settings"
android:icon="@drawable/ic_settings_black_24dp"

View File

@ -1,99 +0,0 @@
/**
* ProjectLaogai
*
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.mosad.seil0.projectlaogai.controller.GradesController
import org.mosad.seil0.projectlaogai.util.GradeSubject
import java.io.File
import java.io.FileReader
class GradesControllerTest {
@Test
fun diffGrades_noDiff() {
val origFile = File(GradesControllerTest::class.java.getResource("/grades_orig.json")?.path!!)
val diffFile = File(GradesControllerTest::class.java.getResource("/grades_orig.json")?.path!!)
val mapA: HashMap<String, ArrayList<GradeSubject>> = FileReader(origFile).use {
GsonBuilder().create().fromJson(it, object : TypeToken<HashMap<String, ArrayList<GradeSubject>>>() {}.type)
}
val mapB: HashMap<String, ArrayList<GradeSubject>> = FileReader(diffFile).use {
GsonBuilder().create().fromJson(it, object : TypeToken<HashMap<String, ArrayList<GradeSubject>>>() {}.type)
}
val exp = ArrayList<GradeSubject>()
val ret = GradesController().diffGrades(mapA, mapB)
Assertions.assertEquals(exp, ret)
}
@Test
fun diffGrades_diffSubject() {
val origFile = File(GradesControllerTest::class.java.getResource("/grades_orig.json")?.path!!)
val diffFile = File(GradesControllerTest::class.java.getResource("/grades_diff_subject.json")?.path!!)
val mapA: HashMap<String, ArrayList<GradeSubject>> = FileReader(origFile).use {
GsonBuilder().create().fromJson(it, object : TypeToken<HashMap<String, ArrayList<GradeSubject>>>() {}.type)
}
val mapB: HashMap<String, ArrayList<GradeSubject>> = FileReader(diffFile).use {
GsonBuilder().create().fromJson(it, object : TypeToken<HashMap<String, ArrayList<GradeSubject>>>() {}.type)
}
val exp = arrayListOf(
GradeSubject("AI-3010", "Computernetze", "WiSe 18/19", "0,7", "2,0"),
GradeSubject("AI-3020", "Datenbanksysteme 1", "WiSe 18/19", "1,7", "2,0"),
GradeSubject("AI-3025", "Praktikum Datenbanksysteme", "WiSe 18/19", "bestanden", "3,0")
)
val ret = GradesController().diffGrades(mapA, mapB)
Assertions.assertEquals(exp, ret)
}
@Test
fun diffGrades_diffSemester() {
val origFile = File(GradesControllerTest::class.java.getResource("/grades_orig.json")?.path!!)
val diffFile = File(GradesControllerTest::class.java.getResource("/grades_diff_semester.json")?.path!!)
val mapA: HashMap<String, ArrayList<GradeSubject>> = FileReader(origFile).use {
GsonBuilder().create().fromJson(it, object : TypeToken<HashMap<String, ArrayList<GradeSubject>>>() {}.type)
}
val mapB: HashMap<String, ArrayList<GradeSubject>> = FileReader(diffFile).use {
GsonBuilder().create().fromJson(it, object : TypeToken<HashMap<String, ArrayList<GradeSubject>>>() {}.type)
}
val exp = arrayListOf(
GradeSubject("AI-2010", "Mathemaik 7", "SoSe 19", "1,7", "4,0"),
GradeSubject("AI-2015", "Praktikum Mathemaik 7", "SoSe 19", "bestanden", "1,0")
)
val ret = GradesController().diffGrades(mapA, mapB)
Assertions.assertEquals(exp, ret)
}
}