From 638c321798700b3ae23260deded53380e74034e3 Mon Sep 17 00:00:00 2001 From: Jannik Seiler Date: Sat, 15 Aug 2020 18:13:29 +0200 Subject: [PATCH] grades ui polishing & sorting fix --- app/build.gradle | 2 +- .../projectlaogai/controller/QISPOSParser.kt | 72 +++++++---------- .../preferences/EncryptedPreferences.kt | 3 +- .../projectlaogai/fragments/GradesFragment.kt | 80 +++++++++++++++++-- .../fragments/SettingsFragment.kt | 3 +- .../uicomponents/GradeLinearLayout.kt | 10 ++- .../uicomponents/dialogs/LoginDialog.kt | 10 ++- .../res/drawable/ic_grading_black_24dp.xml | 9 +++ .../activities/layout/linearlayout_grade.xml | 37 ++++++++- .../fragments/layout/fragment_on_login.xml | 28 +++---- .../fragments/layout/fragment_settings.xml | 2 +- .../main/res/menu/activity_main_drawer.xml | 2 +- app/src/main/res/values-de-rDE/strings.xml | 8 +- app/src/main/res/values/strings.xml | 16 ++-- 14 files changed, 194 insertions(+), 88 deletions(-) create mode 100644 app/src/main/res/drawable/ic_grading_black_24dp.xml diff --git a/app/build.gradle b/app/build.gradle index 262ce53..95c27c0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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") diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/QISPOSParser.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/controller/QISPOSParser.kt index 09efd86..9552a7e 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/QISPOSParser.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/controller/QISPOSParser.kt @@ -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> { + /** + * parse the html from readGrades() + * @return a SortedMap, each entry is a semester, each semester has a ArrayList with subjects + */ + fun parseGrades(): SortedMap> { val gradesMap = HashMap>() - 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}") - } - // TODO sort + // return the sorted map + return gradesMap.toSortedMap(compareBy{ + val oText = it.substringAfter(" ") - println("finished parsing!") - - return gradesMap + 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(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() diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/preferences/EncryptedPreferences.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/controller/preferences/EncryptedPreferences.kt index 0925dbc..3ac8825 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/preferences/EncryptedPreferences.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/controller/preferences/EncryptedPreferences.kt @@ -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() } } diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/GradesFragment.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/GradesFragment.kt index 5de4433..30d52b5 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/GradesFragment.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/GradesFragment.kt @@ -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 - GlobalScope.launch(Dispatchers.Default) { + 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() 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 + } } } \ No newline at end of file diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/SettingsFragment.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/SettingsFragment.kt index ecd7c23..63bb370 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/SettingsFragment.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/SettingsFragment.kt @@ -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 { diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/uicomponents/GradeLinearLayout.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/uicomponents/GradeLinearLayout.kt index ac2f16a..2059923 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/uicomponents/GradeLinearLayout.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/uicomponents/GradeLinearLayout.kt @@ -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 } } \ No newline at end of file diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/uicomponents/dialogs/LoginDialog.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/uicomponents/dialogs/LoginDialog.kt index caef238..7300327 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/uicomponents/dialogs/LoginDialog.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/uicomponents/dialogs/LoginDialog.kt @@ -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() diff --git a/app/src/main/res/drawable/ic_grading_black_24dp.xml b/app/src/main/res/drawable/ic_grading_black_24dp.xml new file mode 100644 index 0000000..66f0b63 --- /dev/null +++ b/app/src/main/res/drawable/ic_grading_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layouts/activities/layout/linearlayout_grade.xml b/app/src/main/res/layouts/activities/layout/linearlayout_grade.xml index 9c8eee6..ccf4c5d 100644 --- a/app/src/main/res/layouts/activities/layout/linearlayout_grade.xml +++ b/app/src/main/res/layouts/activities/layout/linearlayout_grade.xml @@ -10,6 +10,7 @@ android:paddingBottom="3dp"> @@ -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" /> + + + + + + + + diff --git a/app/src/main/res/layouts/fragments/layout/fragment_on_login.xml b/app/src/main/res/layouts/fragments/layout/fragment_on_login.xml index 4c2477b..9d8032a 100644 --- a/app/src/main/res/layouts/fragments/layout/fragment_on_login.xml +++ b/app/src/main/res/layouts/fragments/layout/fragment_on_login.xml @@ -36,22 +36,22 @@ app:layout_constraintStart_toStartOf="parent"> + android:id="@+id/txtView_Grades" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/login_heading" + android:textAlignment="center" + android:textSize="26sp" + android:textStyle="bold" /> + android:id="@+id/txtView_GradesDesc" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="7dp" + android:text="@string/login_desc_on" + android:textAlignment="center" + android:textSize="18sp" /> + android:text="@string/user_desc" /> los geht\'s Studiengang Wähle deinen aktuellen Studiengang.\nZusätzliche Vorlesungen können später hinzugefügt werden. - Noten - 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. + Login + 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. E-Mail Passwort login @@ -41,10 +41,12 @@ Lade Daten von den Hochschul Servern.\nDas kann eine Weile dauern. + Diese Funktion benötigt deine Login-Daten. Bitte logge dich über die Einstellungen ein. + Alle Angaben ohne Gewähr. Info - Benutzer + Zum bearbeiten tippen Tippe um den Kurs zu ändern Bearbeite Zusätzliche Vorlesungen Tippe um zusätzliche Vorlesungen zu bearbeiten diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fa5453a..4d35cea 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -21,8 +21,8 @@ get started Course Select your current course.\nAdditional lessons can be added later. - Grades - 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. + Login + 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. E-Mail Password login @@ -43,10 +43,12 @@ Loading data from the university servers.\nThis may take a while. + This feature needs your Login-Data to work. Please login via the settings. + All information is supplied without guarantee. Info - User + Tap to edit Tap to change course Manage Additional Lessons Tap to manage additional lessons @@ -79,8 +81,12 @@ spinefield@stud.hs-offenburg.de - SampleCourse 3 - Montag, 30.02 + Everything + Earthbending + Applied Earthbending + 1,0 + passed + Monday, 30.02 0.00 – 23.59