grades ui polishing & sorting fix
continuous-integration/drone/push Build is passing Details

This commit is contained in:
Jannik 2020-08-15 18:13:29 +02:00
parent 42d938b0bc
commit 638c321798
Signed by: Seil0
GPG Key ID: E8459F3723C52C24
14 changed files with 194 additions and 88 deletions

View File

@ -11,7 +11,7 @@ android {
minSdkVersion 23
targetSdkVersion 29
versionCode 15
versionName "0.5.92"
versionName "0.5.93-RC1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "build_time", buildTime()
setProperty("archivesBaseName", "projectlaogai-$versionName")

View File

@ -35,34 +35,33 @@ 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
/**
* TODO context in constructor?
* Parse the qispos site the get all needed data for the grades fragment
*/
class QISPOSParser {
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"
//private val rootPath = "/qispos/rds?state=change&type=1&moduleParameter=studyPOSMenu&nextdir=change&next=menu.vm&subdir=applications&xml=menu&purge=y&navigationPosition=functions%2CstudyPOSMenu&breadcrumb=studyPOSMenu&topitem=functions&subitem=studyPOSMenu"
fun parseGrades(context: Context): HashMap<String, ArrayList<gradeSubject>> {
/**
* 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(context)
val gradesListHtml = readGrades()
gradesListHtml?.select("table > tbody > tr")?.forEach {
//val row = it.select("td.qis_konto")
val row = it.select("td.tabelle1_alignleft,td.tabelle1_aligncenter,td.tabelle1_alignright")
//println("-----------------------------------------------------------")
//println(it.select("td.qis_konto"))
//if(row.size >= 6) {
// only real subjects will be selected
if(row.size >= 6 && row[0].text().length >=7) {
val subject = gradeSubject(
@ -75,39 +74,30 @@ class QISPOSParser {
if (gradesMap.containsKey(subject.semester)) {
gradesMap[subject.semester]!!.add(subject)
println(subject.name)
} else {
gradesMap[subject.semester] = arrayListOf(subject)
println(subject.name)
}
println("ID: ${row[0].text()}")
println("Subject Name: ${row[1].text()}")
println("Semester: ${row[2].text()}")
println("Grade: ${row[3].text()}")
println("Credits: ${row[5].text()}")
println("------------------------------------------------------")
}
}
println(gradesMap)
gradesMap.forEach {
println("${it.key}: ${it.value}")
// return the sorted map
return gradesMap.toSortedMap(compareBy<String>{
val oText = it.substringAfter(" ")
if (oText.contains("/")) {
oText.substringBefore("/").toInt() + 0.5
} else {
oText.toDouble()
}
// TODO sort
println("finished parsing!")
return gradesMap
}.thenBy { it })
}
/**
* read the grades html from qispos
* @return the grades list as html element or null
*/
private fun readGrades(context: Context): Element?{
private fun readGrades(): Element?{
val credentials = EncryptedPreferences.readCredentials(context)
val username = credentials.first.substringBefore("@")
@ -116,10 +106,7 @@ class QISPOSParser {
return runBlocking {
withContext(Dispatchers.IO) {
try {
println("querying qispos ...")
println("using $username with ********")
val socketFactory = createSSLSocketFactory(context)
val socketFactory = createSSLSocketFactory()
// login, asdf = username, fdsa = password, wtf
val list = mapOf(
@ -136,16 +123,13 @@ class QISPOSParser {
.data(list)
.postDataCharset("UTF-8")
.execute()
Log.i(className, "login status is: ${res.statusMessage()}: ${res.statusCode()}")
val loginCookies = res.cookies()
println("cookies: $loginCookies")
println("status is: ${res.statusMessage()}: ${res.statusCode()}")
// grades root document and url
val rootHtml =Jsoup.parse(res.body())
val gradesRootLink = rootHtml.select("li.menueListStyle > a.auflistung").last().attr("abs:href")
println(gradesRootLink)
// parse grades url
val gradesHtml = Jsoup.connect(gradesRootLink)
@ -156,7 +140,6 @@ class QISPOSParser {
.get()
val gradesNextLink = gradesHtml.select("li.treelist > a.regular").attr("abs:href")
println(gradesNextLink)
val gradesNextHtml = Jsoup.connect(gradesNextLink)
.sslSocketFactory(socketFactory)
@ -166,7 +149,6 @@ class QISPOSParser {
.get()
val gradesListLink = gradesNextHtml.selectFirst("li.treelist > ul > li").selectFirst("a").attr("abs:href")
println(gradesListLink)
// get the grades list
val gradesListHtml = Jsoup.connect(gradesListLink)
@ -179,7 +161,7 @@ class QISPOSParser {
//println(gradesListHtml.body())
gradesListHtml.body()
} catch (ex: Exception) {
Log.e(className, "could not load qispos", ex)
Log.e(className, "error while loading qispos", ex)
null
}
}
@ -187,7 +169,10 @@ class QISPOSParser {
}
private fun createSSLSocketFactory(context: Context): SSLSocketFactory {
/**
* 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")
@ -195,7 +180,6 @@ class QISPOSParser {
val ca = caInput.use {
cf.generateCertificate(it) as X509Certificate
}
println("ca=" + ca.subjectDN)
// Create a KeyStore containing our trusted CAs
val keyStoreType = KeyStore.getDefaultType()

View File

@ -71,8 +71,7 @@ object EncryptedPreferences {
fun load(context: Context) {
with(getEncryptedPreferences(context)) {
email = this?.getString(
context.getString(R.string.save_key_user_email),
context.getString(R.string.sample_user)
context.getString(R.string.save_key_user_email), ""
).toString()
}
}

View File

@ -20,13 +20,13 @@
*
*/
package org.mosad.seil0.projectlaogai.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import kotlinx.android.synthetic.main.fragment_grades.*
@ -36,11 +36,14 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.mosad.seil0.projectlaogai.R
import org.mosad.seil0.projectlaogai.controller.QISPOSParser
import org.mosad.seil0.projectlaogai.controller.preferences.EncryptedPreferences
import org.mosad.seil0.projectlaogai.uicomponents.DayCardView
import org.mosad.seil0.projectlaogai.uicomponents.GradeLinearLayout
import org.mosad.seil0.projectlaogai.uicomponents.dialogs.LoginDialog
/**
* A simple [Fragment] subclass.
* The grades fragment class
* contains all needed parts to display and the grades screen
*/
class GradesFragment : Fragment() {
@ -52,20 +55,51 @@ class GradesFragment : Fragment() {
refreshLayoutGrades = view.findViewById(R.id.refreshLayout_Grades)
refreshLayoutGrades.isEnabled = false // disable swipe
if (checkCredentials()) {
GlobalScope.launch(Dispatchers.Default) {
addGrades()
}
}
return view
}
/**
* check if the credentials are set, if not show a login dialog
*/
private fun checkCredentials(): Boolean {
val credentials = EncryptedPreferences.readCredentials(context!!)
var credentialsPresent = false
// if there is no password set, show the login dialog
if (credentials.first.isEmpty() || credentials.second.isEmpty()) {
LoginDialog(this.context!!)
.positiveButton {
EncryptedPreferences.saveCredentials(email, password, context)
addGrades()
}
.negativeButton {
txtView_Loading.text = resources.getString(R.string.credentials_missing)
}
.show {
email = EncryptedPreferences.email
password = ""
}
} else {
credentialsPresent = true
}
return credentialsPresent
}
// add the grades to the layout, async
private fun addGrades() = GlobalScope.launch(Dispatchers.Default) {
withContext(Dispatchers.Main) {
refreshLayout_Grades.isRefreshing = true
}
val parser = QISPOSParser()
val grades = parser.parseGrades(context!!)
val parser = QISPOSParser(context!!)
val grades = parser.parseGrades()
withContext(Dispatchers.Main) {
linLayout_Grades.removeAllViews() // clear layout
@ -73,16 +107,40 @@ class GradesFragment : Fragment() {
// 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()
// FIXME this is broken
// disable divider if last element
//if (index == semester.value.lastIndex || semester.value.indexOf(subSubject) == semester.value.lastIndex)
if (index == semester.value.lastIndex)
subjectLayout.disableDivider()
@ -94,11 +152,17 @@ class GradesFragment : Fragment() {
linLayout_Grades.addView(semesterCard)
}
}
withContext(Dispatchers.Main) {
refreshLayout_Grades.isRefreshing = false
val txtViewLegal = TextView(context).apply {
text = resources.getString(R.string.without_guarantee)
textAlignment = View.TEXT_ALIGNMENT_CENTER
}
//refreshLayoutGrades.isRefreshing = false
// stop refreshing and show legal warning
withContext(Dispatchers.Main) {
linLayout_Grades.addView(txtViewLegal)
refreshLayout_Grades.isRefreshing = false
}
}
}

View File

@ -105,7 +105,7 @@ class SettingsFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
// initialize the settings gui
txtView_User.text = EncryptedPreferences.email
txtView_User.text = EncryptedPreferences.email.ifEmpty { resources.getString(R.string.sample_user) }
txtView_Course.text = cCourse.courseName
txtView_AboutDesc.text = resources.getString(R.string.about_version, BuildConfig.VERSION_NAME, getString(R.string.build_time))
switch_buffet.isChecked = cShowBuffet // init switch
@ -140,7 +140,6 @@ class SettingsFragment : Fragment() {
// open a new dialog
LoginDialog(context!!)
.positiveButton {
println("Test: $password")
EncryptedPreferences.saveCredentials(email, password, context)
}
.show {

View File

@ -33,6 +33,8 @@ class GradeLinearLayout(context: Context?): LinearLayout(context) {
var subjectName = ""
var grade = ""
var subSubjectName = ""
var subGrade = ""
init {
CardView.inflate(context, R.layout.linearlayout_grade, this)
@ -43,9 +45,15 @@ class GradeLinearLayout(context: Context?): LinearLayout(context) {
txtView_subject.text = subjectName
txtView_grade.text = grade
txtView_sub_subject.text = subSubjectName
txtView_sub_grade.text = subGrade
}
fun disableDivider() {
divider_grades.visibility = View.GONE
divider_grade.visibility = View.GONE
}
fun disableSubSubject() {
linLayout_sub_subject.visibility = View.GONE
}
}

View File

@ -45,8 +45,8 @@ class LoginDialog(val context: Context) {
var password = ""
init {
dialog.title(R.string.grades_heading)
.message(R.string.grades_desc_on)
dialog.title(R.string.login_heading)
.message(R.string.login_desc_on)
.customView(R.layout.dialog_login)
.positiveButton(R.string.save)
.negativeButton(R.string.cancel)
@ -69,6 +69,12 @@ class LoginDialog(val context: Context) {
}
}
fun negativeButton(func: LoginDialog.() -> Unit): LoginDialog = apply {
dialog.negativeButton {
func()
}
}
fun show(func: LoginDialog.() -> Unit): LoginDialog = apply {
func()

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4,7h16v2H4V7zM4,13h16v-2H4V13zM4,17h7v-2H4V17zM4,21h7v-2H4V21zM15.41,18.17L14,16.75l-1.41,1.41L15.41,21L20,16.42L18.58,15L15.41,18.17zM4,3v2h16V3H4z"
android:fillColor="#000000"/>
</vector>

View File

@ -10,6 +10,7 @@
android:paddingBottom="3dp">
<LinearLayout
android:id="@+id/linLayout_subject"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
@ -19,23 +20,51 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Mathematik 1"
android:text="@string/sample_subject"
android:textSize="15sp" />
<TextView
android:id="@+id/txtView_grade"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:text="2,0"
android:text="@string/sample_grade"
android:textAlignment="textEnd"
android:textSize="15sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/linLayout_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/txtView_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/txtView_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_grades"
android:id="@+id/divider_grade"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="2dp"
android:layout_marginTop="3dp"
android:background="?dividerColor" />
</LinearLayout>

View File

@ -39,7 +39,7 @@
android:id="@+id/txtView_Grades"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/grades_heading"
android:text="@string/login_heading"
android:textAlignment="center"
android:textSize="26sp"
android:textStyle="bold" />
@ -49,7 +49,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="7dp"
android:text="@string/grades_desc_on"
android:text="@string/login_desc_on"
android:textAlignment="center"
android:textSize="18sp" />

View File

@ -65,7 +65,7 @@
android:id="@+id/txtView_UserDesc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/user" />
android:text="@string/user_desc" />
</LinearLayout>
<View

View File

@ -22,7 +22,7 @@
android:title="@string/moodle"/>
<item
android:id="@+id/nav_grades"
android:icon="@drawable/icon_checkmark_black"
android:icon="@drawable/ic_grading_black_24dp"
android:title="@string/grades" />
<item
android:id="@+id/nav_settings"

View File

@ -19,8 +19,8 @@
<string name="get_started">los geht\'s</string>
<string name="course_heading">Studiengang</string>
<string name="course_desc_on">Wähle deinen aktuellen Studiengang.\nZusätzliche Vorlesungen können später hinzugefügt werden.</string>
<string name="grades_heading">Noten</string>
<string name="grades_desc_on">Project Laogai kann auf die Notenverwaltung zugreifen. Deine Login-Daten werden verschlüsselt auf deinem Gerät gespeichert. Diese Funktion wird "as is" und ohne Garantie bereitgestellt.</string>
<string name="login_heading">Login</string>
<string name="login_desc_on">Project Laogai kann auf die Notenverwaltung zugreifen. Deine Login-Daten werden verschlüsselt auf deinem Gerät gespeichert. Diese Funktion wird "as is" und ohne Garantie bereitgestellt.</string>
<string name="email">E-Mail</string>
<string name="password">Passwort</string>
<string name="login">login</string>
@ -41,10 +41,12 @@
<!-- fragment_grades -->
<string name="loading_from_hs">Lade Daten von den Hochschul Servern.\nDas kann eine Weile dauern.</string>
<string name="credentials_missing">Diese Funktion benötigt deine Login-Daten. Bitte logge dich über die Einstellungen ein.</string>
<string name="without_guarantee">Alle Angaben ohne Gewähr.</string>
<!-- fragment_settings -->
<string name="info">Info</string>
<string name="user">Benutzer</string>
<string name="user_desc">Zum bearbeiten tippen</string>
<string name="course_desc">Tippe um den Kurs zu ändern</string>
<string name="manage_lessons">Bearbeite Zusätzliche Vorlesungen</string>
<string name="manage_lessons_desc">Tippe um zusätzliche Vorlesungen zu bearbeiten</string>

View File

@ -21,8 +21,8 @@
<string name="get_started">get started</string>
<string name="course_heading">Course</string>
<string name="course_desc_on">Select your current course.\nAdditional lessons can be added later.</string>
<string name="grades_heading">Grades</string>
<string name="grades_desc_on">Project Laogai can connect to the HIS Online-Portal. Your Login-Data will be stored encrypted on your device. This feature is provided "as is", without any warranty.</string>
<string name="login_heading">Login</string>
<string name="login_desc_on">Project Laogai can connect to the HIS Online-Portal. Your Login-Data will be stored encrypted on your device. This feature is provided "as is", without any guarantee.</string>
<string name="email">E-Mail</string>
<string name="password">Password</string>
<string name="login">login</string>
@ -43,10 +43,12 @@
<!-- fragment_grades -->
<string name="loading_from_hs">Loading data from the university servers.\nThis may take a while.</string>
<string name="credentials_missing">This feature needs your Login-Data to work. Please login via the settings.</string>
<string name="without_guarantee">All information is supplied without guarantee.</string>
<!-- fragment_settings -->
<string name="info">Info</string>
<string name="user">User</string>
<string name="user_desc">Tap to edit</string>
<string name="course_desc">Tap to change course</string>
<string name="manage_lessons">Manage Additional Lessons</string>
<string name="manage_lessons_desc">Tap to manage additional lessons</string>
@ -79,8 +81,12 @@
<!-- sample strings -->
<string name="sample_user" translatable="false">spinefield@stud.hs-offenburg.de</string>
<string name="sample_course" translatable="false">SampleCourse 3</string>
<string name="sample_date" translatable="false">Montag, 30.02</string>
<string name="sample_course" translatable="false">Everything</string>
<string name="sample_subject" translatable="false">Earthbending</string>
<string name="sample_sub_subject" translatable="false">Applied Earthbending</string>
<string name="sample_grade" translatable="false">1,0</string>
<string name="sample_grade_state" translatable="false">passed</string>
<string name="sample_date" translatable="false">Monday, 30.02</string>
<string name="a_time" translatable="false">0.00 23.59</string>
<!-- errors -->