Browse Source

Merge pull request 'version 0.6.0' (#46) from develop into master

Reviewed-on: #46
pull/47/head
Jannik 2 years ago
parent
commit
0c6a486dd9
  1. 4
      .drone.yml
  2. 18
      README.md
  3. 34
      app/build.gradle
  4. 9
      app/proguard-rules.pro
  5. 6
      app/src/androidTest/java/org/mosad/seil0/projectlaogai/ExampleInstrumentedTest.kt
  6. 8
      app/src/main/AndroidManifest.xml
  7. BIN
      app/src/main/ic_laogai_icon-playstore.png
  8. BIN
      app/src/main/ic_laogai_icon-web.png
  9. 29
      app/src/main/java/org/mosad/seil0/projectlaogai/MainActivity.kt
  10. 145
      app/src/main/java/org/mosad/seil0/projectlaogai/OnboardingActivity.kt
  11. 177
      app/src/main/java/org/mosad/seil0/projectlaogai/controller/CacheController.kt
  12. 5
      app/src/main/java/org/mosad/seil0/projectlaogai/controller/NFCMensaCard.kt
  13. 183
      app/src/main/java/org/mosad/seil0/projectlaogai/controller/PreferencesController.kt
  14. 236
      app/src/main/java/org/mosad/seil0/projectlaogai/controller/QISPOSParser.kt
  15. 136
      app/src/main/java/org/mosad/seil0/projectlaogai/controller/TCoRAPIController.kt
  16. 375
      app/src/main/java/org/mosad/seil0/projectlaogai/controller/cache/CacheController.kt
  17. 131
      app/src/main/java/org/mosad/seil0/projectlaogai/controller/cache/TimetableController.kt
  18. 110
      app/src/main/java/org/mosad/seil0/projectlaogai/controller/preferences/EncryptedPreferences.kt
  19. 192
      app/src/main/java/org/mosad/seil0/projectlaogai/controller/preferences/Preferences.kt
  20. 200
      app/src/main/java/org/mosad/seil0/projectlaogai/fragments/GradesFragment.kt
  21. 28
      app/src/main/java/org/mosad/seil0/projectlaogai/fragments/HomeFragment.kt
  22. 50
      app/src/main/java/org/mosad/seil0/projectlaogai/fragments/MensaFragment.kt
  23. 7
      app/src/main/java/org/mosad/seil0/projectlaogai/fragments/MoodleFragment.kt
  24. 155
      app/src/main/java/org/mosad/seil0/projectlaogai/fragments/SettingsFragment.kt
  25. 134
      app/src/main/java/org/mosad/seil0/projectlaogai/fragments/TimeTableFragment.kt
  26. 59
      app/src/main/java/org/mosad/seil0/projectlaogai/onboarding/ViewPagerAdapter.kt
  27. 12
      app/src/main/java/org/mosad/seil0/projectlaogai/uicomponents/DayCardView.kt
  28. 58
      app/src/main/java/org/mosad/seil0/projectlaogai/uicomponents/GradeLinearLayout.kt
  29. 5
      app/src/main/java/org/mosad/seil0/projectlaogai/uicomponents/LessonLinearLayout.kt
  30. 5
      app/src/main/java/org/mosad/seil0/projectlaogai/uicomponents/MealLinearLayout.kt
  31. 63
      app/src/main/java/org/mosad/seil0/projectlaogai/uicomponents/TextViewInfo.kt
  32. 164
      app/src/main/java/org/mosad/seil0/projectlaogai/uicomponents/dialogs/AddSubjectDialog.kt
  33. 87
      app/src/main/java/org/mosad/seil0/projectlaogai/uicomponents/dialogs/LoginDialog.kt
  34. 75
      app/src/main/java/org/mosad/seil0/projectlaogai/util/DataTypes.kt
  35. 64
      app/src/main/java/org/mosad/seil0/projectlaogai/util/NotRetardedCalendar.kt
  36. 4
      app/src/main/res/drawable/background_splash.xml
  37. 9
      app/src/main/res/drawable/ic_add_black_24dp.xml
  38. 9
      app/src/main/res/drawable/ic_error_outline_black_24dp.xml
  39. 9
      app/src/main/res/drawable/ic_grading_black_24dp.xml
  40. 9
      app/src/main/res/drawable/ic_menu_send.xml
  41. 9
      app/src/main/res/drawable/ic_menu_share.xml
  42. 13
      app/src/main/res/drawable/ic_settings_black_24dp.xml
  43. BIN
      app/src/main/res/drawable/ic_splash_logo.png
  44. 9
      app/src/main/res/drawable/side_nav_bar.xml
  45. 53
      app/src/main/res/layouts/activities/layout/activity_onboarding.xml
  46. 30
      app/src/main/res/layouts/activities/layout/dialog_login.xml
  47. 70
      app/src/main/res/layouts/activities/layout/linearlayout_grade.xml
  48. 2
      app/src/main/res/layouts/activities/layout/linearlayout_lesson.xml
  49. 30
      app/src/main/res/layouts/activities/layout/nav_header_main.xml
  50. 42
      app/src/main/res/layouts/dialogs/layout/dialog_add_lesson.xml
  51. 2
      app/src/main/res/layouts/dialogs/layout/dialog_mensa_credit.xml
  52. 38
      app/src/main/res/layouts/fragments/layout/fragment_grades.xml
  53. 33
      app/src/main/res/layouts/fragments/layout/fragment_mensa.xml
  54. 72
      app/src/main/res/layouts/fragments/layout/fragment_on_course.xml
  55. 93
      app/src/main/res/layouts/fragments/layout/fragment_on_login.xml
  56. 70
      app/src/main/res/layouts/fragments/layout/fragment_on_welcome.xml
  57. 483
      app/src/main/res/layouts/fragments/layout/fragment_settings.xml
  58. 23
      app/src/main/res/layouts/fragments/layout/fragment_timetable.xml
  59. 4
      app/src/main/res/menu/activity_main_drawer.xml
  60. 4
      app/src/main/res/mipmap-anydpi-v26/ic_laogai_icon.xml
  61. BIN
      app/src/main/res/mipmap-hdpi/ic_laogai_icon.png
  62. BIN
      app/src/main/res/mipmap-hdpi/ic_laogai_icon_foreground.png
  63. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
  64. BIN
      app/src/main/res/mipmap-mdpi/ic_laogai_icon.png
  65. BIN
      app/src/main/res/mipmap-mdpi/ic_laogai_icon_foreground.png
  66. BIN
      app/src/main/res/mipmap-mdpi/ic_laogai_icon_splash.png
  67. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
  68. BIN
      app/src/main/res/mipmap-xhdpi/ic_laogai_icon.png
  69. BIN
      app/src/main/res/mipmap-xhdpi/ic_laogai_icon_foreground.png
  70. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
  71. BIN
      app/src/main/res/mipmap-xxhdpi/ic_laogai_icon.png
  72. BIN
      app/src/main/res/mipmap-xxhdpi/ic_laogai_icon_foreground.png
  73. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
  74. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_laogai_icon.png
  75. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_laogai_icon_foreground.png
  76. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
  77. BIN
      app/src/main/res/raw/notenverwaltung_hs_offenburg_de.crt
  78. 24
      app/src/main/res/raw/notices.xml
  79. 44
      app/src/main/res/values-de-rDE/strings.xml
  80. 18
      app/src/main/res/values/colors.xml
  81. 77
      app/src/main/res/values/strings.xml
  82. 10
      app/src/main/res/values/styles.xml
  83. 4
      build.gradle
  84. 46
      etc/drawable_resources/laogai_icon.svg
  85. 12
      fastlane/metadata/android/de-DE/full_description.txt
  86. 9
      fastlane/metadata/android/en-US/changelogs/6000.txt
  87. 12
      fastlane/metadata/android/en-US/full_description.txt
  88. BIN
      fastlane/metadata/android/en-US/images/phoneScreenshots/ProjectLaogai_Settings.png
  89. BIN
      fastlane/metadata/android/en-US/images/phoneScreenshots/ProjectLaogai_Settings_dark.png
  90. BIN
      gradle/wrapper/gradle-wrapper.jar
  91. 2
      gradle/wrapper/gradle-wrapper.properties
  92. 31
      gradlew
  93. 4
      gradlew.bat

4
.drone.yml

@ -3,7 +3,7 @@ name: default
steps:
- name: assembleRelease
image: nextcloudci/android:android-51
image: nextcloudci/android10:android-56
commands:
- gradle assembleRelease
- ./gradlew assembleRelease

18
README.md

@ -1,17 +1,21 @@
[![Build Status](https://drone.mosad.xyz/api/badges/Seil0/ProjectLaogai/status.svg)](https://drone.mosad.xyz/Seil0/ProjectLaogai)
![fdroid version](https://img.shields.io/f-droid/v/org.mosad.seil0.projectlaogai.svg?style=flat-square)
[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg?style=flat-square)](https://www.gnu.org/licenses/gpl-3.0)
# ProjectLaogai "hso App"
ProjectLaogai is a app to access the timetable and the mensa menu of Hochschule Offenburg.
# Project Laogai
Project Laogai is an app to access the timetable, grades (qispos) and the canteen menu of Hochschule Offenburg. Laogai uses the TCoR-API fot timetables and the canteen menu, wich makes acessing them super fast. To get the grades from Qispos, Laogai will ask for your login data, wich are stored encrypted on your device.
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="75">](https://f-droid.org/packages/org.mosad.seil0.projectlaogai/)
[<img src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" height="75">](https://play.google.com/store/apps/details?id=org.mosad.seil0.projectlaogai)
## Features
* check out the mensa menu of this and next week
* access your timetable
* have your grades displayed directly in the app
* show the timetable of your course
* take a look at the canteen menu for the current and next week
* check the current balance of your mensa card
* open moodle
* probably some funny bugs
* open moodle directly in the app
Please report bugs and issues to support@mosad.xyz
## Screenshots
[<img src="https://www.mosad.xyz/images/Project_Laogai/ProjectLaogai_HomeScreen.png" width=180>](https://www.mosad.xyz/images/Project_Laogai/ProjectLaogai_HomeScreen.png)
@ -20,4 +24,4 @@ ProjectLaogai is a app to access the timetable and the mensa menu of Hochschule
[<img src="https://www.mosad.xyz/images/Project_Laogai/ProjectLaogai_Settings.png" width=180>](https://www.mosad.xyz/images/Project_Laogai/ProjectLaogai_Settings.png)
[<img src="https://www.mosad.xyz/images/Project_Laogai/ProjectLaogai_Mensa_dark.png" width=180>](https://www.mosad.xyz/images/Project_Laogai/ProjectLaogai_Mensa_dark.png)
ProjectLaogai © 2019-2020 [@Seil0](https://git.mosad.xyz/Seil0), a [mosad](http://www.mosad.xyz) Project
ProjectLaogai © 2019-2020 [@Seil0](https://git.mosad.xyz/Seil0), a [mosad.xyz](http://www.mosad.xyz) Project

34
app/build.gradle

@ -10,8 +10,8 @@ android {
applicationId "org.mosad.seil0.projectlaogai"
minSdkVersion 23
targetSdkVersion 29
versionCode 15
versionName "0.5.1"
versionCode 6000 // 0006000
versionName "0.6.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "build_time", buildTime()
setProperty("archivesBaseName", "projectlaogai-$versionName")
@ -19,8 +19,8 @@ android {
buildTypes {
release {
minifyEnabled false
shrinkResources false
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
@ -47,23 +47,27 @@ android {
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8'
implementation 'androidx.core:core:1.3.1'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.security:security-crypto:1.1.0-alpha02'
implementation 'com.google.android.material:material:1.2.0'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.afollestad:aesthetic:1.0.0-beta05'
implementation 'com.afollestad.material-dialogs:core:3.1.1'
implementation 'com.afollestad.material-dialogs:color:3.1.1'
implementation 'com.afollestad.material-dialogs:core:3.3.0'
implementation 'com.afollestad.material-dialogs:color:3.3.0'
implementation 'com.afollestad.material-dialogs:bottomsheets:3.3.0'
implementation 'de.psdev.licensesdialog:licensesdialog:2.1.0'
implementation 'org.apache.commons:commons-lang3:3.9'
implementation 'org.apache.commons:commons-lang3:3.11'
implementation 'org.jsoup:jsoup:1.13.1'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test:core:1.2.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
}
static def buildTime() {

9
app/proguard-rules.pro vendored

@ -15,7 +15,16 @@
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
-dontobfuscate
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-keep class org.mosad.seil0.projectlaogai.util.** { <fields>; }
#Gson
-keepattributes Signature
-dontwarn sun.misc.**
#misc
-dontwarn java.lang.instrument.ClassFileTransformer

6
app/src/androidTest/java/org/mosad/seil0/projectlaogai/ExampleInstrumentedTest.kt

@ -1,7 +1,7 @@
package org.mosad.seil0.projectlaogai
import androidx.test.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Test
import org.junit.runner.RunWith
@ -18,7 +18,7 @@ class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getTargetContext()
val appContext = InstrumentationRegistry.getInstrumentation().context
assertEquals("org.mosad.seil0.projectlaogai", appContext.packageName)
}
}

8
app/src/main/AndroidManifest.xml

@ -28,6 +28,14 @@
android:resource="@xml/shortcuts" />
</activity>
<activity
android:name=".OnboardingActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.Light"
android:screenOrientation="portrait"
android:launchMode="singleTop">
</activity>
<activity
android:name=".MainActivity"
android:label="@string/app_name"

BIN
app/src/main/ic_laogai_icon-playstore.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
app/src/main/ic_laogai_icon-web.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

29
app/src/main/java/org/mosad/seil0/projectlaogai/MainActivity.kt

@ -31,6 +31,7 @@ import android.nfc.NfcManager
import android.nfc.tech.NfcA
import android.os.Bundle
import android.util.Log
import android.util.TypedValue
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.ActionBarDrawerToggle
@ -43,11 +44,12 @@ import com.afollestad.aesthetic.NavigationViewMode
import com.google.android.material.navigation.NavigationView
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.app_bar_main.*
import org.mosad.seil0.projectlaogai.controller.CacheController
import org.mosad.seil0.projectlaogai.controller.NFCMensaCard
import org.mosad.seil0.projectlaogai.controller.PreferencesController
import org.mosad.seil0.projectlaogai.controller.PreferencesController.Companion.cColorAccent
import org.mosad.seil0.projectlaogai.controller.PreferencesController.Companion.cColorPrimary
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.controller.preferences.Preferences.cColorAccent
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.cColorPrimary
import org.mosad.seil0.projectlaogai.fragments.*
import kotlin.system.measureTimeMillis
@ -86,7 +88,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
nav_view.setNavigationItemSelectedListener(this)
// based on the inent we get, call readBalance or open a Fragment
// based on the intent we get, call readBalance or open a Fragment
when (intent.action) {
NfcAdapter.ACTION_TECH_DISCOVERED -> NFCMensaCard.readBalance(intent, this)
"org.mosad.seil0.projectlaogai.fragments.MensaFragment" -> activeFragment = MensaFragment()
@ -154,6 +156,7 @@ 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()
}
@ -172,8 +175,9 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
*/
private fun load() {
val startupTime = measureTimeMillis {
PreferencesController.load(this) // load the settings, must be finished before doing anything else
Preferences.load(this) // load the settings, must be finished before doing anything else
CacheController(this) // load the cache
EncryptedPreferences.load(this)
}
Log.i(className, "startup completed in $startupTime ms")
}
@ -181,13 +185,15 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
private fun initAesthetic() {
// If we haven't set any defaults, do that now
if (Aesthetic.isFirstTime) {
// this is executed on the first app start, use this to show tutorial etc.
// set the default theme at the first app start
Aesthetic.config {
activityTheme(R.style.AppTheme_Light)
apply()
}
SettingsFragment().selectCourse(this) // FIXME this is not working
// show the onboarding activity
startActivity(Intent(this, OnboardingActivity::class.java))
finish()
}
Aesthetic.config {
@ -198,6 +204,13 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
apply()
}
// set theme color values
val out = TypedValue()
this.theme.resolveAttribute(R.attr.themePrimary, out, true)
Preferences.themePrimary = out.data
this.theme.resolveAttribute(R.attr.themeSecondary, out, true)
Preferences.themeSecondary = out.data
}
private fun initForegroundDispatch() {

145
app/src/main/java/org/mosad/seil0/projectlaogai/OnboardingActivity.kt

@ -0,0 +1,145 @@
/**
* 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 android.content.Intent
import android.graphics.Color
import android.os.Bundle
import android.view.View
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.text.HtmlCompat
import androidx.viewpager.widget.ViewPager
import com.afollestad.materialdialogs.callbacks.onDismiss
import kotlinx.android.synthetic.main.activity_onboarding.*
import org.mosad.seil0.projectlaogai.controller.preferences.EncryptedPreferences
import org.mosad.seil0.projectlaogai.fragments.SettingsFragment
import org.mosad.seil0.projectlaogai.onboarding.ViewPagerAdapter
class OnboardingActivity : AppCompatActivity() {
companion object {
val layouts = intArrayOf(R.layout.fragment_on_welcome, R.layout.fragment_on_course, R.layout.fragment_on_login)
}
private lateinit var viewPager: ViewPager
private lateinit var viewPagerAdapter: ViewPagerAdapter
private lateinit var linLayoutDots: LinearLayout
private lateinit var dots: Array<TextView>
private lateinit var editTextEmail: EditText
private lateinit var editTextPassword: EditText
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_onboarding)
viewPager = findViewById(R.id.viewPager)
linLayoutDots = findViewById(R.id.linLayout_Dots)
addBottomDots(0)
viewPagerAdapter = ViewPagerAdapter(this)
viewPager.adapter = viewPagerAdapter
viewPager.addOnPageChangeListener(viewPagerPageChangeListener)
// we don't use the skip button, instead we use the start button to skip the last fragment
btn_Skip.visibility = View.GONE
}
fun btnNextClick(@Suppress("UNUSED_PARAMETER")v: View) {
if (viewPager.currentItem < layouts.size - 1) {
viewPager.currentItem++
} else {
launchHomeScreen()
}
}
fun btnSkipClick(@Suppress("UNUSED_PARAMETER")v: View) {
launchHomeScreen()
}
fun btnSelectCourseClick(@Suppress("UNUSED_PARAMETER")v: View) {
SettingsFragment().selectCourse(this).show {
onDismiss {
btnNextClick(v) // show the next fragment
}
}
}
fun btnLoginClick(@Suppress("UNUSED_PARAMETER")v: View) {
editTextEmail = findViewById(R.id.editText_email)
editTextPassword = findViewById(R.id.editText_password)
// get login credentials from gui
val email = editTextEmail.text.toString()
val password = editTextPassword.text.toString()
// save the credentials
EncryptedPreferences.saveCredentials(email, password, this)
launchHomeScreen()
}
private fun addBottomDots(currentPage: Int) {
dots = Array(layouts.size) { TextView(this) }
linLayoutDots.removeAllViews()
dots.forEach {
it.text = HtmlCompat.fromHtml("&#8226;", HtmlCompat.FROM_HTML_MODE_LEGACY)
it.textSize = 35f
it.setTextColor(Color.parseColor("#cccccc"))
linLayoutDots.addView(it)
}
if (dots.isNotEmpty())
dots[currentPage].setTextColor(Color.parseColor("#000000"))
}
private fun launchHomeScreen() {
startActivity(Intent(this, MainActivity::class.java))
finish()
}
private var viewPagerPageChangeListener: ViewPager.OnPageChangeListener = object : ViewPager.OnPageChangeListener {
override fun onPageSelected(position: Int) {
addBottomDots(position)
// changing the next button text to skip for the login fragment
if (position == layouts.size - 1) {
btn_Next.text = getString(R.string.skip)
btn_Next.visibility = View.VISIBLE
btn_Skip.visibility = View.GONE
} else {
btn_Next.visibility = View.GONE
btn_Skip.visibility = View.GONE
}
}
override fun onPageScrolled(arg0: Int, arg1: Float, arg2: Int) {}
override fun onPageScrollStateChanged(arg0: Int) {}
}
}

177
app/src/main/java/org/mosad/seil0/projectlaogai/controller/CacheController.kt

@ -1,177 +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 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.PreferencesController.Companion.cCourse
import org.mosad.seil0.projectlaogai.hsoparser.Course
import org.mosad.seil0.projectlaogai.hsoparser.MensaMenu
import org.mosad.seil0.projectlaogai.hsoparser.TimetableCourseWeek
import java.io.BufferedReader
import java.io.File
import java.io.FileReader
import java.util.*
import kotlin.collections.ArrayList
class CacheController(cont: Context) {
private val className = "CacheController"
private val context = cont
init {
val cal = Calendar.getInstance()
val currentDay = Calendar.getInstance().get(Calendar.DAY_OF_WEEK)
val currentTime = System.currentTimeMillis() / 1000
// check if we need to update the mensa data before displaying it
readMensa(context)
cal.time = Date(mensaMenu.meta.updateTime * 1000)
// if a) it's monday and the last cache update was on sunday or b) the cache is older than 24hr, update blocking
if ((currentDay == Calendar.MONDAY && cal.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY) || (currentTime - mensaMenu.meta.updateTime) > 86400) {
Log.i(className, "update mensa blocking")
GlobalScope.launch(Dispatchers.Default) { TCoRAPIController.getMensa(context).join() }
}
// check if we need to update the timetables before displaying them
readTimetable(cCourse.courseName, 0, context)
cal.time = Date(timetables[0].meta.updateTime * 1000)
// if a) it`s monday and the last cache update was not on a sunday or b) the cache is older than 24hr, update blocking
if ((currentDay == Calendar.MONDAY && cal.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY) || (currentTime - timetables[0].meta.updateTime) > 86400) {
Log.i(className, "updating timetable after sunday!")
GlobalScope.launch(Dispatchers.Default) {
val threads = listOf(
TCoRAPIController.getTimetable(cCourse.courseName, 0, context),
TCoRAPIController.getTimetable(cCourse.courseName, 1, context)
)
threads.joinAll()
}
TCoRAPIController.getTimetable(cCourse.courseName, 0, context)
TCoRAPIController.getTimetable(cCourse.courseName, 1, context)
}
// check if an update is necessary, not blocking
if (currentTime - PreferencesController.coursesCacheTime > 86400)
TCoRAPIController.getCoursesList(context)
if (currentTime - PreferencesController.mensaCacheTime > 10800)
TCoRAPIController.getMensa(context)
if (currentTime - PreferencesController.timetableCacheTime > 10800) {
TCoRAPIController.getTimetable(cCourse.courseName, 0, context)
TCoRAPIController.getTimetable(cCourse.courseName, 1, context)
}
readStartCache(cCourse.courseName)
}
companion object {
var coursesList = ArrayList<Course>()
var timetables = ArrayList<TimetableCourseWeek>()
var mensaMenu = MensaMenu()
/**
* read the courses list from the cached file
* add them to the coursesList object
*/
private fun readCoursesList(context: Context) {
val file = File(context.filesDir, "courses.json")
// make sure the file exists
if (!file.exists())
runBlocking { TCoRAPIController.getCoursesList(context).join() }
val fileReader = FileReader(file)
val bufferedReader = BufferedReader(fileReader)
val coursesObject = JsonParser.parseString(bufferedReader.readLine()).asJsonObject
coursesList = Gson().fromJson(
coursesObject.getAsJsonArray("courses"),
object : TypeToken<List<Course>>() {}.type
)
}
/**
* get the MensaMenu object from the cached json,
* if cache is empty create the cache file
*/
fun readMensa(context: Context) {
val file = File(context.filesDir, "mensa.json")
// make sure the file exists
if (!file.exists())
runBlocking { TCoRAPIController.getMensa(context).join() }
val fileReader = FileReader(file)
val bufferedReader = BufferedReader(fileReader)
val mensaObject = JsonParser.parseString(bufferedReader.readLine()).asJsonObject
mensaMenu = GsonBuilder().create().fromJson(mensaObject, MensaMenu().javaClass)
}
/**
* read the weeks timetable from the cached file
* @param courseName the course name (e.g AI1)
* @param week the week to read (0 for the current and so on)
*/
fun readTimetable(courseName: String, week: Int, context: Context) {
val file = File(context.filesDir, "timetable-$courseName-$week.json")
// make sure the file exists
if (!file.exists())
runBlocking { TCoRAPIController.getTimetable(courseName, week, context).join() }
val fileReader = FileReader(file)
val bufferedReader = BufferedReader(fileReader)
val timetableObject = JsonParser.parseString(bufferedReader.readLine()).asJsonObject
// make sure you add the single weeks in the exact order!
if (timetables.size > week) {
timetables[week] = (Gson().fromJson(timetableObject, TimetableCourseWeek().javaClass))
} else {
timetables.add(Gson().fromJson(timetableObject, TimetableCourseWeek().javaClass))
}
}
}
/**
* read coursesList, mensa (current and next week), timetable (current and next week)
* @param courseName the course name (e.g AI1)
*/
private fun readStartCache(courseName: String) {
readCoursesList(context)
readMensa(context)
readTimetable(courseName, 0, context)
readTimetable(courseName, 1, context)
}
}

5
app/src/main/java/org/mosad/seil0/projectlaogai/controller/NFCMensaCard.kt

@ -35,6 +35,7 @@ import com.codebutler.farebot.card.desfire.DesfireFileSettings
import com.codebutler.farebot.card.desfire.DesfireProtocol
import kotlinx.android.synthetic.main.dialog_mensa_credit.*
import org.mosad.seil0.projectlaogai.R
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences
import java.lang.Exception
class NFCMensaCard {
@ -84,13 +85,13 @@ class NFCMensaCard {
val dialog = MaterialDialog(context)
.customView(R.layout.dialog_mensa_credit)
val current = if (!PreferencesController.oGiants) {
val current = if (!Preferences.oGiants) {
String.format("%.2f €", (currentRaw.toFloat() / 1000))
} else {
String.format("%.4f shm", (currentRaw.toFloat() * 0.0000075))
}
val last = if (!PreferencesController.oGiants) {
val last = if (!Preferences.oGiants) {
String.format("%.2f €", (lastRaw.toFloat() / 1000))
} else {
String.format("%.4f shm", (lastRaw.toFloat() * 0.0000075))

183
app/src/main/java/org/mosad/seil0/projectlaogai/controller/PreferencesController.kt

@ -1,183 +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.graphics.Color
import org.mosad.seil0.projectlaogai.R
import org.mosad.seil0.projectlaogai.hsoparser.Course
/**
* The PreferencesController class
* contains all preferences and global variables that exist in this app
*/
class PreferencesController {
companion object {
var coursesCacheTime: Long = 0
var mensaCacheTime: Long = 0
var timetableCacheTime: Long = 0
var cColorPrimary: Int = Color.BLACK
var cColorAccent: Int = Color.parseColor("#3F51B5")
var cCourse = Course("https://www.hs-offenburg.de/index.php?id=6627&class=class&iddV=DA64F6FE-9DDB-429E-A677-05D0D40CB636&week=0", "AI3")
var cShowBuffet = true
var oGiants = false
// the save function
fun save(context: Context) {
val sharedPref = context.getSharedPreferences(
context.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
// save the update times (cache)
with (sharedPref.edit()) {
putLong(context.getString(R.string.save_key_coursesCacheTime),
coursesCacheTime
)
putLong(context.getString(R.string.save_key_mensaCacheTime),
mensaCacheTime
)
putLong(context.getString(R.string.save_key_timetableCacheTime),
timetableCacheTime
)
apply()
}
}
/**
* save the course locally
*/
fun saveCourse(context: Context, course: Course) {
val sharedPref = context.getSharedPreferences(
context.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
with (sharedPref.edit()) {
putString(context.getString(R.string.save_key_course), course.courseName)
putString(context.getString(R.string.save_key_courseTTLink), course.courseLink)
apply()
}
cCourse = course
}
/**
* save the primary color
*/
fun saveColorPrimary(context: Context, colorPrimary: Int) {
val sharedPref = context.getSharedPreferences(
context.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
with (sharedPref.edit()) {
putInt(context.getString(R.string.save_key_colorPrimary),
colorPrimary
)
apply()
}
cColorPrimary = colorPrimary
}
/**
* save the accent color
*/
fun saveColorAccent(context: Context, colorAccent: Int) {
val sharedPref = context.getSharedPreferences(
context.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
with (sharedPref.edit()) {
putInt(context.getString(R.string.save_key_colorAccent),
colorAccent
)
apply()
}
cColorAccent = colorAccent
}
/**
* save showBuffet
*/
fun saveShowBuffet(context: Context, showBuffet: Boolean) {
val sharedPref = context.getSharedPreferences(
context.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
with (sharedPref.edit()) {
putBoolean(context.getString(R.string.save_key_showBuffet),
showBuffet
)
apply()
}
cShowBuffet = showBuffet
}
// the load function
fun load(context: Context) {
val sharedPref = context.getSharedPreferences(
context.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
// load the update times (cache)
coursesCacheTime = sharedPref.getLong(context.getString(
R.string.save_key_coursesCacheTime
), 0)
mensaCacheTime = sharedPref.getLong(context.getString(
R.string.save_key_mensaCacheTime
), 0)
timetableCacheTime = sharedPref.getLong(context.getString(
R.string.save_key_timetableCacheTime
), 0)
// load saved course
cCourse = Course(
sharedPref.getString(context.getString(R.string.save_key_courseTTLink), "https://www.hs-offenburg.de/index.php?id=6627&class=class&iddV=DA64F6FE-9DDB-429E-A677-05D0D40CB636&week=0")!!,
sharedPref.getString(context.getString(R.string.save_key_course), "AI3")!!
)
// load saved colors
cColorPrimary = sharedPref.getInt(context.getString(
R.string.save_key_colorPrimary
), Color.BLACK)
cColorAccent = sharedPref.getInt(context.getString(
R.string.save_key_colorAccent
), Color.parseColor("#3F51B5"))
// load showBuffet
cShowBuffet = sharedPref.getBoolean(context.getString(
R.string.save_key_showBuffet
), true)
}
}
}

236
app/src/main/java/org/mosad/seil0/projectlaogai/controller/QISPOSParser.kt

@ -0,0 +1,236 @@
/**
* 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()
res.statusCode()
} catch (exHttp: HttpStatusException) {
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 the sorted map
return gradesMap.toSortedMap(compareBy<String>{
val oText = it.substringAfter(" ")
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.statusMessage()}: ${res.statusCode()}")
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()
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
}
}

136
app/src/main/java/org/mosad/seil0/projectlaogai/controller/TCoRAPIController.kt

@ -22,105 +22,95 @@
package org.mosad.seil0.projectlaogai.controller
import android.content.Context
import android.util.Log
import com.google.gson.Gson
import com.google.gson.JsonParser
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.*
import org.json.JSONObject
import org.mosad.seil0.projectlaogai.controller.PreferencesController.Companion.coursesCacheTime
import org.mosad.seil0.projectlaogai.controller.PreferencesController.Companion.mensaCacheTime
import org.mosad.seil0.projectlaogai.controller.PreferencesController.Companion.timetableCacheTime
import java.io.BufferedWriter
import java.io.File
import java.io.FileWriter
import org.mosad.seil0.projectlaogai.util.*
import java.net.URL
import kotlin.Exception
/**
* This Controller calls the tcor api,
* all functions return tcor api objects
*/
class TCoRAPIController {
companion object {
private const val className = "TCoRAPIController"
private const val tcorBaseURL = "https://tcor.mosad.xyz"
/**
* get the json object from tcor api and write it as file (cache)
* Get a array of all currently available courses at the tcor API.
* Read the json object from tcor api
*/
fun getCoursesList(context: Context) = GlobalScope.launch {
try {
val url = URL("$tcorBaseURL/courseList")
val file = File(context.filesDir, "courses.json")
// read data from the API
val coursesObject = JSONObject(url.readText()) //JSONObject(inReader.readLine())
// write the json object to a file
withContext(Dispatchers.IO) {
val writer = BufferedWriter(FileWriter(file))
writer.write(coursesObject.toString())
writer.close()
}
// update cache time
coursesCacheTime = System.currentTimeMillis() / 1000
PreferencesController.save(context)
} catch (ex: Exception) {
Log.e(className, "failed to get /courseList", ex)
}
fun getCourseListNEW(): CoursesList {
val url = URL("$tcorBaseURL/courseList")
return Gson().fromJson(url.readText(), CoursesList().javaClass)
}
/**
* get the json object from tcor api and write it as file (cache)
* Get current and next weeks mensa menus from the tcor API.
* Read the json object from tcor api
*/
fun getMensa(context: Context) = GlobalScope.launch {
try {
val url = URL("$tcorBaseURL/mensamenu")
val file = File(context.filesDir, "mensa.json")
// read data from the API
val mensaObject = JSONObject(url.readText()) //JSONObject(inReader.readLine())
// write the json object to a file
withContext(Dispatchers.IO) {
val writer = BufferedWriter(FileWriter(file))
writer.write(mensaObject.toString())
writer.close()
}
fun getMensaMenu(): MensaMenu {
val url = URL("$tcorBaseURL/mensamenu")
// update cache time
mensaCacheTime = System.currentTimeMillis() / 1000
PreferencesController.save(context)
} catch (ex: Exception) {
Log.e(className, "failed to get /mensamenu", ex)
}
return Gson().fromJson(url.readText(), MensaMenu().javaClass)
}
/**
* Get the timetable for "courseName" at week "week"
* Read the json object from tcor api
* @param courseName the course name (e.g AI1)
* @param week the week to update (0 for the current and so on)
*/
fun getTimetable(courseName: String, week: Int): TimetableWeek {
val url = URL("$tcorBaseURL/timetable?course=$courseName&week=$week")
val timetableCW = Gson().fromJson(url.readText(), TimetableCourseWeek().javaClass)
return TimetableWeek(
timetableCW.meta.weekIndex,
timetableCW.meta.weekNumberYear,
timetableCW.timetable.days
)
}
/**
* get the json object from tcor api and write it as file (cache)
* Get all lessons for a course at one week (async)
* @param courseName the course name
* @param week the week to look up
*/
fun getTimetable(courseName: String, week: Int, context: Context) = GlobalScope.launch {
try {
val url = URL("$tcorBaseURL/timetable?courseName=$courseName&week=$week")
val file = File(context.filesDir, "timetable-$courseName-$week.json")
// read data from the API
val timetableObject = JSONObject(url.readText()) //JSONObject(inReader.readLine())
// write the json object to a file
withContext(Dispatchers.IO) {
val writer = BufferedWriter(FileWriter(file))
writer.write(timetableObject.toString())
writer.close()
}
fun getSubjectListAsync(courseName: String, week: Int): Deferred<ArrayList<String>> {
val url = URL("$tcorBaseURL/subjectList?course=$courseName&week=$week")
return GlobalScope.async {
Gson().fromJson(url.readText(), ArrayList<String>()::class.java)
}
}
// update cache time
timetableCacheTime = System.currentTimeMillis() / 1000
PreferencesController.save(context)
} catch (ex: Exception) {
Log.e(className, "failed to get /timetable", ex)
/**
* Get all occurrences of a lesson for a course at one week
* @param courseName the course name
* @param subject the subject to search for
* @param week the week to look up
*/
fun getLessons(courseName: String, subject: String, week: Int): ArrayList<Lesson> {
val url = URL("$tcorBaseURL/lessons?course=$courseName&subject=$subject&week=$week")
var array: ArrayList<Lesson>
runBlocking {
withContext(Dispatchers.Default) {
array = Gson().fromJson(
JsonParser.parseString(url.readText()).asJsonArray,
object : TypeToken<ArrayList<Lesson>>() {}.type
)
}
}
return array
}
}
}

375
app/src/main/java/org/mosad/seil0/projectlaogai/controller/cache/CacheController.kt vendored

@ -0,0 +1,375 @@
/**
* 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.cache
import android.content.Context
import android.util.Log
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.preferences.Preferences
import org.mosad.seil0.projectlaogai.controller.TCoRAPIController
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.cCourse
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.coursesCacheTime
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.mensaCacheTime
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.timetableCacheTime
import org.mosad.seil0.projectlaogai.util.*
import java.io.*
import java.util.*
import kotlin.collections.ArrayList
/**
* The cacheController reads and updates the cache files.
* It contains the courseList and mensaMenu object, all timetable objects
* are located in TimetableController.
*/
class CacheController(cont: Context) {
private val className = "CacheController"
private val context = cont
init {
val cal = Calendar.getInstance()
val currentDay = Calendar.getInstance().get(Calendar.DAY_OF_WEEK)
val currentTime = System.currentTimeMillis() / 1000
// check if we need to update the mensa data before displaying it
cal.time = Date(mensaCacheTime * 1000)
// if a) it's monday and the last cache update was on sunday or b) the cache is older than 24hr, update blocking
if ((currentDay == Calendar.MONDAY && cal.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY) || (currentTime - mensaMenu.meta.updateTime) > 86400) {
Log.i(className, "update mensa blocking")
GlobalScope.launch(Dispatchers.Default) { updateMensaMenu(context).join() }
}
// check if we need to update the timetable before displaying it
cal.time = Date(timetableCacheTime * 1000)
// if a) it`s monday and the last cache update was not on a sunday or b) the cache is older than 24hr, update blocking
if ((currentDay == Calendar.MONDAY && cal.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY) || (currentTime - timetableCacheTime) > 86400) {
Log.i(className, "updating timetable after sunday!")
GlobalScope.launch(Dispatchers.Default) {
val threads = listOf(
updateTimetable(cCourse.courseName, 0, context),
updateTimetable(cCourse.courseName, 1, context)
)
threads.joinAll()
}
}
updateCourseList(context)
readStartCache(cCourse.courseName) // initially read values from cache
// check if an update is necessary, not blocking
if (currentTime - coursesCacheTime > 86400)
updateCourseList(context)