diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/MainActivity.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/MainActivity.kt index acdf00e..425e096 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/MainActivity.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/MainActivity.kt @@ -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() } diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/GradesController.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/controller/GradesController.kt deleted file mode 100644 index 96b2e79..0000000 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/GradesController.kt +++ /dev/null @@ -1,99 +0,0 @@ -/** - * ProjectLaogai - * - * Copyright 2019-2020 - * - * 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>, diffMap: Map>): ArrayList { - val diff = ArrayList() - - 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( - 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") - } - - } -} \ No newline at end of file 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 deleted file mode 100644 index 61eec89..0000000 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/QISPOSParser.kt +++ /dev/null @@ -1,240 +0,0 @@ -/** - * ProjectLaogai - * - * Copyright 2019-2020 - * - * 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> { - val gradesMap = HashMap>() - 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{ - 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 - } - - - - -} \ No newline at end of file diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/cache/CacheController.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/controller/cache/CacheController.kt index af45b2f..ab5151b 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/cache/CacheController.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/controller/cache/CacheController.kt @@ -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> { - 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> = encryptedFile.openFileInput().bufferedReader().use { - GsonBuilder().create() - .fromJson(it, object : TypeToken>>() {}.type) - } - - // same sorting as qispos parser - return map.toSortedMap(compareBy{ - val oText = it.substringAfter(" ") - - if (oText.contains("/")) { - oText.substringBefore("/").toInt() + 0.5 - } else { - oText.toDouble() - } - }.thenBy { it }) - } - - return sortedMapOf() - } - } \ No newline at end of file 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 deleted file mode 100644 index c6f7d18..0000000 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/GradesFragment.kt +++ /dev/null @@ -1,228 +0,0 @@ -/** - * ProjectLaogai - * - * Copyright 2019-2020 - * - * 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() - 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") - } - -} \ 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 cac921e..e491e8e 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 @@ -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) } 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 deleted file mode 100644 index b29cb7d..0000000 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/uicomponents/GradeLinearLayout.kt +++ /dev/null @@ -1,56 +0,0 @@ -/** - * ProjectLaogai - * - * Copyright 2019-2020 - * - * 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 - } -} \ No newline at end of file diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/worker/GradesUpdateWorker.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/worker/GradesUpdateWorker.kt deleted file mode 100644 index 2b10c32..0000000 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/worker/GradesUpdateWorker.kt +++ /dev/null @@ -1,112 +0,0 @@ -/** - * ProjectLaogai - * - * Copyright 2019-2020 - * - * 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() - } - - - -} \ No newline at end of file diff --git a/app/src/main/res/layouts/activities/layout/linearlayout_grade.xml b/app/src/main/res/layouts/activities/layout/linearlayout_grade.xml deleted file mode 100644 index 69461de..0000000 --- a/app/src/main/res/layouts/activities/layout/linearlayout_grade.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layouts/fragments/layout/fragment_grades.xml b/app/src/main/res/layouts/fragments/layout/fragment_grades.xml deleted file mode 100644 index 67ec69b..0000000 --- a/app/src/main/res/layouts/fragments/layout/fragment_grades.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layouts/fragments/layout/fragment_settings.xml b/app/src/main/res/layouts/fragments/layout/fragment_settings.xml index 50cfba8..6fd3aaa 100644 --- a/app/src/main/res/layouts/fragments/layout/fragment_settings.xml +++ b/app/src/main/res/layouts/fragments/layout/fragment_settings.xml @@ -18,7 +18,8 @@ + android:orientation="vertical" + android:paddingBottom="11dp"> - - - - - - - - - - - * - * 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> = FileReader(origFile).use { - GsonBuilder().create().fromJson(it, object : TypeToken>>() {}.type) - } - - val mapB: HashMap> = FileReader(diffFile).use { - GsonBuilder().create().fromJson(it, object : TypeToken>>() {}.type) - } - - val exp = ArrayList() - 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> = FileReader(origFile).use { - GsonBuilder().create().fromJson(it, object : TypeToken>>() {}.type) - } - - val mapB: HashMap> = FileReader(diffFile).use { - GsonBuilder().create().fromJson(it, object : TypeToken>>() {}.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> = FileReader(origFile).use { - GsonBuilder().create().fromJson(it, object : TypeToken>>() {}.type) - } - - val mapB: HashMap> = FileReader(diffFile).use { - GsonBuilder().create().fromJson(it, object : TypeToken>>() {}.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) - } -} \ No newline at end of file