67 Commits

Author SHA1 Message Date
  Jannik 28520bee74
changelog: minor text fixes 8 months ago
  Jannik 0c6a486dd9 Merge pull request 'version 0.6.0' (#46) from develop into master 8 months ago
  Jannik ec15c79b63
update fastlane screenshot 8 months ago
  Jannik 4592d1984f
release version 0.6.0 8 months ago
  Jannik aae74cf9db „README.md“ ändern 8 months ago
  Jannik e19b0db39d
add missing licenses to license dialog 8 months ago
  Jannik 23fd52b0c5
fab: use plus as icon 8 months ago
  Jannik 62a62049c3
code clean up 8 months ago
  Jannik 849698779b
add icon as svg 8 months ago
  Jannik a42a92a3c5
update constraintlayout 8 months ago
  Jannik be30e2f968
enable optimisations and minifying for release builds 8 months ago
  Jannik c629b2aec2
fix login button 8 months ago
  Jannik 99ba87c3f6
init fragment in onViewCreated() 8 months ago
  Jannik 953b4825a9
check qispos status and show message if unavailable 8 months ago
  Jannik 2807843d25
fix grades divider shown when sub-subject was added 8 months ago
  Jannik 638c321798
grades ui polishing & sorting fix 8 months ago
  Jannik 42d938b0bc
fix background in fragment_grades 8 months ago
  Jannik 9d7504bbaf
add grades ui 8 months ago
  Jannik 8ecfe8f120
QISPOSParser [Part 2] 8 months ago
  Jannik ff0c4ad1a7
QISPOSParser [Part 1] 8 months ago
  Jannik ea0caea91e
save email and password to encrypted preference 8 months ago
  Jannik 1f660bdd37
add login dialog 8 months ago
  Jannik be43d87b1a
fix more lint warnings 8 months ago
  Jannik dd5c7b3fb8
fix some lint warnings 8 months ago
  Jannik fb3dab6dc3
save additional subjects and load them on start 8 months ago
  Jannik bcfdb83d14
rework CacheController and TCoRAPIController 9 months ago
  Jannik 8c55366ec0
update libs 9 months ago
  Jannik 6f68fbb73c „README.md“ ändern 10 months ago
  Jannik 297dd77e79
use gradle wrapper for drone 10 months ago
  Jannik 6a70f26807
update .drone.yml 10 months ago
  Jannik f6b00a8d81
additional Subject are now added to the timetable 10 months ago
  Jannik 69ce9fef5a
code clean up 10 months ago
  Jannik 2d753851c0
more "Additional Lessons" work 10 months ago
  Jannik 6c0624c793
make the app more tolerant about wrong API Data 10 months ago
  Jannik 7779296345
change default accent color, code clean up 11 months ago
  Jannik 3e2ef0521e
change primary color to reflect mosad.xyz design guidelines 11 months ago
  Jannik bdfeb46faf
add "Manage Lessons" Dialog to Settings, minor style fixes 11 months ago
  Jannik 18ca435764
update TCoRAPIController to API version 1.2.0 11 months ago
  Jannik 948f330ebe
TimeTableFragment clean up 11 months ago
  Jannik 72e9efb9e7
fix warings in TCoRAPIController 11 months ago
  Jannik bea1b47396
complete & move AddLessonDialog to separate class 11 months ago
  Jannik 34a68ff75d
add courses to addLesson Dialog, read lessonSubjects from tcor 11 months ago
  Jannik 1ba3f1fa87
update gradle tool to 4.0.0 11 months ago
  Jannik 48544aef2f
add a complete version of the addLesson Dialog 11 months ago
  Jannik bc09e33147
update gradle, constraintlayout & coroutines 11 months ago
  Jannik 6ec9b9f5e2
update commons-lang and coroutines 12 months ago
  Jannik a19173b499
update kotlin 1 year ago
  Jannik 85dc3181fd
fix compatibility with tcor version 1.2.2 and higher 1 year ago
  Jannik e46c234b6c
fix OnboardingActivity is not closed on start of MainActivity 1 year ago
  Jannik faa07966da
fix crash on first start, if tcor is not reachable 1 year ago
  Jannik ccc0f0f2bc
Onboarding [Part 2] 1 year ago
  Jannik e15bf95b85
update to gradle 6.2.1 and gradle android plugin 3.6.0 1 year ago
  Jannik 01677c04ef
Onboarding [Part 1] 1 year ago
  Jannik 6e9c63d3d4 Merge branch 'develop' of Seil0/ProjectLaogai into master [Hotfix] 1 year ago
  Jannik c343735b57
fix crash on first startup 1 year ago
  Jannik 9c5274dc06 Merge branch 'develop' of Seil0/ProjectLaogai into master 1 year ago
  Jannik b186a2e96e
release version 0.5.1 1 year ago
  Jannik 2cb4b72369
anko code removed, coroutines are now used for asynchronous functions 1 year ago
  Jannik 2d497d1a96
update gradle wrapper to version 6.0.1 1 year ago
  Jannik 9d2de3fcb3
don't use deprecated Gson methods 1 year ago
  Jannik bed3f5d978
added Timetable and Moodle shortcut, cleaned up activity init 1 year ago
  Jannik 8f5a4dd1b3
reworked preference saving 1 year ago
  Jannik 9907d083e9
added a Mensa shortcut 1 year ago
  Jannik d4860b2a32
updated some libs 1 year ago
  Jannik 5ec2b53bce
added "Get it on Google Play" badge 2 years ago
  Jannik 701a351e6e
update .drone.yml 2 years ago
  Jannik 5cad924b26
removed anko dependency from PreferenceController 2 years ago
95 changed files with 3492 additions and 1046 deletions
Split View
  1. +2
    -2
      .drone.yml
  2. +12
    -7
      README.md
  3. +20
    -18
      app/build.gradle
  4. +9
    -0
      app/proguard-rules.pro
  5. +3
    -3
      app/src/androidTest/java/org/mosad/seil0/projectlaogai/ExampleInstrumentedTest.kt
  6. +11
    -0
      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. +40
    -18
      app/src/main/java/org/mosad/seil0/projectlaogai/MainActivity.kt
  10. +145
    -0
      app/src/main/java/org/mosad/seil0/projectlaogai/OnboardingActivity.kt
  11. +0
    -164
      app/src/main/java/org/mosad/seil0/projectlaogai/controller/CacheController.kt
  12. +5
    -4
      app/src/main/java/org/mosad/seil0/projectlaogai/controller/NFCMensaCard.kt
  13. +0
    -134
      app/src/main/java/org/mosad/seil0/projectlaogai/controller/PreferencesController.kt
  14. +236
    -0
      app/src/main/java/org/mosad/seil0/projectlaogai/controller/QISPOSParser.kt
  15. +68
    -72
      app/src/main/java/org/mosad/seil0/projectlaogai/controller/TCoRAPIController.kt
  16. +375
    -0
      app/src/main/java/org/mosad/seil0/projectlaogai/controller/cache/CacheController.kt
  17. +131
    -0
      app/src/main/java/org/mosad/seil0/projectlaogai/controller/cache/TimetableController.kt
  18. +110
    -0
      app/src/main/java/org/mosad/seil0/projectlaogai/controller/preferences/EncryptedPreferences.kt
  19. +192
    -0
      app/src/main/java/org/mosad/seil0/projectlaogai/controller/preferences/Preferences.kt
  20. +200
    -0
      app/src/main/java/org/mosad/seil0/projectlaogai/fragments/GradesFragment.kt
  21. +24
    -24
      app/src/main/java/org/mosad/seil0/projectlaogai/fragments/HomeFragment.kt
  22. +48
    -32
      app/src/main/java/org/mosad/seil0/projectlaogai/fragments/MensaFragment.kt
  23. +5
    -4
      app/src/main/java/org/mosad/seil0/projectlaogai/fragments/MoodleFragment.kt
  24. +114
    -62
      app/src/main/java/org/mosad/seil0/projectlaogai/fragments/SettingsFragment.kt
  25. +64
    -74
      app/src/main/java/org/mosad/seil0/projectlaogai/fragments/TimeTableFragment.kt
  26. +59
    -0
      app/src/main/java/org/mosad/seil0/projectlaogai/onboarding/ViewPagerAdapter.kt
  27. +7
    -7
      app/src/main/java/org/mosad/seil0/projectlaogai/uicomponents/DayCardView.kt
  28. +58
    -0
      app/src/main/java/org/mosad/seil0/projectlaogai/uicomponents/GradeLinearLayout.kt
  29. +3
    -4
      app/src/main/java/org/mosad/seil0/projectlaogai/uicomponents/LessonLinearLayout.kt
  30. +3
    -4
      app/src/main/java/org/mosad/seil0/projectlaogai/uicomponents/MealLinearLayout.kt
  31. +63
    -0
      app/src/main/java/org/mosad/seil0/projectlaogai/uicomponents/TextViewInfo.kt
  32. +164
    -0
      app/src/main/java/org/mosad/seil0/projectlaogai/uicomponents/dialogs/AddSubjectDialog.kt
  33. +87
    -0
      app/src/main/java/org/mosad/seil0/projectlaogai/uicomponents/dialogs/LoginDialog.kt
  34. +22
    -55
      app/src/main/java/org/mosad/seil0/projectlaogai/util/DataTypes.kt
  35. +64
    -0
      app/src/main/java/org/mosad/seil0/projectlaogai/util/NotRetardedCalendar.kt
  36. +2
    -2
      app/src/main/res/drawable/background_splash.xml
  37. +9
    -0
      app/src/main/res/drawable/ic_add_black_24dp.xml
  38. +9
    -0
      app/src/main/res/drawable/ic_error_outline_black_24dp.xml
  39. +9
    -0
      app/src/main/res/drawable/ic_grading_black_24dp.xml
  40. +0
    -9
      app/src/main/res/drawable/ic_menu_send.xml
  41. +0
    -9
      app/src/main/res/drawable/ic_menu_share.xml
  42. +7
    -6
      app/src/main/res/drawable/ic_settings_black_24dp.xml
  43. BIN
      app/src/main/res/drawable/ic_splash_logo.png
  44. +0
    -9
      app/src/main/res/drawable/side_nav_bar.xml
  45. +53
    -0
      app/src/main/res/layouts/activities/layout/activity_onboarding.xml
  46. +30
    -0
      app/src/main/res/layouts/activities/layout/dialog_login.xml
  47. +70
    -0
      app/src/main/res/layouts/activities/layout/linearlayout_grade.xml
  48. +1
    -1
      app/src/main/res/layouts/activities/layout/linearlayout_lesson.xml
  49. +16
    -14
      app/src/main/res/layouts/activities/layout/nav_header_main.xml
  50. +43
    -0
      app/src/main/res/layouts/activities/xml/shortcuts.xml
  51. +42
    -0
      app/src/main/res/layouts/dialogs/layout/dialog_add_lesson.xml
  52. +1
    -1
      app/src/main/res/layouts/dialogs/layout/dialog_mensa_credit.xml
  53. +38
    -0
      app/src/main/res/layouts/fragments/layout/fragment_grades.xml
  54. +17
    -16
      app/src/main/res/layouts/fragments/layout/fragment_mensa.xml
  55. +72
    -0
      app/src/main/res/layouts/fragments/layout/fragment_on_course.xml
  56. +93
    -0
      app/src/main/res/layouts/fragments/layout/fragment_on_login.xml
  57. +70
    -0
      app/src/main/res/layouts/fragments/layout/fragment_on_welcome.xml
  58. +274
    -209
      app/src/main/res/layouts/fragments/layout/fragment_settings.xml
  59. +16
    -7
      app/src/main/res/layouts/fragments/layout/fragment_timetable.xml
  60. +4
    -0
      app/src/main/res/menu/activity_main_drawer.xml
  61. +2
    -2
      app/src/main/res/mipmap-anydpi-v26/ic_laogai_icon.xml
  62. BIN
      app/src/main/res/mipmap-hdpi/ic_laogai_icon.png
  63. BIN
      app/src/main/res/mipmap-hdpi/ic_laogai_icon_foreground.png
  64. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
  65. BIN
      app/src/main/res/mipmap-mdpi/ic_laogai_icon.png
  66. BIN
      app/src/main/res/mipmap-mdpi/ic_laogai_icon_foreground.png
  67. BIN
      app/src/main/res/mipmap-mdpi/ic_laogai_icon_splash.png
  68. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
  69. BIN
      app/src/main/res/mipmap-xhdpi/ic_laogai_icon.png
  70. BIN
      app/src/main/res/mipmap-xhdpi/ic_laogai_icon_foreground.png
  71. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
  72. BIN
      app/src/main/res/mipmap-xxhdpi/ic_laogai_icon.png
  73. BIN
      app/src/main/res/mipmap-xxhdpi/ic_laogai_icon_foreground.png
  74. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
  75. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_laogai_icon.png
  76. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_laogai_icon_foreground.png
  77. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
  78. BIN
      app/src/main/res/raw/notenverwaltung_hs_offenburg_de.crt
  79. +20
    -8
      app/src/main/res/raw/notices.xml
  80. +57
    -7
      app/src/main/res/values-de-rDE/strings.xml
  81. +6
    -12
      app/src/main/res/values/colors.xml
  82. +84
    -10
      app/src/main/res/values/strings.xml
  83. +6
    -4
      app/src/main/res/values/styles.xml
  84. +2
    -2
      build.gradle
  85. +46
    -0
      etc/drawable_resources/laogai_icon.svg
  86. +7
    -5
      fastlane/metadata/android/de-DE/full_description.txt
  87. +5
    -0
      fastlane/metadata/android/en-US/changelogs/15.txt
  88. +9
    -0
      fastlane/metadata/android/en-US/changelogs/6000.txt
  89. +7
    -5
      fastlane/metadata/android/en-US/full_description.txt
  90. BIN
      fastlane/metadata/android/en-US/images/phoneScreenshots/ProjectLaogai_Settings.png
  91. BIN
      fastlane/metadata/android/en-US/images/phoneScreenshots/ProjectLaogai_Settings_dark.png
  92. BIN
      gradle/wrapper/gradle-wrapper.jar
  93. +1
    -1
      gradle/wrapper/gradle-wrapper.properties
  94. +16
    -19
      gradlew
  95. +4
    -0
      gradlew.bat

+ 2
- 2
.drone.yml View File

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

+ 12
- 7
README.md View File

@ -1,16 +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)
@ -19,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 [@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

+ 20
- 18
app/build.gradle View File

@ -1,7 +1,5 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
@ -12,8 +10,8 @@ android {
applicationId "org.mosad.seil0.projectlaogai"
minSdkVersion 23
targetSdkVersion 29
versionCode 14
versionName "0.5.0"
versionCode 6000 // 0006000
versionName "0.6.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "build_time", buildTime()
setProperty("archivesBaseName", "projectlaogai-$versionName")
@ -21,8 +19,8 @@ android {
buildTypes {
release {
minifyEnabled false
shrinkResources false
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
@ -49,23 +47,27 @@ android {
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'org.jetbrains.anko:anko-commons:0.10.8'
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-beta2'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'com.google.code.gson:gson:2.8.5'
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
- 0
app/proguard-rules.pro View File

@ -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

+ 3
- 3
app/src/androidTest/java/org/mosad/seil0/projectlaogai/ExampleInstrumentedTest.kt View File

@ -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)
}
}

+ 11
- 0
app/src/main/AndroidManifest.xml View File

@ -23,6 +23,17 @@
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data android:name="android.app.shortcuts"
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


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

Before After
Width: 512  |  Height: 512  |  Size: 13 KiB

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

Before After
Width: 512  |  Height: 512  |  Size: 39 KiB

+ 40
- 18
app/src/main/java/org/mosad/seil0/projectlaogai/MainActivity.kt View File

@ -1,7 +1,7 @@
/**
* ProjectLaogai
*
* Copyright 2019 <seil0@mosad.xyz>
* 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
@ -30,6 +30,8 @@ import android.nfc.NfcAdapter
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
@ -42,18 +44,22 @@ 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
// TODO save the current fragment to show it when the app is restarted
/**
* TODO save the current fragment to show it when the app is restarted
*/
class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {
private var activeFragment: Fragment = HomeFragment() // the currently active fragment, home at the start
private val className = "MainActivity"
private lateinit var adapter: NfcAdapter
private lateinit var pendingIntent: PendingIntent
@ -62,6 +68,8 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
private var useNFC = false
override fun onCreate(savedInstanceState: Bundle?) {
val fragmentTransaction: FragmentTransaction = supportFragmentManager.beginTransaction()
Aesthetic.attach(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
@ -72,11 +80,6 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
initAesthetic()
initForegroundDispatch()
//init home fragment
val fragmentTransaction: FragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.replace(R.id.fragment_container, activeFragment)
fragmentTransaction.commit()
val toggle = ActionBarDrawerToggle(
this, drawer_layout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close
)
@ -85,9 +88,17 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
nav_view.setNavigationItemSelectedListener(this)
// if we get an NFC read intent while the app is closed call readBalance
if (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action)
NFCMensaCard.readBalance(intent, this)
// 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()
"org.mosad.seil0.projectlaogai.fragments.TimeTableFragment" -> activeFragment = TimeTableFragment()
"org.mosad.seil0.projectlaogai.fragments.MoodleFragment" -> activeFragment = MoodleFragment()
}
// open the activeFragment, default is the HomeFragment
fragmentTransaction.replace(R.id.fragment_container, activeFragment)
fragmentTransaction.commit()
}
override fun onNewIntent(intent: Intent) {
@ -145,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()
}
@ -163,22 +175,25 @@ 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)
}
println("startup completed in $startupTime ms")
Log.i(className, "startup completed in $startupTime ms")
}
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 {
@ -189,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
- 0
app/src/main/java/org/mosad/seil0/projectlaogai/OnboardingActivity.kt View File

@ -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) {}
}
}

+ 0
- 164
app/src/main/java/org/mosad/seil0/projectlaogai/controller/CacheController.kt View File

@ -1,164 +0,0 @@
/**
* ProjectLaogai
*
* Copyright 2019 <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 com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonParser
import com.google.gson.reflect.TypeToken
import org.mosad.seil0.projectlaogai.controller.PreferencesController.Companion.cCourse
import org.mosad.seil0.projectlaogai.hsoparser.*
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 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) {
println("update mensa blocking")
TCoRAPIController.getMensa(context).get()
}
// 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) {
println("updating timetable after sunday!")
val jobA = TCoRAPIController.getTimetable(cCourse.courseName, 0, context)
val jobB = TCoRAPIController.getTimetable(cCourse.courseName, 1, context)
jobA.get()
jobB.get()
}
// 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())
TCoRAPIController.getCoursesList(context).get()
val fileReader = FileReader(file)
val bufferedReader = BufferedReader(fileReader)
val coursesObject = JsonParser().parse(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()) {
TCoRAPIController.getMensa(context).get()
}
val fileReader = FileReader(file)
val bufferedReader = BufferedReader(fileReader)
val mensaObject = JsonParser().parse(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())
TCoRAPIController.getTimetable(courseName, week, context).get()
val fileReader = FileReader(file)
val bufferedReader = BufferedReader(fileReader)
val timetableObject = JsonParser().parse(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)
*/
fun readStartCache(courseName: String) {
readCoursesList(context)
readMensa(context)
readTimetable(courseName, 0, context)
readTimetable(courseName, 1, context)
}
}

+ 5
- 4
app/src/main/java/org/mosad/seil0/projectlaogai/controller/NFCMensaCard.kt View File

@ -1,7 +1,7 @@
/**
* ProjectLaogai
*
* Copyright 2019 <seil0@mosad.xyz>
* 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
@ -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 {
@ -66,7 +67,7 @@ class NFCMensaCard {
lookAtMe(context, data, settings.value).show()
}
} catch (ex: Exception) {
Log.i(className,"could not connect to tag", ex)
Log.w(className,"could not connect to tag", ex)
}
}
@ -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))


+ 0
- 134
app/src/main/java/org/mosad/seil0/projectlaogai/controller/PreferencesController.kt View File

@ -1,134 +0,0 @@
/**
* ProjectLaogai
*
* Copyright 2019 <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.jetbrains.anko.defaultSharedPreferences
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.defaultSharedPreferences
// 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
with (sharedPref.edit()) {
putString(context.getString(R.string.save_key_course), cCourse.courseName)
putString(context.getString(R.string.save_key_courseTTLink), cCourse.courseLink)
apply()
}
// save the primary color
with (sharedPref.edit()) {
putInt(context.getString(R.string.save_key_colorPrimary),
cColorPrimary
)
apply()
}
// save the accent color
with (sharedPref.edit()) {
putInt(context.getString(R.string.save_key_colorAccent),
cColorAccent
)
apply()
}
// save showBuffet
with (sharedPref.edit()) {
putBoolean(context.getString(R.string.save_key_showBuffet),
cShowBuffet
)
apply()
}
}
// the load function
fun load(context: Context) {
val sharedPref = context.defaultSharedPreferences
// 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
- 0
app/src/main/java/org/mosad/seil0/projectlaogai/controller/QISPOSParser.kt View File

@ -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
}
}

+ 68
- 72
app/src/main/java/org/mosad/seil0/projectlaogai/controller/TCoRAPIController.kt View File

@ -1,7 +1,7 @@
/**
* ProjectLaogai
*
* Copyright 2019 <seil0@mosad.xyz>
* 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
@ -22,99 +22,95 @@
package org.mosad.seil0.projectlaogai.controller
import android.content.Context
import android.util.Log
import org.jetbrains.anko.doAsync
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 com.google.gson.Gson
import com.google.gson.JsonParser
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.*
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) = doAsync {
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
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) = doAsync {
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
val writer = BufferedWriter(FileWriter(file))
writer.write(mensaObject.toString())
writer.close()
// update cache time
mensaCacheTime = System.currentTimeMillis() / 1000
PreferencesController.save(context)
} catch (ex: Exception) {
Log.e(className, "failed to get /mensamenu", ex)
}
fun getMensaMenu(): MensaMenu {
val url = URL("$tcorBaseURL/mensamenu")
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 all lessons for a course at one week (async)
* @param courseName the course name
* @param week the week to look up
*/
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)
}
}
/**
* get the json object from tcor api and write it as file (cache)
* 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 getTimetable(courseName: String, week: Int, context: Context) = doAsync {
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 mensaObject = JSONObject(url.readText()) //JSONObject(inReader.readLine())
// write the json object to a file
val writer = BufferedWriter(FileWriter(file))
writer.write(mensaObject.toString())
writer.close()
// update cache time
timetableCacheTime = System.currentTimeMillis() / 1000
PreferencesController.save(context)
} catch (ex: Exception) {
Log.e(className, "failed to get /timetable", ex)
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
- 0
app/src/main/java/org/mosad/seil0/projectlaogai/controller/cache/CacheController.kt View File

@ -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)
if (currentTime - mensaCacheTime > 10800)
updateMensaMenu(context)
if (currentTime - timetableCacheTime > 10800) {
TimetableController.update(context)
}