Compare commits
106 Commits
Author | SHA1 | Date | |
---|---|---|---|
28520bee74 | |||
0c6a486dd9 | |||
ec15c79b63 | |||
4592d1984f | |||
aae74cf9db | |||
e19b0db39d | |||
23fd52b0c5 | |||
62a62049c3 | |||
849698779b | |||
a42a92a3c5 | |||
be30e2f968 | |||
c629b2aec2 | |||
99ba87c3f6 | |||
953b4825a9 | |||
2807843d25 | |||
638c321798 | |||
42d938b0bc | |||
9d7504bbaf | |||
8ecfe8f120 | |||
ff0c4ad1a7 | |||
ea0caea91e | |||
1f660bdd37 | |||
be43d87b1a | |||
dd5c7b3fb8 | |||
fb3dab6dc3 | |||
bcfdb83d14 | |||
8c55366ec0 | |||
6f68fbb73c | |||
297dd77e79 | |||
6a70f26807 | |||
f6b00a8d81 | |||
69ce9fef5a | |||
2d753851c0 | |||
6c0624c793 | |||
7779296345 | |||
3e2ef0521e | |||
bdfeb46faf | |||
18ca435764 | |||
948f330ebe | |||
72e9efb9e7 | |||
bea1b47396 | |||
34a68ff75d | |||
1ba3f1fa87 | |||
48544aef2f | |||
bc09e33147 | |||
6ec9b9f5e2 | |||
a19173b499 | |||
85dc3181fd | |||
e46c234b6c | |||
faa07966da | |||
ccc0f0f2bc | |||
e15bf95b85 | |||
01677c04ef | |||
6e9c63d3d4 | |||
c343735b57 | |||
9c5274dc06 | |||
b186a2e96e | |||
2cb4b72369 | |||
2d497d1a96 | |||
9d2de3fcb3 | |||
bed3f5d978 | |||
8f5a4dd1b3 | |||
9907d083e9 | |||
d4860b2a32 | |||
5ec2b53bce | |||
701a351e6e | |||
5cad924b26 | |||
605bf6248d | |||
d5adc4df51 | |||
c23454f081 | |||
b240beacc9 | |||
aa69d2242f | |||
6e6c9f71a0 | |||
889c673c5d | |||
522f31e077 | |||
b9ca18044c | |||
bd49e482e2 | |||
2f5b2a6579 | |||
4ce37b3dcf | |||
7550fcfd22 | |||
a23f7c9212 | |||
75168c6688 | |||
be916a74ab | |||
3e909ab68f | |||
5f2b3aa496 | |||
6cc1671a52 | |||
b4ed1ca927 | |||
e74c307566 | |||
733b675ffa | |||
3a0a31f781 | |||
f52151fbf1 | |||
e8cae7e807 | |||
c6ac19bfae | |||
a7abd48726 | |||
9a22d9b737 | |||
2cc2ecf952 | |||
ea70aedbd0 | |||
98b3adbf3b | |||
a055f59cc8 | |||
01db6bb451 | |||
4838c9406c | |||
23195f94e0 | |||
5e766ec126 | |||
9c1f95ca25 | |||
e99127a63a | |||
f7fa96b1ae |
9
.drone.yml
Normal file
@ -0,0 +1,9 @@
|
||||
kind: pipeline
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: assembleRelease
|
||||
image: nextcloudci/android10:android-56
|
||||
commands:
|
||||
- ./gradlew assembleRelease
|
||||
|
32
README.md
@ -1,21 +1,27 @@
|
||||
[![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)
|
||||
|
||||
[<img src="https://f-droid.org/badge/get-it-on.png" height="75">](https://f-droid.org/packages/org.mosad.seil0.projectlaogai/)
|
||||
## Features
|
||||
* check out the mensa menu of this and next week
|
||||
* access your timetable
|
||||
* open moodle
|
||||
* probably some funny bugs
|
||||
* 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 directly in the app
|
||||
|
||||
Please report bugs and issues to support@mosad.xyz
|
||||
|
||||
## Screenshots
|
||||
[<img src="https://raw.githubusercontent.com/Seil0/Seil0.github.io/master/images/Project_Laogai/ProjectLaogai_HomeScreen.png" width=180>](https://github.com/Seil0/Seil0.github.io/blob/master/images/Project_Laogai/ProjectLaogai_HomeScreen.png)
|
||||
[<img src="https://raw.githubusercontent.com/Seil0/Seil0.github.io/master/images/Project_Laogai/ProjectLaogai_Mensa.png" width=180>](https://github.com/Seil0/Seil0.github.io/blob/master/images/Project_Laogai/ProjectLaogai_Mensa.png)
|
||||
[<img src="https://raw.githubusercontent.com/Seil0/Seil0.github.io/master/images/Project_Laogai/ProjectLaogai_Timetable.png" width=180>](https://github.com/Seil0/Seil0.github.io/blob/master/images/Project_Laogai/ProjectLaogai_Timetable.png)
|
||||
[<img src="https://raw.githubusercontent.com/Seil0/Seil0.github.io/master/images/Project_Laogai/ProjectLaogai_Settings.png" width=180>](https://github.com/Seil0/Seil0.github.io/blob/master/images/Project_Laogai/ProjectLaogai_Settings.png)
|
||||
[<img src="https://raw.githubusercontent.com/Seil0/Seil0.github.io/master/images/Project_Laogai/ProjectLaogai_NavDrawer.png" width=180>](https://github.com/Seil0/Seil0.github.io/blob/master/images/Project_Laogai/ProjectLaogai_NavDrawer.png)
|
||||
[<img src="https://www.mosad.xyz/images/Project_Laogai/ProjectLaogai_HomeScreen.png" width=180>](https://www.mosad.xyz/images/Project_Laogai/ProjectLaogai_HomeScreen.png)
|
||||
[<img src="https://www.mosad.xyz/images/Project_Laogai/ProjectLaogai_Mensa.png" width=180>](https://www.mosad.xyz/images/Project_Laogai/ProjectLaogai_Mensa.png)
|
||||
[<img src="https://www.mosad.xyz/images/Project_Laogai/ProjectLaogai_Timetable.png" width=180>](https://www.mosad.xyz/images/Project_Laogai/ProjectLaogai_Timetable.png)
|
||||
[<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 mosad [www.mosad.xyz](http://www.mosad.xyz), Project by [@Seil0](https://git.mosad.xyz/Seil0)
|
||||
ProjectLaogai © 2019-2020 [@Seil0](https://git.mosad.xyz/Seil0), a [mosad.xyz](http://www.mosad.xyz) Project
|
||||
|
@ -1,19 +1,17 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
|
||||
android {
|
||||
signingConfigs {
|
||||
}
|
||||
compileSdkVersion 28
|
||||
compileSdkVersion 29
|
||||
defaultConfig {
|
||||
applicationId "org.mosad.seil0.projectlaogai"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 28
|
||||
versionCode 13
|
||||
versionName "0.4.1"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 29
|
||||
versionCode 6000 // 0006000
|
||||
versionName "0.6.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resValue "string", "build_time", buildTime()
|
||||
setProperty("archivesBaseName", "projectlaogai-$versionName")
|
||||
@ -21,34 +19,55 @@ android {
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
shrinkResources false
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
res.srcDirs =
|
||||
[
|
||||
'src/main/res/layouts/activities',
|
||||
'src/main/res/layouts/dialogs',
|
||||
'src/main/res/layouts/fragments',
|
||||
'src/main/res/layouts',
|
||||
'src/main/res'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.0.2'
|
||||
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-alpha5'
|
||||
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:2.8.1'
|
||||
implementation 'com.afollestad.material-dialogs:color:2.8.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.11'
|
||||
implementation 'org.jsoup:jsoup:1.13.1'
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test:runner:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
|
||||
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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.mosad.seil0.projectlaogai">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
@ -10,24 +11,46 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_laogai_icon"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
android:theme="@style/AppTheme.Light">
|
||||
|
||||
<activity
|
||||
android:name=".SplashActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/SplashTheme"
|
||||
android:screenOrientation="portrait">
|
||||
|
||||
<intent-filter>
|
||||
<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
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
android:screenOrientation="portrait">
|
||||
android:theme="@style/AppTheme.Light"
|
||||
android:screenOrientation="portrait"
|
||||
android:launchMode="singleTop">
|
||||
|
||||
<!-- nfc stuff -->
|
||||
<intent-filter>
|
||||
<action android:name="android.nfc.action.TECH_DISCOVERED"/>
|
||||
</intent-filter>
|
||||
|
||||
<meta-data android:name="android.nfc.action.TECH_DISCOVERED"
|
||||
android:resource="@xml/nfc_tech_filter" />
|
||||
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
|
BIN
app/src/main/ic_laogai_icon-playstore.png
Normal file
After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 39 KiB |
227
app/src/main/java/com/codebutler/farebot/Utils.kt
Normal file
@ -0,0 +1,227 @@
|
||||
/**
|
||||
* Utils.kt
|
||||
*
|
||||
* Copyright (C) 2011 Eric Butler
|
||||
* Copyright (C) 2019 <seil0@mosad.xyz>
|
||||
*
|
||||
* Authors:
|
||||
* Eric Butler <eric@codebutler.com>
|
||||
*
|
||||
* 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, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.codebutler.farebot
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.util.Log
|
||||
import android.view.WindowManager
|
||||
import com.codebutler.farebot.card.desfire.DesfireException
|
||||
import com.codebutler.farebot.card.desfire.DesfireFileSettings
|
||||
import com.codebutler.farebot.card.desfire.DesfireProtocol
|
||||
import org.w3c.dom.Node
|
||||
import java.io.StringWriter
|
||||
import javax.xml.transform.OutputKeys
|
||||
import javax.xml.transform.TransformerFactory
|
||||
import javax.xml.transform.dom.DOMSource
|
||||
import javax.xml.transform.stream.StreamResult
|
||||
import kotlin.experimental.and
|
||||
|
||||
class Utils {
|
||||
|
||||
companion object {
|
||||
private val TAG = Utils::class.java.name
|
||||
|
||||
@Suppress("unused")
|
||||
fun showError(activity: Activity, ex: Exception) {
|
||||
Log.e(activity.javaClass.name, ex.message, ex)
|
||||
AlertDialog.Builder(activity)
|
||||
.setMessage(getErrorMessage(ex))
|
||||
.show()
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun showErrorAndFinish(activity: Activity, ex: Exception) {
|
||||
try {
|
||||
Log.e(activity.javaClass.name, getErrorMessage(ex))
|
||||
ex.printStackTrace()
|
||||
|
||||
AlertDialog.Builder(activity)
|
||||
.setMessage(getErrorMessage(ex))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> activity.finish() }
|
||||
.show()
|
||||
} catch (unused: WindowManager.BadTokenException) {
|
||||
/* Ignore... happens if the activity was destroyed */
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun getHexString(b: ByteArray): String {
|
||||
var result = ""
|
||||
for (i in b.indices) {
|
||||
result += ((b[i] and 0xff.toByte()) + 0x100).toString(16).substring(1)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun getHexString(b: ByteArray, defaultResult: String): String {
|
||||
return try {
|
||||
getHexString(b)
|
||||
} catch (ex: Exception) {
|
||||
defaultResult
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun hexStringToByteArray(s: String): ByteArray {
|
||||
if (s.length % 2 != 0) {
|
||||
throw IllegalArgumentException("Bad input string: $s")
|
||||
}
|
||||
|
||||
val len = s.length
|
||||
val data = ByteArray(len / 2)
|
||||
var i = 0
|
||||
while (i < len) {
|
||||
data[i / 2] = ((Character.digit(s[i], 16) shl 4) + Character.digit(s[i + 1], 16)).toByte()
|
||||
i += 2
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun byteArrayToInt(b: ByteArray, offset: Int = 0, length: Int = b.size): Int {
|
||||
return byteArrayToLong(b, offset, length).toInt()
|
||||
}
|
||||
|
||||
fun byteArrayToLong(b: ByteArray, offset: Int, length: Int): Long {
|
||||
if (b.size < length)
|
||||
throw IllegalArgumentException("length must be less than or equal to b.length")
|
||||
|
||||
var value: Long = 0
|
||||
for (i in 0 until length) {
|
||||
val shift = (length - 1 - i) * 8
|
||||
value += ((b[i + offset].toInt() and 0x000000FF).toLong() shl shift)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun byteArraySlice(b: ByteArray, offset: Int, length: Int): ByteArray {
|
||||
val ret = ByteArray(length)
|
||||
for (i in 0 until length)
|
||||
ret[i] = b[offset + i]
|
||||
return ret
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@Throws(Exception::class)
|
||||
fun xmlNodeToString(node: Node): String {
|
||||
// The amount of code required to do simple things in Java is incredible.
|
||||
val source = DOMSource(node)
|
||||
val stringWriter = StringWriter()
|
||||
val result = StreamResult(stringWriter)
|
||||
val factory = TransformerFactory.newInstance()
|
||||
val transformer = factory.newTransformer()
|
||||
transformer.setOutputProperty(OutputKeys.INDENT, "yes")
|
||||
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2")
|
||||
transformer.uriResolver = null
|
||||
transformer.transform(source, result)
|
||||
return stringWriter.buffer.toString()
|
||||
}
|
||||
|
||||
fun getErrorMessage(ex: Throwable): String {
|
||||
var errorMessage: String? = ex.localizedMessage
|
||||
if (errorMessage == null)
|
||||
errorMessage = ex.message
|
||||
if (errorMessage == null)
|
||||
errorMessage = ex.toString()
|
||||
|
||||
if (ex.cause != null) {
|
||||
var causeMessage: String? = ex.cause!!.localizedMessage
|
||||
if (causeMessage == null)
|
||||
causeMessage = ex.cause!!.message
|
||||
if (causeMessage == null)
|
||||
causeMessage = ex.cause.toString()
|
||||
|
||||
errorMessage += ": $causeMessage"
|
||||
}
|
||||
|
||||
return errorMessage
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun <T> findInList(list: List<T>, matcher: Matcher<T>): T? {
|
||||
for (item in list) {
|
||||
if (matcher.matches(item)) {
|
||||
return item
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
interface Matcher<T> {
|
||||
fun matches(t: T): Boolean
|
||||
}
|
||||
|
||||
fun selectAppFile(tag: DesfireProtocol, appID: Int, fileID: Int): DesfireFileSettings? {
|
||||
try {
|
||||
tag.selectApp(appID)
|
||||
} catch (e: DesfireException) {
|
||||
Log.w(TAG, "App not found")
|
||||
return null
|
||||
}
|
||||
|
||||
return try {
|
||||
tag.getFileSettings(fileID)
|
||||
} catch (e: DesfireException) {
|
||||
Log.w(TAG, "File not found")
|
||||
null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun arrayContains(arr: IntArray, item: Int): Boolean {
|
||||
for (i in arr)
|
||||
if (i == item)
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun containsAppFile(tag: DesfireProtocol, appID: Int, fileID: Int): Boolean {
|
||||
try {
|
||||
tag.selectApp(appID)
|
||||
} catch (e: DesfireException) {
|
||||
Log.w(TAG, "App not found")
|
||||
Log.w(TAG, e)
|
||||
return false
|
||||
}
|
||||
|
||||
return try {
|
||||
arrayContains(tag.fileList, fileID)
|
||||
} catch (e: DesfireException) {
|
||||
Log.w(TAG, "File not found")
|
||||
Log.w(TAG, e)
|
||||
false
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.codebutler.farebot.card.desfire
|
||||
|
||||
/**
|
||||
* Created by Jakob Wenzel on 16.11.13.
|
||||
*/
|
||||
class DesfireException : Exception {
|
||||
constructor(message: String) : super(message)
|
||||
constructor(cause: Throwable) : super(cause)
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
/**
|
||||
* DesfireFile.kt
|
||||
*
|
||||
* Copyright (C) 2011 Eric Butler
|
||||
* Copyright (C) 2019 <seil0@mosad.xyz>
|
||||
*
|
||||
* Authors:
|
||||
* Eric Butler <eric@codebutler.com>
|
||||
*
|
||||
* 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, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.codebutler.farebot.card.desfire
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.codebutler.farebot.card.desfire.DesfireFileSettings.RecordDesfireFileSettings
|
||||
import org.apache.commons.lang3.ArrayUtils
|
||||
|
||||
|
||||
open class DesfireFile private constructor(val id: Int, private val fileSettings: DesfireFileSettings?, val data: ByteArray) :
|
||||
Parcelable {
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeInt(id)
|
||||
if (this is InvalidDesfireFile) {
|
||||
parcel.writeInt(1)
|
||||
parcel.writeString(this.errorMessage)
|
||||
} else {
|
||||
parcel.writeInt(0)
|
||||
parcel.writeParcelable(fileSettings, 0)
|
||||
parcel.writeInt(data.size)
|
||||
parcel.writeByteArray(data)
|
||||
}
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
class RecordDesfireFile(fileId: Int, fileSettings: DesfireFileSettings, fileData: ByteArray) :
|
||||
DesfireFile(fileId, fileSettings, fileData) {
|
||||
private val records: Array<DesfireRecord?>
|
||||
|
||||
init {
|
||||
|
||||
val settings = fileSettings as RecordDesfireFileSettings
|
||||
|
||||
val records = arrayOfNulls<DesfireRecord>(settings.curRecords)
|
||||
for (i in 0 until settings.curRecords) {
|
||||
val offset = settings.recordSize * i
|
||||
records[i] = DesfireRecord(ArrayUtils.subarray(data, offset, offset + settings.recordSize))
|
||||
}
|
||||
this.records = records
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidDesfireFile(fileId: Int, val errorMessage: String?) : DesfireFile(fileId, null, ByteArray(0))
|
||||
|
||||
companion object {
|
||||
|
||||
fun create(fileId: Int, fileSettings: DesfireFileSettings, fileData: ByteArray): DesfireFile {
|
||||
return (fileSettings as? RecordDesfireFileSettings)?.let { RecordDesfireFile(fileId, it, fileData) }
|
||||
?: DesfireFile(fileId, fileSettings, fileData)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<DesfireFile> = object : Parcelable.Creator<DesfireFile> {
|
||||
override fun createFromParcel(source: Parcel): DesfireFile {
|
||||
val fileId = source.readInt()
|
||||
|
||||
val isError = source.readInt() == 1
|
||||
|
||||
return if (!isError) {
|
||||
val fileSettings =
|
||||
source.readParcelable<Parcelable>(DesfireFileSettings::class.java.classLoader) as DesfireFileSettings
|
||||
val dataLength = source.readInt()
|
||||
val fileData = ByteArray(dataLength)
|
||||
source.readByteArray(fileData)
|
||||
|
||||
create(fileId, fileSettings, fileData)
|
||||
} else {
|
||||
InvalidDesfireFile(fileId, source.readString())
|
||||
}
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<DesfireFile?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,248 @@
|
||||
/**
|
||||
* DesfireFileSettings.kt
|
||||
*
|
||||
* Copyright (C) 2011 Eric Butler
|
||||
* Copyright (C) 2019 <seil0@mosad.xyz>
|
||||
*
|
||||
* Authors:
|
||||
* Eric Butler <eric@codebutler.com>
|
||||
*
|
||||
* 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, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.codebutler.farebot.card.desfire
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.codebutler.farebot.Utils
|
||||
import org.apache.commons.lang3.ArrayUtils
|
||||
import java.io.ByteArrayInputStream
|
||||
|
||||
abstract class DesfireFileSettings : Parcelable {
|
||||
private val fileType: Byte
|
||||
private val commSetting: Byte
|
||||
private val accessRights: ByteArray
|
||||
|
||||
@Suppress("unused")
|
||||
val fileTypeName: String
|
||||
get() {
|
||||
return when (fileType) {
|
||||
STANDARD_DATA_FILE -> "Standard"
|
||||
BACKUP_DATA_FILE -> "Backup"
|
||||
VALUE_FILE -> "Value"
|
||||
LINEAR_RECORD_FILE -> "Linear Record"
|
||||
CYCLIC_RECORD_FILE -> "Cyclic Record"
|
||||
else -> "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
private constructor(stream: ByteArrayInputStream) {
|
||||
fileType = stream.read().toByte()
|
||||
commSetting = stream.read().toByte()
|
||||
|
||||
accessRights = ByteArray(2)
|
||||
stream.read(accessRights, 0, accessRights.size)
|
||||
}
|
||||
|
||||
private constructor(fileType: Byte, commSetting: Byte, accessRights: ByteArray) {
|
||||
this.fileType = fileType
|
||||
this.commSetting = commSetting
|
||||
this.accessRights = accessRights
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeByte(fileType)
|
||||
parcel.writeByte(commSetting)
|
||||
parcel.writeInt(accessRights.size)
|
||||
parcel.writeByteArray(accessRights)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
class StandardDesfireFileSettings : DesfireFileSettings {
|
||||
private val fileSize: Int
|
||||
|
||||
internal constructor(stream: ByteArrayInputStream) : super(stream) {
|
||||
val buf = ByteArray(3)
|
||||
stream.read(buf, 0, buf.size)
|
||||
ArrayUtils.reverse(buf)
|
||||
fileSize = Utils.byteArrayToInt(buf)
|
||||
}
|
||||
|
||||
internal constructor(
|
||||
fileType: Byte,
|
||||
commSetting: Byte,
|
||||
accessRights: ByteArray,
|
||||
fileSize: Int
|
||||
) : super(fileType, commSetting, accessRights) {
|
||||
this.fileSize = fileSize
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
super.writeToParcel(parcel, flags)
|
||||
parcel.writeInt(fileSize)
|
||||
}
|
||||
}
|
||||
|
||||
class RecordDesfireFileSettings : DesfireFileSettings {
|
||||
private val maxRecords: Int
|
||||
val recordSize: Int
|
||||
val curRecords: Int
|
||||
|
||||
constructor(stream: ByteArrayInputStream) : super(stream) {
|
||||
|
||||
var buf = ByteArray(3)
|
||||
stream.read(buf, 0, buf.size)
|
||||
ArrayUtils.reverse(buf)
|
||||
recordSize = Utils.byteArrayToInt(buf)
|
||||
|
||||
buf = ByteArray(3)
|
||||
stream.read(buf, 0, buf.size)
|
||||
ArrayUtils.reverse(buf)
|
||||
maxRecords = Utils.byteArrayToInt(buf)
|
||||
|
||||
buf = ByteArray(3)
|
||||
stream.read(buf, 0, buf.size)
|
||||
ArrayUtils.reverse(buf)
|
||||
curRecords = Utils.byteArrayToInt(buf)
|
||||
}
|
||||
|
||||
internal constructor(
|
||||
fileType: Byte,
|
||||
commSetting: Byte,
|
||||
accessRights: ByteArray,
|
||||
recordSize: Int,
|
||||
maxRecords: Int,
|
||||
curRecords: Int
|
||||
) : super(fileType, commSetting, accessRights) {
|
||||
this.recordSize = recordSize
|
||||
this.maxRecords = maxRecords
|
||||
this.curRecords = curRecords
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
super.writeToParcel(parcel, flags)
|
||||
parcel.writeInt(recordSize)
|
||||
parcel.writeInt(maxRecords)
|
||||
parcel.writeInt(curRecords)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ValueDesfireFileSettings(stream: ByteArrayInputStream) : DesfireFileSettings(stream) {
|
||||
private val lowerLimit: Int
|
||||
private val upperLimit: Int
|
||||
val value: Int
|
||||
private val limitedCreditEnabled: Byte
|
||||
|
||||
init {
|
||||
|
||||
var buf = ByteArray(4)
|
||||
stream.read(buf, 0, buf.size)
|
||||
ArrayUtils.reverse(buf)
|
||||
lowerLimit = Utils.byteArrayToInt(buf)
|
||||
|
||||
buf = ByteArray(4)
|
||||
stream.read(buf, 0, buf.size)
|
||||
ArrayUtils.reverse(buf)
|
||||
upperLimit = Utils.byteArrayToInt(buf)
|
||||
|
||||
buf = ByteArray(4)
|
||||
stream.read(buf, 0, buf.size)
|
||||
ArrayUtils.reverse(buf)
|
||||
value = Utils.byteArrayToInt(buf)
|
||||
|
||||
|
||||
buf = ByteArray(1)
|
||||
stream.read(buf, 0, buf.size)
|
||||
limitedCreditEnabled = buf[0]
|
||||
|
||||
//http://www.skyetek.com/docs/m2/desfire.pdf
|
||||
//http://neteril.org/files/M075031_desfire.pdf
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
super.writeToParcel(parcel, flags)
|
||||
parcel.writeInt(lowerLimit)
|
||||
parcel.writeInt(upperLimit)
|
||||
parcel.writeInt(value)
|
||||
parcel.writeByte(limitedCreditEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
class UnsupportedDesfireFileSettings(fileType: Byte) :
|
||||
DesfireFileSettings(fileType, java.lang.Byte.MIN_VALUE, ByteArray(0))
|
||||
|
||||
companion object {
|
||||
|
||||
/* DesfireFile Types */
|
||||
internal const val STANDARD_DATA_FILE = 0x00.toByte()
|
||||
internal const val BACKUP_DATA_FILE = 0x01.toByte()
|
||||
internal const val VALUE_FILE = 0x02.toByte()
|
||||
internal const val LINEAR_RECORD_FILE = 0x03.toByte()
|
||||
internal const val CYCLIC_RECORD_FILE = 0x04.toByte()
|
||||
|
||||
@Throws(DesfireException::class)
|
||||
fun create(data: ByteArray): DesfireFileSettings {
|
||||
val fileType = data[0]
|
||||
|
||||
val stream = ByteArrayInputStream(data)
|
||||
|
||||
return if (fileType == STANDARD_DATA_FILE || fileType == BACKUP_DATA_FILE)
|
||||
StandardDesfireFileSettings(stream)
|
||||
else if (fileType == LINEAR_RECORD_FILE || fileType == CYCLIC_RECORD_FILE)
|
||||
RecordDesfireFileSettings(stream)
|
||||
else if (fileType == VALUE_FILE)
|
||||
ValueDesfireFileSettings(stream)
|
||||
else
|
||||
throw DesfireException("Unknown file type: " + Integer.toHexString(fileType.toInt()))
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<DesfireFileSettings> = object : Parcelable.Creator<DesfireFileSettings> {
|
||||
override fun createFromParcel(source: Parcel): DesfireFileSettings {
|
||||
val fileType = source.readByte()
|
||||
val commSetting = source.readByte()
|
||||
val accessRights = ByteArray(source.readInt())
|
||||
source.readByteArray(accessRights)
|
||||
|
||||
if (fileType == STANDARD_DATA_FILE || fileType == BACKUP_DATA_FILE) {
|
||||
val fileSize = source.readInt()
|
||||
return StandardDesfireFileSettings(fileType, commSetting, accessRights, fileSize)
|
||||
} else if (fileType == LINEAR_RECORD_FILE || fileType == CYCLIC_RECORD_FILE) {
|
||||
val recordSize = source.readInt()
|
||||
val maxRecords = source.readInt()
|
||||
val curRecords = source.readInt()
|
||||
return RecordDesfireFileSettings(
|
||||
fileType,
|
||||
commSetting,
|
||||
accessRights,
|
||||
recordSize,
|
||||
maxRecords,
|
||||
curRecords
|
||||
)
|
||||
} else {
|
||||
return UnsupportedDesfireFileSettings(fileType)
|
||||
}
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<DesfireFileSettings?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,181 @@
|
||||
/**
|
||||
* DesfireManufacturingData.kt
|
||||
*
|
||||
* Copyright (C) 2011 Eric Butler
|
||||
* Copyright (C) 2019 <seil0@mosad.xyz>
|
||||
*
|
||||
* Authors:
|
||||
* Eric Butler <eric@codebutler.com>
|
||||
*
|
||||
* 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, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.codebutler.farebot.card.desfire
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.codebutler.farebot.Utils
|
||||
import org.w3c.dom.Element
|
||||
|
||||
import java.io.ByteArrayInputStream
|
||||
|
||||
class DesfireManufacturingData : Parcelable {
|
||||
private val hwVendorID: Int
|
||||
private val hwType: Int
|
||||
private val hwSubType: Int
|
||||
private val hwMajorVersion: Int
|
||||
private val hwMinorVersion: Int
|
||||
private val hwStorageSize: Int
|
||||
private val hwProtocol: Int
|
||||
|
||||
private val swVendorID: Int
|
||||
private val swType: Int
|
||||
private val swSubType: Int
|
||||
private val swMajorVersion: Int
|
||||
private val swMinorVersion: Int
|
||||
private val swStorageSize: Int
|
||||
private val swProtocol: Int
|
||||
|
||||
private val uid: Int
|
||||
private val batchNo: Int
|
||||
private val weekProd: Int
|
||||
private val yearProd: Int
|
||||
|
||||
constructor(data: ByteArray) {
|
||||
val stream = ByteArrayInputStream(data)
|
||||
hwVendorID = stream.read()
|
||||
hwType = stream.read()
|
||||
hwSubType = stream.read()
|
||||
hwMajorVersion = stream.read()
|
||||
hwMinorVersion = stream.read()
|
||||
hwStorageSize = stream.read()
|
||||
hwProtocol = stream.read()
|
||||
|
||||
swVendorID = stream.read()
|
||||
swType = stream.read()
|
||||
swSubType = stream.read()
|
||||
swMajorVersion = stream.read()
|
||||
swMinorVersion = stream.read()
|
||||
swStorageSize = stream.read()
|
||||
swProtocol = stream.read()
|
||||
|
||||
// FIXME: This has fewer digits than what's contained in EXTRA_ID, why?
|
||||
var buf = ByteArray(7)
|
||||
stream.read(buf, 0, buf.size)
|
||||
uid = Utils.byteArrayToInt(buf)
|
||||
|
||||
// FIXME: This is returning a negative number. Probably is unsigned.
|
||||
buf = ByteArray(5)
|
||||
stream.read(buf, 0, buf.size)
|
||||
batchNo = Utils.byteArrayToInt(buf)
|
||||
|
||||
// FIXME: These numbers aren't making sense.
|
||||
weekProd = stream.read()
|
||||
yearProd = stream.read()
|
||||
}
|
||||
|
||||
private constructor(element: Element) {
|
||||
hwVendorID = Integer.parseInt(element.getElementsByTagName("hw-vendor-id").item(0).textContent)
|
||||
hwType = Integer.parseInt(element.getElementsByTagName("hw-type").item(0).textContent)
|
||||
hwSubType = Integer.parseInt(element.getElementsByTagName("hw-sub-type").item(0).textContent)
|
||||
hwMajorVersion = Integer.parseInt(element.getElementsByTagName("hw-major-version").item(0).textContent)
|
||||
hwMinorVersion = Integer.parseInt(element.getElementsByTagName("hw-minor-version").item(0).textContent)
|
||||
hwStorageSize = Integer.parseInt(element.getElementsByTagName("hw-storage-size").item(0).textContent)
|
||||
hwProtocol = Integer.parseInt(element.getElementsByTagName("hw-protocol").item(0).textContent)
|
||||
|
||||
swVendorID = Integer.parseInt(element.getElementsByTagName("sw-vendor-id").item(0).textContent)
|
||||
swType = Integer.parseInt(element.getElementsByTagName("sw-type").item(0).textContent)
|
||||
swSubType = Integer.parseInt(element.getElementsByTagName("sw-sub-type").item(0).textContent)
|
||||
swMajorVersion = Integer.parseInt(element.getElementsByTagName("sw-major-version").item(0).textContent)
|
||||
swMinorVersion = Integer.parseInt(element.getElementsByTagName("sw-minor-version").item(0).textContent)
|
||||
swStorageSize = Integer.parseInt(element.getElementsByTagName("sw-storage-size").item(0).textContent)
|
||||
swProtocol = Integer.parseInt(element.getElementsByTagName("sw-protocol").item(0).textContent)
|
||||
|
||||
uid = Integer.parseInt(element.getElementsByTagName("uid").item(0).textContent)
|
||||
batchNo = Integer.parseInt(element.getElementsByTagName("batch-no").item(0).textContent)
|
||||
weekProd = Integer.parseInt(element.getElementsByTagName("week-prod").item(0).textContent)
|
||||
yearProd = Integer.parseInt(element.getElementsByTagName("year-prod").item(0).textContent)
|
||||
}
|
||||
|
||||
private constructor(parcel: Parcel) {
|
||||
hwVendorID = parcel.readInt()
|
||||
hwType = parcel.readInt()
|
||||
hwSubType = parcel.readInt()
|
||||
hwMajorVersion = parcel.readInt()
|
||||
hwMinorVersion = parcel.readInt()
|
||||
hwStorageSize = parcel.readInt()
|
||||
hwProtocol = parcel.readInt()
|
||||
|
||||
swVendorID = parcel.readInt()
|
||||
swType = parcel.readInt()
|
||||
swSubType = parcel.readInt()
|
||||
swMajorVersion = parcel.readInt()
|
||||
swMinorVersion = parcel.readInt()
|
||||
swStorageSize = parcel.readInt()
|
||||
swProtocol = parcel.readInt()
|
||||
|
||||
uid = parcel.readInt()
|
||||
batchNo = parcel.readInt()
|
||||
weekProd = parcel.readInt()
|
||||
yearProd = parcel.readInt()
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeInt(hwVendorID)
|
||||
parcel.writeInt(hwType)
|
||||
parcel.writeInt(hwSubType)
|
||||
parcel.writeInt(hwMajorVersion)
|
||||
parcel.writeInt(hwMinorVersion)
|
||||
parcel.writeInt(hwStorageSize)
|
||||
parcel.writeInt(hwProtocol)
|
||||
|
||||
parcel.writeInt(swVendorID)
|
||||
parcel.writeInt(swType)
|
||||
parcel.writeInt(swSubType)
|
||||
parcel.writeInt(swMajorVersion)
|
||||
parcel.writeInt(swMinorVersion)
|
||||
parcel.writeInt(swStorageSize)
|
||||
parcel.writeInt(swProtocol)
|
||||
|
||||
parcel.writeInt(uid)
|
||||
parcel.writeInt(batchNo)
|
||||
parcel.writeInt(weekProd)
|
||||
parcel.writeInt(yearProd)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@Suppress("unused")
|
||||
fun fromXml(element: Element): DesfireManufacturingData {
|
||||
return DesfireManufacturingData(element)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<DesfireManufacturingData> =
|
||||
object : Parcelable.Creator<DesfireManufacturingData> {
|
||||
override fun createFromParcel(source: Parcel): DesfireManufacturingData {
|
||||
return DesfireManufacturingData(source)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<DesfireManufacturingData?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,214 @@
|
||||
/**
|
||||
* DesfireProtocol.kt
|
||||
*
|
||||
* Copyright (C) 2011 Eric Butler
|
||||
* Copyright (C) 2019 <seil0@mosad.xyz>
|
||||
*
|
||||
* Authors:
|
||||
* Eric Butler <eric@codebutler.com>
|
||||
*
|
||||
* 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, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.codebutler.farebot.card.desfire
|
||||
|
||||
import android.nfc.tech.IsoDep
|
||||
import com.codebutler.farebot.Utils
|
||||
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils
|
||||
import kotlin.experimental.and
|
||||
|
||||
class DesfireProtocol(private val mTagTech: IsoDep) {
|
||||
|
||||
@Suppress("unused")
|
||||
val manufacturingData: DesfireManufacturingData
|
||||
@Throws(DesfireException::class)
|
||||
get() {
|
||||
val respBuffer = sendRequest(GET_MANUFACTURING_DATA)
|
||||
|
||||
if (respBuffer.size != 28)
|
||||
throw DesfireException("Invalid response")
|
||||
|
||||
return DesfireManufacturingData(respBuffer)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
val appList: IntArray
|
||||
@Throws(DesfireException::class)
|
||||
get() {
|
||||
val appDirBuf = sendRequest(GET_APPLICATION_DIRECTORY)
|
||||
|
||||
val appIds = IntArray(appDirBuf.size / 3)
|
||||
|
||||
var app = 0
|
||||
while (app < appDirBuf.size) {
|
||||
val appId = ByteArray(3)
|
||||
System.arraycopy(appDirBuf, app, appId, 0, 3)
|
||||
|
||||
appIds[app / 3] = Utils.byteArrayToInt(appId)
|
||||
app += 3
|
||||
}
|
||||
|
||||
return appIds
|
||||
}
|
||||
|
||||
val fileList: IntArray
|
||||
@Throws(DesfireException::class)
|
||||
get() {
|
||||
val buf = sendRequest(GET_FILES)
|
||||
val fileIds = IntArray(buf.size)
|
||||
for (x in buf.indices) {
|
||||
fileIds[x] = buf[x].toInt()
|
||||
}
|
||||
return fileIds
|
||||
}
|
||||
|
||||
@Throws(DesfireException::class)
|
||||
fun selectApp(appId: Int) {
|
||||
val appIdBuff = ByteArray(3)
|
||||
appIdBuff[0] = (appId and 0xFF0000 shr 16).toByte()
|
||||
appIdBuff[1] = (appId and 0xFF00 shr 8).toByte()
|
||||
appIdBuff[2] = (appId and 0xFF).toByte()
|
||||
|
||||
sendRequest(SELECT_APPLICATION, appIdBuff)
|
||||
}
|
||||
|
||||
@Throws(DesfireException::class)
|
||||
fun getFileSettings(fileNo: Int): DesfireFileSettings {
|
||||
val data = sendRequest(GET_FILE_SETTINGS, byteArrayOf(fileNo.toByte()))
|
||||
return DesfireFileSettings.create(data)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@Throws(DesfireException::class)
|
||||
fun readFile(fileNo: Int): ByteArray {
|
||||
return sendRequest(
|
||||
READ_DATA,
|
||||
byteArrayOf(
|
||||
fileNo.toByte(),
|
||||
0x0.toByte(),
|
||||
0x0.toByte(),
|
||||
0x0.toByte(),
|
||||
0x0.toByte(),
|
||||
0x0.toByte(),
|
||||
0x0.toByte()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@Throws(DesfireException::class)
|
||||
fun readRecord(fileNum: Int): ByteArray {
|
||||
return sendRequest(
|
||||
READ_RECORD,
|
||||
byteArrayOf(
|
||||
fileNum.toByte(),
|
||||
0x0.toByte(),
|
||||
0x0.toByte(),
|
||||
0x0.toByte(),
|
||||
0x0.toByte(),
|
||||
0x0.toByte(),
|
||||
0x0.toByte()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Throws(DesfireException::class)
|
||||
fun readValue(fileNum: Int): Int {
|
||||
val buf = sendRequest(READ_VALUE, byteArrayOf(fileNum.toByte()))
|
||||
ArrayUtils.reverse(buf)
|
||||
return Utils.byteArrayToInt(buf)
|
||||
}
|
||||
|
||||
@Throws(DesfireException::class)
|
||||
private fun sendRequest(command: Byte, parameters: ByteArray? = null): ByteArray {
|
||||
val output = ByteArrayOutputStream()
|
||||
|
||||
var recvBuffer: ByteArray
|
||||
try {
|
||||
recvBuffer = mTagTech.transceive(wrapMessage(command, parameters))
|
||||
} catch (e: IOException) {
|
||||
throw DesfireException(e)
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if (recvBuffer[recvBuffer.size - 2] != 0x91.toByte())
|
||||
throw DesfireException("Invalid response")
|
||||
|
||||
output.write(recvBuffer, 0, recvBuffer.size - 2)
|
||||
|
||||
val status = recvBuffer[recvBuffer.size - 1]
|
||||
if (status == OPERATION_OK) {
|
||||
break
|
||||
} else if (status == ADDITIONAL_FRAME) {
|
||||
try {
|
||||
recvBuffer = mTagTech.transceive(wrapMessage(GET_ADDITIONAL_FRAME, null))
|
||||
} catch (e: IOException) {
|
||||
throw DesfireException(e)
|
||||
}
|
||||
|
||||
} else if (status == PERMISSION_DENIED) {
|
||||
throw DesfireException("Permission denied")
|
||||
} else {
|
||||
throw DesfireException("Unknown status code: " + Integer.toHexString((status and 0xFF.toByte()).toInt()))
|
||||
}
|
||||
}
|
||||
|
||||
return output.toByteArray()
|
||||
}
|
||||
|
||||
@Throws(DesfireException::class)
|
||||
private fun wrapMessage(command: Byte, parameters: ByteArray?): ByteArray {
|
||||
val stream = ByteArrayOutputStream()
|
||||
|
||||
stream.write(0x90.toByte().toInt())
|
||||
stream.write(command.toInt())
|
||||
stream.write(0x00.toByte().toInt())
|
||||
stream.write(0x00.toByte().toInt())
|
||||
if (parameters != null) {
|
||||
stream.write(parameters.size.toByte().toInt())
|
||||
try {
|
||||
stream.write(parameters)
|
||||
} catch (e: IOException) {
|
||||
throw DesfireException(e)
|
||||
}
|
||||
|
||||
}
|
||||
stream.write(0x00.toByte().toInt())
|
||||
|
||||
return stream.toByteArray()
|
||||
}
|
||||
|
||||
companion object {
|
||||
/* Commands */
|
||||
internal const val GET_MANUFACTURING_DATA = 0x60.toByte()
|
||||
internal const val GET_APPLICATION_DIRECTORY = 0x6A.toByte()
|
||||
internal const val GET_ADDITIONAL_FRAME = 0xAF.toByte()
|
||||
internal const val SELECT_APPLICATION = 0x5A.toByte()
|
||||
internal const val READ_DATA = 0xBD.toByte()
|
||||
internal const val READ_RECORD = 0xBB.toByte()
|
||||
internal const val READ_VALUE = 0x6C.toByte()
|
||||
internal const val GET_FILES = 0x6F.toByte()
|
||||
internal const val GET_FILE_SETTINGS = 0xF5.toByte()
|
||||
|
||||
/* Status codes */
|
||||
internal const val OPERATION_OK = 0x00.toByte()
|
||||
internal const val PERMISSION_DENIED = 0x9D.toByte()
|
||||
internal const val ADDITIONAL_FRAME = 0xAF.toByte()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* DesfireRecord.kt
|
||||
*
|
||||
* Copyright (C) 2011 Eric Butler
|
||||
* Copyright (C) 2019 <seil0@mosad.xyz>
|
||||
*
|
||||
* Authors:
|
||||
* Eric Butler <eric@codebutler.com>
|
||||
*
|
||||
* 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, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.codebutler.farebot.card.desfire
|
||||
|
||||
class DesfireRecord(val data: ByteArray)
|
@ -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,8 +22,16 @@
|
||||
|
||||
package org.mosad.seil0.projectlaogai
|
||||
|
||||
import android.graphics.Color
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
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
|
||||
@ -32,28 +40,36 @@ import androidx.core.view.GravityCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import com.afollestad.aesthetic.Aesthetic
|
||||
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.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.PreferencesController.Companion.cCourse
|
||||
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 org.mosad.seil0.projectlaogai.controller.TCoRAPIController
|
||||
import org.mosad.seil0.projectlaogai.controller.NFCMensaCard
|
||||
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 java.sql.Date
|
||||
import java.util.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
/**
|
||||
* 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
|
||||
private lateinit var intentFiltersArray: Array<IntentFilter>
|
||||
private lateinit var techListsArray: Array<Array<String>>
|
||||
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)
|
||||
@ -61,31 +77,8 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
||||
|
||||
// load mensa, timetable and color
|
||||
load()
|
||||
|
||||
// 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.
|
||||
Aesthetic.config {
|
||||
colorPrimary(Color.BLACK)
|
||||
colorPrimaryDark(Color.BLACK)
|
||||
colorAccent(Color.parseColor("#3F51B5"))
|
||||
apply()
|
||||
}
|
||||
|
||||
SettingsFragment().selectCourse(this)
|
||||
} else {
|
||||
Aesthetic.config {
|
||||
colorPrimary(cColorPrimary)
|
||||
colorPrimaryDark(cColorPrimary)
|
||||
colorAccent(cColorAccent)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
//init home fragment
|
||||
val fragmentTransaction: FragmentTransaction = supportFragmentManager.beginTransaction()
|
||||
fragmentTransaction.replace(R.id.fragment_container, activeFragment)
|
||||
fragmentTransaction.commit()
|
||||
initAesthetic()
|
||||
initForegroundDispatch()
|
||||
|
||||
val toggle = ActionBarDrawerToggle(
|
||||
this, drawer_layout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close
|
||||
@ -94,22 +87,48 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
||||
toggle.syncState()
|
||||
|
||||
nav_view.setNavigationItemSelectedListener(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) {
|
||||
super.onNewIntent(intent)
|
||||
|
||||
if (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action)
|
||||
NFCMensaCard.readBalance(intent, this)
|
||||
}
|
||||
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
Aesthetic.resume(this)
|
||||
if(useNFC)
|
||||
adapter.enableForegroundDispatch(this, pendingIntent, intentFiltersArray, techListsArray)
|
||||
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
Aesthetic.pause(this)
|
||||
if(useNFC)
|
||||
adapter.disableForegroundDispatch(this)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (drawer_layout.isDrawerOpen(GravityCompat.START)) {
|
||||
drawer_layout.closeDrawer(GravityCompat.START)
|
||||
} else {
|
||||
// TODO only call on double tap
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
@ -132,22 +151,14 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
||||
|
||||
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
||||
// Handle navigation view item clicks here.
|
||||
when (item.itemId) {
|
||||
R.id.nav_home -> {
|
||||
activeFragment = HomeFragment()
|
||||
}
|
||||
R.id.nav_mensa -> {
|
||||
activeFragment = MensaFragment()
|
||||
}
|
||||
R.id.nav_timetable -> {
|
||||
activeFragment = TimeTableFragment()
|
||||
}
|
||||
R.id.nav_moodle -> {
|
||||
activeFragment = MoodleFragment()
|
||||
}
|
||||
R.id.nav_settings -> {
|
||||
activeFragment = SettingsFragment()
|
||||
}
|
||||
activeFragment = when(item.itemId) {
|
||||
R.id.nav_home -> HomeFragment()
|
||||
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()
|
||||
}
|
||||
|
||||
val fragmentTransaction: FragmentTransaction = supportFragmentManager.beginTransaction()
|
||||
@ -155,6 +166,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
||||
fragmentTransaction.commit()
|
||||
|
||||
drawer_layout.closeDrawer(GravityCompat.START)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@ -163,56 +175,58 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
||||
*/
|
||||
private fun load() {
|
||||
val startupTime = measureTimeMillis {
|
||||
// load the settings
|
||||
PreferencesController.load(this) // this must be finished before doing anything else
|
||||
|
||||
val tcor = TCoRAPIController(this)
|
||||
val currentTime = System.currentTimeMillis() / 1000
|
||||
val currentDay = Calendar.getInstance().get(Calendar.DAY_OF_WEEK)
|
||||
val cal = Calendar.getInstance()
|
||||
|
||||
// timetable sunday workaround
|
||||
cal.time = Date(timetableCacheTime * 1000)
|
||||
val timetableCacheDay = cal.get(Calendar.DAY_OF_WEEK)
|
||||
|
||||
// TODO this sill backfire if someone has to update before the server finished updating the timetable at 0001/0101
|
||||
// update blocking if a) it`s monday and the last cache was not on a monday or b) the cache is older than 6 days
|
||||
if((currentDay == Calendar.MONDAY && timetableCacheDay != Calendar.MONDAY) || (System.currentTimeMillis() / 1000) - timetableCacheTime > 518400) {
|
||||
println("updating timetable after sunday!")
|
||||
val jobA = TCoRAPIController.getTimetable(cCourse.courseName, 0, this)
|
||||
val jobB = TCoRAPIController.getTimetable(cCourse.courseName, 1, this)
|
||||
|
||||
jobA.get()
|
||||
jobB.get()
|
||||
}
|
||||
|
||||
// mensa sunday workaround
|
||||
cal.time = Date(System.currentTimeMillis()) // reset to current time
|
||||
|
||||
// update blocking if it's sunday after 1500
|
||||
// TODO and the last update was before 1500
|
||||
if(currentDay == Calendar.SUNDAY && cal.get(Calendar.HOUR_OF_DAY) >= 15) {
|
||||
val jobA = TCoRAPIController.getMensa(this)
|
||||
jobA.get()
|
||||
}
|
||||
|
||||
// get the cached files
|
||||
val cache = CacheController(this)
|
||||
cache.readStartCache(cCourse.courseName)
|
||||
|
||||
// check if an update is necessary
|
||||
if(currentTime - coursesCacheTime > 86400)
|
||||
tcor.getCoursesList()
|
||||
|
||||
if(currentTime - mensaCacheTime > 10800)
|
||||
TCoRAPIController.getMensa(this)
|
||||
|
||||
if(currentTime - timetableCacheTime > 10800) {
|
||||
TCoRAPIController.getTimetable(cCourse.courseName, 0, this)
|
||||
TCoRAPIController.getTimetable(cCourse.courseName, 1, this)
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
private fun initAesthetic() {
|
||||
// If we haven't set any defaults, do that now
|
||||
if (Aesthetic.isFirstTime) {
|
||||
// set the default theme at the first app start
|
||||
Aesthetic.config {
|
||||
activityTheme(R.style.AppTheme_Light)
|
||||
apply()
|
||||
}
|
||||
|
||||
// show the onboarding activity
|
||||
startActivity(Intent(this, OnboardingActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
|
||||
Aesthetic.config {
|
||||
colorPrimary(cColorPrimary)
|
||||
colorPrimaryDark(cColorPrimary)
|
||||
colorAccent(cColorAccent)
|
||||
navigationViewMode(NavigationViewMode.SELECTED_ACCENT)
|
||||
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() {
|
||||
val nfcManager = this.getSystemService(Context.NFC_SERVICE) as NfcManager
|
||||
val nfcAdapter = nfcManager.defaultAdapter
|
||||
|
||||
if (nfcAdapter != null) {
|
||||
useNFC = true
|
||||
intentFiltersArray = arrayOf(IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED).apply { addDataType("*/*") })
|
||||
techListsArray = arrayOf(arrayOf(NfcA::class.java.name))
|
||||
adapter = NfcAdapter.getDefaultAdapter(this)
|
||||
pendingIntent = PendingIntent.getActivity(
|
||||
this, 0,
|
||||
Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0
|
||||
)
|
||||
}
|
||||
println("startup completed in $startupTime ms")
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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("•", 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) {}
|
||||
}
|
||||
|
||||
}
|
@ -1,123 +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.JsonParser
|
||||
import org.mosad.seil0.projectlaogai.hsoparser.Course
|
||||
import org.mosad.seil0.projectlaogai.hsoparser.MensaWeek
|
||||
import org.mosad.seil0.projectlaogai.hsoparser.TimetableWeek
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.FileReader
|
||||
import com.google.gson.reflect.TypeToken
|
||||
|
||||
class CacheController(cont: Context) {
|
||||
|
||||
private val context = cont
|
||||
|
||||
companion object {
|
||||
var coursesList = ArrayList<Course>()
|
||||
var mensaCurrentWeek = MensaWeek()
|
||||
var mensaNextWeek = MensaWeek()
|
||||
var timetables = ArrayList<TimetableWeek>()
|
||||
|
||||
/**
|
||||
* read current and next weeks mensa menus from the cached 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
|
||||
|
||||
val currentWeek = mensaObject.getAsJsonObject("currentWeek")
|
||||
val nextWeek = mensaObject.getAsJsonObject("nextWeek")
|
||||
|
||||
mensaCurrentWeek = Gson().fromJson(currentWeek, MensaWeek().javaClass)
|
||||
mensaNextWeek = Gson().fromJson(nextWeek, MensaWeek().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.add(Gson().fromJson(timetableObject.getAsJsonObject("timetable"), TimetableWeek().javaClass))
|
||||
} else if (timetables.size >= week) {
|
||||
timetables[week] = Gson().fromJson(timetableObject.getAsJsonObject("timetable"), TimetableWeek().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()
|
||||
readMensa(context)
|
||||
readTimetable(courseName, 0, context)
|
||||
readTimetable(courseName, 1, context)
|
||||
}
|
||||
|
||||
/**
|
||||
* read the courses list from the cached file
|
||||
* add them to the coursesList object
|
||||
*/
|
||||
private fun readCoursesList() {
|
||||
val file = File(context.filesDir, "courses.json")
|
||||
|
||||
// make sure the file exists
|
||||
if (!file.exists())
|
||||
TCoRAPIController(context).getCoursesList().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)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 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.content.Intent
|
||||
import android.nfc.NfcAdapter
|
||||
import android.nfc.Tag
|
||||
import android.nfc.tech.IsoDep
|
||||
import android.util.Log
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.customview.customView
|
||||
import com.codebutler.farebot.Utils
|
||||
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 {
|
||||
|
||||
companion object {
|
||||
private const val className = "NFCMensaCard"
|
||||
private const val appId = 0x5F8415
|
||||
private const val fileId = 1
|
||||
|
||||
/**
|
||||
* read the current balance and last payment from the mensa card
|
||||
* @param intent a nfc intent
|
||||
* @param context the context to show the dialog in
|
||||
*/
|
||||
fun readBalance(intent: Intent, context: Context) {
|
||||
val tag: Tag? = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG)
|
||||
val isoDep = IsoDep.get(tag)
|
||||
try {
|
||||
isoDep.connect()
|
||||
|
||||
val card = DesfireProtocol(isoDep)
|
||||
val settings = Utils.selectAppFile(card, appId, fileId)
|
||||
|
||||
if (settings is DesfireFileSettings.ValueDesfireFileSettings) {
|
||||
val data = try {
|
||||
card.readValue(fileId)
|
||||
} catch (ex: Exception) { 0 }
|
||||
|
||||
lookAtMe(context, data, settings.value).show()
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Log.w(className,"could not connect to tag", ex)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* generate the values for current balance and last payment
|
||||
* if the easter egg is active use schmeckles as currency
|
||||
* 0.0000075 = 1.11 / 148 / 1000 (dollar / shm / card multiplier)
|
||||
* @param context the context to access resources
|
||||
* @param currentRaw the raw card value of the current balance
|
||||
* @param lastRaw the raw card value of the last payment
|
||||
* @return the message containing all values
|
||||
*/
|
||||
private fun lookAtMe(context: Context, currentRaw: Int, lastRaw: Int): MaterialDialog {
|
||||
val dialog = MaterialDialog(context)
|
||||
.customView(R.layout.dialog_mensa_credit)
|
||||
|
||||
val current = if (!Preferences.oGiants) {
|
||||
String.format("%.2f €", (currentRaw.toFloat() / 1000))
|
||||
} else {
|
||||
String.format("%.4f shm", (currentRaw.toFloat() * 0.0000075))
|
||||
}
|
||||
|
||||
val last = if (!Preferences.oGiants) {
|
||||
String.format("%.2f €", (lastRaw.toFloat() / 1000))
|
||||
} else {
|
||||
String.format("%.4f shm", (lastRaw.toFloat() * 0.0000075))
|
||||
}
|
||||
|
||||
dialog.txtView_current.text = current
|
||||
dialog.txtView_last.text = context.resources.getString(R.string.mensa_last, last)
|
||||
|
||||
return dialog
|
||||
}
|
||||
}
|
||||
}
|
@ -1,133 +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
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
@ -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,77 +22,95 @@
|
||||
|
||||
package org.mosad.seil0.projectlaogai.controller
|
||||
|
||||
import android.content.Context
|
||||
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.*
|
||||
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
|
||||
|
||||
class TCoRAPIController(cont: Context) {
|
||||
private val context = cont
|
||||
/**
|
||||
* This Controller calls the tcor api,
|
||||
* all functions return tcor api objects
|
||||
*/
|
||||
class TCoRAPIController {
|
||||
|
||||
companion object {
|
||||
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 getMensa(context: Context) = doAsync {
|
||||
val url = URL("https://tcor.mosad.xyz/mensamenu")
|
||||
val file = File(context.filesDir, "mensa.json")
|
||||
|
||||
// read data from the API
|
||||
val mensaObject = JSONObject(url.readText()) //JSONObject(inReader.readLine())
|
||||
fun getCourseListNEW(): CoursesList {
|
||||
val url = URL("$tcorBaseURL/courseList")
|
||||
|
||||
// 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)
|
||||
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 getTimetable(courseName: String, week: Int, context: Context) = doAsync {
|
||||
val url = URL("https://tcor.mosad.xyz/timetable?courseName=$courseName&week=$week")
|
||||
val file = File(context.filesDir, "timetable-$courseName-$week.json")
|
||||
fun getMensaMenu(): MensaMenu {
|
||||
val url = URL("$tcorBaseURL/mensamenu")
|
||||
|
||||
// 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)
|
||||
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 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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* get the json object from tcor api and write it as file (cache)
|
||||
*/
|
||||
fun getCoursesList() = doAsync {
|
||||
val url = URL("https://tcor.mosad.xyz/courses")
|
||||
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)
|
||||
}
|
||||
}
|
375
app/src/main/java/org/mosad/seil0/projectlaogai/controller/cache/CacheController.kt
vendored
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val className = "CacheController"
|
||||
var coursesList = ArrayList<Course>()
|
||||
var mensaMenu = MensaMenu()
|
||||
|
||||
/**
|
||||
* update the course list, async
|
||||
*/
|
||||
fun updateCourseList(context: Context): Job {
|
||||
val file = File(context.filesDir, "courses.json")
|
||||
var courseListUp = CoursesList()
|
||||
|
||||
return GlobalScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
courseListUp = TCoRAPIController.getCourseListNEW()
|
||||
} catch (ex: Exception) {
|
||||
Log.e(className, "could not load course list from tcor", ex)
|
||||
}
|
||||
|
||||
// update coursesList array list
|
||||
coursesList = courseListUp.courses
|
||||
|
||||
// save cache file and update time
|
||||
save(file, Gson().toJson(courseListUp))
|
||||
coursesCacheTime = System.currentTimeMillis() / 1000
|
||||
Preferences.save(context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* update the mensa menu, async
|
||||
*/
|
||||
fun updateMensaMenu(context: Context): Job {
|
||||
val file = File(context.filesDir, "mensa.json")
|
||||
|
||||
return GlobalScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
mensaMenu = TCoRAPIController.getMensaMenu()
|
||||
} catch (ex: Exception) {
|
||||
Log.e(className, "could not load mensa menu from tcor", ex)
|
||||
}
|
||||
|
||||
// save cache file and update time
|
||||
save(file, Gson().toJson(mensaMenu))
|
||||
mensaCacheTime = System.currentTimeMillis() / 1000
|
||||
Preferences.save(context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* update the timetable for a week, async
|
||||
* @param courseName the course name (e.g AI1)
|
||||
* @param week the week to update (0 for the current and so on)
|
||||
*/
|
||||
fun updateTimetable(courseName: String, week: Int, context: Context): Job {
|
||||
val file = File(context.filesDir, "timetable-$courseName-$week.json")
|
||||
var timetable = TimetableWeek()
|
||||
|
||||
// try to update timetable from tcor, async
|
||||
return GlobalScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
timetable = TCoRAPIController.getTimetable(courseName, week)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(className, "could not load timetable $courseName[$week] from tcor", ex)
|
||||
}
|
||||
|
||||
// update timetable in TTC
|
||||
if (TimetableController.timetable.size > week) {
|
||||
TimetableController.timetable[week] = timetable
|
||||
} else {
|
||||
TimetableController.timetable.add(timetable)
|
||||
}
|
||||
|
||||
// save cache file and update time
|
||||
save(file, Gson().toJson(timetable))
|
||||
timetableCacheTime = System.currentTimeMillis() / 1000
|
||||
Preferences.save(context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* update all additional subject lessons, async
|
||||
*/
|
||||
fun updateAdditionalLessons(context: Context): Job {
|
||||
val fileLessons = File(context.filesDir, "additional_lessons.json")
|
||||
|
||||
return GlobalScope.launch(Dispatchers.IO) {
|
||||
TimetableController.subjectMap.forEach { (courseName, subjects) ->
|
||||
// update all subjects for a course
|
||||
subjects.forEach {subject ->
|
||||
try {
|
||||
TCoRAPIController.getLessons(courseName, subject, 0).forEach { lesson ->
|
||||
TimetableController.addLesson(courseName, subject, lesson)
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Log.e(className, "could not load $courseName: $subject", ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
save(fileLessons, Gson().toJson(TimetableController.lessonMap))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* save changes in lessonMap and subjectMap,
|
||||
* called on addSubject or removeSubject
|
||||
*/
|
||||
fun saveAdditionalSubjects(context: Context) {
|
||||
val fileLessons = File(context.filesDir, "additional_lessons.json")
|
||||
val fileSubjects = File(context.filesDir, "additional_subjects.json")
|
||||
|
||||
save(fileLessons, Gson().toJson(TimetableController.lessonMap))
|
||||
save(fileSubjects, Gson().toJson(TimetableController.subjectMap))
|
||||
}
|
||||
|
||||
private fun save(file: File, text: String) {
|
||||
try {
|
||||
val writer = BufferedWriter(FileWriter(file))
|
||||
writer.write(text)
|
||||
writer.close()
|
||||
} catch (ex: Exception) {
|
||||
Log.e(className, "failed to write file \"${file.absoluteFile}\"", ex)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
try {
|
||||
readCoursesList()
|
||||
} catch (ex : Exception) {
|
||||
Log.e(className, "Error while reading the course list", ex)
|
||||
}
|
||||
|
||||
try {
|
||||
readMensa()
|
||||
} catch (ex : Exception) {
|
||||
Log.e(className, "Error while reading the mensa menu", ex)
|
||||
}
|
||||
|
||||
try {
|
||||
readTimetable(courseName, 0)
|
||||
} catch (ex : Exception) {
|
||||
Log.e(className, "Error while reading timetable week 0", ex)
|
||||
}
|
||||
|
||||
try {
|
||||
readTimetable(courseName, 1)
|
||||
} catch (ex : Exception) {
|
||||
Log.e(className, "Error while reading timetable week 1", ex)
|
||||
}
|
||||
|
||||
try {
|
||||
readAdditionalSubjects()
|
||||
} catch (ex : Exception) {
|
||||
Log.e(className, "Error while reading additional subjects", ex)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* read the courses list from the cached file
|
||||
* add them to the coursesList object
|
||||
*/
|
||||
private fun readCoursesList() {
|
||||
val file = File(context.filesDir, "courses.json")
|
||||
|
||||
// make sure the file exists
|
||||
if (!file.exists()) {
|
||||
runBlocking { updateCourseList(context).join() }
|
||||
return
|
||||
}
|
||||
|
||||
coursesList = FileReader(file).use {
|
||||
GsonBuilder().create().fromJson(BufferedReader(it).readLine(), CoursesList().javaClass).courses
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get the MensaMenu object from the cached json,
|
||||
* if cache is empty create the cache file
|
||||
*/
|
||||
private fun readMensa() {
|
||||
val file = File(context.filesDir, "mensa.json")
|
||||
|
||||
// make sure the file exists
|
||||
if (!file.exists()) {
|
||||
runBlocking { updateMensaMenu(context).join() }
|
||||
return
|
||||
}
|
||||
|
||||
mensaMenu = FileReader(file).use {
|
||||
GsonBuilder().create().fromJson(BufferedReader(it).readLine(), 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)
|
||||
*/
|
||||
private fun readTimetable(courseName: String, week: Int) {
|
||||
val file = File(context.filesDir, "timetable-$courseName-$week.json")
|
||||
|
||||
// if the file does not exist, call updateTimetable blocking and return
|
||||
if (!file.exists()) {
|
||||
runBlocking { updateTimetable(courseName, week, context).join() }
|
||||
return
|
||||
}
|
||||
|
||||
val timetableObject = FileReader(file).use {
|
||||
JsonParser.parseString(BufferedReader(it).readLine()).asJsonObject
|
||||
}
|
||||
|
||||
// if its a TimetableCourseWeek object migrate to TimetableWeek TODO remove converting at version 0.8
|
||||
val timetable = if(timetableObject.has("meta")) {
|
||||
Log.i(Companion.className, "trying to migrate TimetableCourseWeek to TimetableWeek")
|
||||
val timetableWC = Gson().fromJson(timetableObject, TimetableCourseWeek().javaClass)
|
||||
save(file, Gson().toJson(TimetableWeek(
|
||||
timetableWC.meta.weekIndex,
|
||||
timetableWC.meta.weekNumberYear,
|
||||
timetableWC.timetable.days
|
||||
)))
|
||||
TimetableWeek(timetableWC.meta.weekIndex, timetableWC.meta.weekNumberYear, timetableWC.timetable.days)
|
||||
} else {
|
||||
Gson().fromJson(timetableObject, TimetableWeek().javaClass)
|
||||
}
|
||||
|
||||
// update timetable in TTC
|
||||
if (TimetableController.timetable.size > week) {
|
||||
TimetableController.timetable[week] = timetable
|
||||
} else {
|
||||
TimetableController.timetable.add(timetable)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readAdditionalSubjects() {
|
||||
val fileLessons = File(context.filesDir, "additional_lessons.json")
|
||||
val fileSubjects = File(context.filesDir, "additional_subjects.json")
|
||||
|
||||
// make sure the file exists
|
||||
if (!fileLessons.exists() || !fileSubjects.exists()) {
|
||||
return
|
||||
}
|
||||
|
||||
// clear the maps before loading, just to be save
|
||||
TimetableController.lessonMap.clear()
|
||||
TimetableController.subjectMap.clear()
|
||||
|
||||
// read subjects and lessons from cache file
|
||||
FileReader(fileLessons).use {
|
||||
TimetableController.lessonMap.putAll(
|
||||
GsonBuilder().create()
|
||||
.fromJson(BufferedReader(it).readLine(), object : TypeToken<HashMap<String, Lesson>>() {}.type)
|
||||
)
|
||||
}
|
||||
|
||||
FileReader(fileSubjects).use {
|
||||
TimetableController.subjectMap.putAll(
|
||||
GsonBuilder().create()
|
||||
.fromJson(BufferedReader(it).readLine(), HashMap<String, ArrayList<String>>().javaClass)
|
||||
)
|
||||
}
|
||||
|
||||
// add lessons to timetable
|
||||
TimetableController.lessonMap.forEach { (_, lesson) ->
|
||||
TimetableController.addLessonToTimetable(lesson)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
131
app/src/main/java/org/mosad/seil0/projectlaogai/controller/cache/TimetableController.kt
vendored
Normal file
@ -0,0 +1,131 @@
|
||||
/**
|
||||
* 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 kotlinx.coroutines.Job
|
||||
import org.mosad.seil0.projectlaogai.controller.TCoRAPIController
|
||||
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.cCourse
|
||||
import org.mosad.seil0.projectlaogai.util.Lesson
|
||||
import org.mosad.seil0.projectlaogai.util.TimetableWeek
|
||||
|
||||
/**
|
||||
* The TimetableController contains the timetable, subjectMap
|
||||
* and lessonMap objects. It also contains the additional subjects logic.
|
||||
* All functions ro read or update cache files are located in the CacheController.
|
||||
*
|
||||
* TODO
|
||||
* * add second week
|
||||
* * add configurable week to addSubject() and removeSubject(), updateAdditionalLessons()
|
||||
*/
|
||||
class TimetableController {
|
||||
|
||||
companion object {
|
||||
val timetable = ArrayList<TimetableWeek>()
|
||||
val lessonMap = HashMap<String, Lesson>() // the key is courseName-subject-lessonID
|
||||
val subjectMap = HashMap<String, ArrayList<String>>() // the key is courseName
|
||||
|
||||
/**
|
||||
* update the main timetable and all additional subjects, async
|
||||
*/
|
||||
fun update(context: Context): List<Job> {
|
||||
return listOf(
|
||||
CacheController.updateTimetable(cCourse.courseName, 0, context),
|
||||
CacheController.updateTimetable(cCourse.courseName, 1, context),
|
||||
CacheController.updateAdditionalLessons(context)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* add a subject to the subjectMap and all it's lessons
|
||||
* to the lessonMap
|
||||
* @param courseName course to which the subject belongs
|
||||
* @param subject the subjects name
|
||||
*/
|
||||
fun addSubject(courseName: String, subject: String, context: Context) {
|
||||
// add subject
|
||||
if (subjectMap.containsKey(courseName)) {
|
||||
subjectMap[courseName]?.add(subject)
|
||||
} else {
|
||||
subjectMap[courseName] = arrayListOf(subject)
|
||||
}
|
||||
|
||||
// add concrete lessons
|
||||
TCoRAPIController.getLessons(courseName, subject, 0).forEach { lesson ->
|
||||
addLesson(courseName, subject, lesson)
|
||||
}
|
||||
|
||||
CacheController.saveAdditionalSubjects(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* remove a subject from the subjectMap and all it's lessons
|
||||
* from the lessonMap
|
||||
* @param courseName course to which the subject belongs
|
||||
* @param subject the subjects name
|
||||
*/
|
||||
fun removeSubject(courseName: String, subject: String, context: Context) {
|
||||
// remove subject
|
||||
subjectMap[courseName]?.remove(subject)
|
||||
|
||||
// remove concrete lessons
|
||||
val iterator = lessonMap.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val it = iterator.next()
|
||||
if(it.key.contains("$courseName-$subject")) {
|
||||
// remove the lesson from the lessons list
|
||||
iterator.remove() // use iterator to remove, otherwise ConcurrentModificationException
|
||||
|
||||
// remove the lesson from the timetable
|
||||
val id = it.value.lessonID.split(".")
|
||||
if(id.size == 3)
|
||||
timetable[0].days[id[0].toInt()].timeslots[id[1].toInt()].remove(it.value)
|
||||
}
|
||||
}
|
||||
|
||||
CacheController.saveAdditionalSubjects(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* add a lesson to the lessonMap, also add it to the timetable
|
||||
*/
|
||||
fun addLesson(courseName: String, subject: String, lesson: Lesson) {
|
||||
//the courseName, subject and lessonID, separator: -
|
||||
val key = "$courseName-$subject-${lesson.lessonID}"
|
||||
lessonMap[key] = lesson
|
||||
|
||||
addLessonToTimetable(lesson)
|
||||
}
|
||||
|
||||
/**
|
||||
* add a lesson to the timetable
|
||||
*/
|
||||
fun addLessonToTimetable(lesson: Lesson) {
|
||||
val id = lesson.lessonID.split(".")
|
||||
if(id.size == 3)
|
||||
timetable[0].days[id[0].toInt()].timeslots[id[1].toInt()].add(lesson)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
/**
|
||||
* 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.preferences
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import android.util.Log
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
import org.mosad.seil0.projectlaogai.R
|
||||
|
||||
object EncryptedPreferences {
|
||||
|
||||
var email = ""
|
||||
internal set
|
||||
|
||||
/**
|
||||
* save user email and password to encrypted preference
|
||||
*/
|
||||
fun saveCredentials(email: String, password: String, context: Context) {
|
||||
this.email = email
|
||||
|
||||
with (getEncryptedPreferences(context)?.edit()) {
|
||||
this?.putString(context.getString(R.string.save_key_user_email), email)
|
||||
this?.putString(context.getString(R.string.save_key_user_password), password)
|
||||
this?.apply()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* read user email and password from encrypted preference
|
||||
* @return Pair(email, password)
|
||||
*/
|
||||
fun readCredentials(context: Context): Pair<String, String> {
|
||||
return with (getEncryptedPreferences(context)) {
|
||||
email = this?.getString(context.getString(R.string.save_key_user_email), "").toString()
|
||||
|
||||
Pair(
|
||||
this?.getString(context.getString(R.string.save_key_user_email), "").toString(),
|
||||
this?.getString(context.getString(R.string.save_key_user_password), "").toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* initially load the stored values
|
||||
*/
|
||||
fun load(context: Context) {
|
||||
with(getEncryptedPreferences(context)) {
|
||||
email = this?.getString(
|
||||
context.getString(R.string.save_key_user_email), ""
|
||||
).toString()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* create a encrypted shared preference
|
||||
*/
|
||||
private fun getEncryptedPreferences(context: Context): SharedPreferences? {
|
||||
return try {
|
||||
val spec = KeyGenParameterSpec.Builder(MasterKey.DEFAULT_MASTER_KEY_ALIAS,
|
||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||
.setKeySize(MasterKey.DEFAULT_AES_GCM_MASTER_KEY_SIZE)
|
||||
.build()
|
||||
|
||||
val masterKey = MasterKey.Builder(context)
|
||||
.setKeyGenParameterSpec(spec)
|
||||
.build()
|
||||
|
||||
EncryptedSharedPreferences.create(
|
||||
context,
|
||||
context.getString(R.string.encrypted_preference_file_key),
|
||||
masterKey,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(javaClass.name, "Could not create encrypted shared preference.", ex)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,192 @@
|
||||
/**
|
||||
* 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.preferences
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import org.mosad.seil0.projectlaogai.R
|
||||
import org.mosad.seil0.projectlaogai.util.Course
|
||||
|
||||
/**
|
||||
* The PreferencesController class
|
||||
* contains all preferences and global variables that exist in this app
|
||||
*/
|
||||
object Preferences {
|
||||
|
||||
var coursesCacheTime: Long = 0
|
||||
var mensaCacheTime: Long = 0
|
||||
var timetableCacheTime: Long = 0
|
||||
var cColorPrimary: Int = Color.parseColor("#009688")
|
||||
var cColorAccent: Int = Color.parseColor("#0096ff")
|
||||
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
|
||||
|
||||
// TODO move!
|
||||
var themePrimary = 0
|
||||
var themeSecondary = 0
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
/**
|
||||
* initially load the stored values
|
||||
*/
|
||||
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
|
||||
), cColorPrimary)
|
||||
cColorAccent = sharedPref.getInt(context.getString(
|
||||
R.string.save_key_colorAccent
|
||||
), cColorAccent)
|
||||
|
||||
// load showBuffet
|
||||
cShowBuffet = sharedPref.getBoolean(context.getString(
|
||||
R.string.save_key_showBuffet
|
||||
), true)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,200 @@
|
||||
/**
|
||||
* 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.fragments
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import kotlinx.android.synthetic.main.fragment_grades.*
|
||||
import kotlinx.coroutines.*
|
||||
import org.mosad.seil0.projectlaogai.R
|
||||
import org.mosad.seil0.projectlaogai.controller.QISPOSParser
|
||||
import org.mosad.seil0.projectlaogai.controller.preferences.EncryptedPreferences
|
||||
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences
|
||||
import org.mosad.seil0.projectlaogai.uicomponents.DayCardView
|
||||
import org.mosad.seil0.projectlaogai.uicomponents.GradeLinearLayout
|
||||
import org.mosad.seil0.projectlaogai.uicomponents.dialogs.LoginDialog
|
||||
|
||||
/**
|
||||
* The grades fragment class
|
||||
* contains all needed parts to display and the grades screen
|
||||
*/
|
||||
class GradesFragment : Fragment() {
|
||||
|
||||
private lateinit var refreshLayoutGrades: SwipeRefreshLayout
|
||||
|
||||
private lateinit var parser: QISPOSParser
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_grades, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
refreshLayoutGrades = view.findViewById(R.id.refreshLayout_Grades)
|
||||
refreshLayoutGrades.isEnabled = false // disable swipe
|
||||
refreshLayoutGrades.setProgressBackgroundColorSchemeColor(Preferences.themeSecondary)
|
||||
|
||||
parser = QISPOSParser(context!!)// init the parser
|
||||
|
||||
if (checkCredentials() && checkQisposStatus()) {
|
||||
GlobalScope.launch(Dispatchers.Default) {
|
||||
addGrades()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* check if the credentials are set, if not show a login dialog
|
||||
*/
|
||||
private fun checkCredentials(): Boolean {
|
||||
val credentials = EncryptedPreferences.readCredentials(context!!)
|
||||
var credentialsPresent = false
|
||||
|
||||
// if there is no password set, show the login dialog
|
||||
if (credentials.first.isEmpty() || credentials.second.isEmpty()) {
|
||||
LoginDialog(this.context!!)
|
||||
.positiveButton {
|
||||
EncryptedPreferences.saveCredentials(email, password, context)
|
||||
addGrades()
|
||||
}
|
||||
.negativeButton {
|
||||
txtView_Loading.text = resources.getString(R.string.credentials_missing)
|
||||
}
|
||||
.show {
|
||||
email = EncryptedPreferences.email
|
||||
password = ""
|
||||
}
|
||||
} else {
|
||||
credentialsPresent = true
|
||||
}
|
||||
|
||||
return credentialsPresent
|
||||
}
|
||||
|
||||
/**
|
||||
* check if qispos is available, if not show an error
|
||||
*/
|
||||
private fun checkQisposStatus(): Boolean {
|
||||
val statusCode = parser.checkQISPOSStatus()
|
||||
|
||||
// show error if the status code is not 200
|
||||
if (statusCode != 200) {
|
||||
val infoText = resources.getString(when(statusCode) {
|
||||
503 -> R.string.qispos_unavailable
|
||||
else -> R.string.qispos_generic_error
|
||||
})
|
||||
|
||||
val img = ResourcesCompat.getDrawable(resources, R.drawable.ic_error_outline_black_24dp, null)?.apply {
|
||||
bounds = Rect(0, 0, 75, 75)
|
||||
}
|
||||
|
||||
txtView_Loading?.apply {
|
||||
text = infoText
|
||||
setCompoundDrawables(null, null, null, img)
|
||||
}
|
||||
}
|
||||
|
||||
return statusCode == 200
|
||||
}
|
||||
|
||||
// add the grades to the layout, async
|
||||
private fun addGrades() = GlobalScope.launch(Dispatchers.Default) {
|
||||
withContext(Dispatchers.Main) {
|
||||
refreshLayout_Grades.isRefreshing = true
|
||||
}
|
||||
|
||||
val grades = parser.parseGrades()
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
linLayout_Grades.removeAllViews() // clear layout
|
||||
}
|
||||
|
||||
// for each semester add a new card
|
||||
grades.forEach { semester ->
|
||||
val usedSubjects = ArrayList<String>()
|
||||
val semesterCard = DayCardView(context!!)
|
||||
semesterCard.setDayHeading(semester.key)
|
||||
|
||||
// for each subject add a new linLayout
|
||||
semester.value.forEachIndexed { index, subject ->
|
||||
if (usedSubjects.contains(subject.name)) {
|
||||
return@forEachIndexed
|
||||
}
|
||||
|
||||
// get the first sub subjects
|
||||
val subSubject = semester.value.firstOrNull {
|
||||
it.name.contains(subject.name) && it.name != subject.name
|
||||
}
|
||||
|
||||
// if sub subject is not null, add it to used subjects
|
||||
subSubject?.let {
|
||||
usedSubjects.add(it.name)
|
||||
}
|
||||
|
||||
val subjectLayout = GradeLinearLayout(context).set {
|
||||
subjectName = subject.name
|
||||
grade = subject.grade
|
||||
subSubjectName = subSubject?.name.toString()
|
||||
subGrade = subSubject?.grade.toString()
|
||||
}
|
||||
|
||||
// disable sub-subject if not set
|
||||
if (subSubject == null)
|
||||
subjectLayout.disableSubSubject()
|
||||
|
||||
// disable divider if last element
|
||||
if (index == semester.value.lastIndex || semester.value.indexOf(subSubject) == semester.value.lastIndex)
|
||||
subjectLayout.disableDivider()
|
||||
|
||||
semesterCard.getLinLayoutDay().addView(subjectLayout)
|
||||
}
|
||||
|
||||
// without context we can't access the view
|
||||
withContext(Dispatchers.Main) {
|
||||
linLayout_Grades.addView(semesterCard)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
val txtViewLegal = TextView(context).apply {
|
||||
text = resources.getString(R.string.without_guarantee)
|
||||
textAlignment = View.TEXT_ALIGNMENT_CENTER
|
||||
}
|
||||
|
||||
// stop refreshing and show legal warning
|
||||
withContext(Dispatchers.Main) {
|
||||
linLayout_Grades.addView(txtViewLegal)
|
||||
refreshLayout_Grades.isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
@ -24,26 +24,22 @@ package org.mosad.seil0.projectlaogai.fragments
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import kotlinx.android.synthetic.main.fragment_home.*
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
import kotlinx.coroutines.*
|
||||
import org.mosad.seil0.projectlaogai.R
|
||||
import org.mosad.seil0.projectlaogai.controller.CacheController.Companion.mensaCurrentWeek
|
||||
import org.mosad.seil0.projectlaogai.controller.CacheController.Companion.timetables
|
||||
import org.mosad.seil0.projectlaogai.hsoparser.DataTypes
|
||||
import org.mosad.seil0.projectlaogai.hsoparser.Meal
|
||||
import org.mosad.seil0.projectlaogai.hsoparser.NotRetardedCalendar
|
||||
import org.mosad.seil0.projectlaogai.hsoparser.TimetableDay
|
||||
import org.mosad.seil0.projectlaogai.controller.cache.CacheController.Companion.mensaMenu
|
||||
import org.mosad.seil0.projectlaogai.controller.cache.TimetableController
|
||||
import org.mosad.seil0.projectlaogai.util.Meal
|
||||
import org.mosad.seil0.projectlaogai.util.TimetableDay
|
||||
import org.mosad.seil0.projectlaogai.uicomponents.DayCardView
|
||||
import org.mosad.seil0.projectlaogai.uicomponents.LessonLinearLayout
|
||||
import org.mosad.seil0.projectlaogai.uicomponents.MealLinearLayout
|
||||
import org.mosad.seil0.projectlaogai.util.NotRetardedCalendar
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@ -53,62 +49,66 @@ import java.util.*
|
||||
*/
|
||||
class HomeFragment : Fragment() {
|
||||
|
||||
private val className = "HomeFragment"
|
||||
private val formatter = SimpleDateFormat("E dd.MM", Locale.getDefault())
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_home, container, false)
|
||||
}
|
||||
|
||||
val view: View = inflater.inflate(R.layout.fragment_home, container, false)
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
addMensaMenu().get()
|
||||
addMensaMenu()
|
||||
addTimeTable()
|
||||
|
||||
// Inflate the layout for this fragment
|
||||
return view
|
||||
}
|
||||
|
||||
/**
|
||||
* add the current mensa meal to the home screens
|
||||
*/
|
||||
private fun addMensaMenu() = doAsync {
|
||||
private fun addMensaMenu() = GlobalScope.launch(Dispatchers.Default) {
|
||||
|
||||
var dayMeals: ArrayList<Meal>
|
||||
val cal = Calendar.getInstance()
|
||||
val mensaCardView = DayCardView(context!!)
|
||||
|
||||
uiThread {
|
||||
withContext(Dispatchers.Main) {
|
||||
|
||||
if (cal.get(Calendar.HOUR_OF_DAY) < 15) {
|
||||
dayMeals = mensaCurrentWeek.days[NotRetardedCalendar().getDayOfWeekIndex()].meals
|
||||
mensaCardView.setDayHeading(resources.getString(R.string.today_date, formatter.format(cal.time)))
|
||||
} else {
|
||||
dayMeals = mensaCurrentWeek.days[NotRetardedCalendar().getTomorrowWeekIndex()].meals
|
||||
cal.add(Calendar.DATE, 1)
|
||||
mensaCardView.setDayHeading(resources.getString(R.string.tomorrow_date, formatter.format(cal.time)))
|
||||
}
|
||||
|
||||
if (dayMeals.size >= 2) {
|
||||
// get the index of the first meal, not a "Schneller Teller"
|
||||
loop@ for ((i, meal) in dayMeals.withIndex()) {
|
||||
if (meal.heading.contains("Essen")) {
|
||||
|
||||
val meal1Layout = MealLinearLayout(context)
|
||||
meal1Layout.setMeal(dayMeals[i])
|
||||
mensaCardView.getLinLayoutDay().addView(meal1Layout)
|
||||
|
||||
val meal2Layout = MealLinearLayout(context)
|
||||
meal2Layout.setMeal(dayMeals[i + 1])
|
||||
meal2Layout.disableDivider()
|
||||
mensaCardView.getLinLayoutDay().addView(meal2Layout)
|
||||
|
||||
break@loop
|
||||
}
|
||||
if (isAdded) {
|
||||
if (cal.get(Calendar.HOUR_OF_DAY) < 15) {
|
||||
dayMeals = mensaMenu.currentWeek.days[NotRetardedCalendar.getDayOfWeekIndex()].meals
|
||||
mensaCardView.setDayHeading(activity!!.resources.getString(R.string.today_date, formatter.format(cal.time)))
|
||||
} else {
|
||||
dayMeals = mensaMenu.currentWeek.days[NotRetardedCalendar.getTomorrowWeekIndex()].meals
|
||||
cal.add(Calendar.DATE, 1)
|
||||
mensaCardView.setDayHeading(activity!!.resources.getString(R.string.tomorrow_date, formatter.format(cal.time)))
|
||||
}
|
||||
|
||||
} else {
|
||||
mensaCardView.getLinLayoutDay().addView(getNoCard(resources.getString(R.string.mensa_closed)))
|
||||
if (dayMeals.size >= 2) {
|
||||
// get the index of the first meal, not a "Schneller Teller"
|
||||
loop@ for ((i, meal) in dayMeals.withIndex()) {
|
||||
if (meal.heading.contains("Essen")) {
|
||||
|
||||
val meal1Layout = MealLinearLayout(context)
|
||||
meal1Layout.setMeal(dayMeals[i])
|
||||
mensaCardView.getLinLayoutDay().addView(meal1Layout)
|
||||
|
||||
val meal2Layout = MealLinearLayout(context)
|
||||
meal2Layout.setMeal(dayMeals[i + 1])
|
||||
meal2Layout.disableDivider()
|
||||
mensaCardView.getLinLayoutDay().addView(meal2Layout)
|
||||
|
||||
break@loop
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
mensaCardView.getLinLayoutDay().addView(getNoCard(resources.getString(R.string.mensa_closed)))
|
||||
}
|
||||
|
||||
linLayout_Home.addView(mensaCardView)
|
||||
}
|
||||
|
||||
linLayout_Home.addView(mensaCardView)
|
||||
}
|
||||
|
||||
}
|
||||
@ -116,100 +116,54 @@ class HomeFragment : Fragment() {
|
||||
/**
|
||||
* add the current timetable to the home screen
|
||||
*/
|
||||
private fun addTimeTable() = doAsync {
|
||||
val dayIndex = NotRetardedCalendar().getDayOfWeekIndex()
|
||||
val cal = Calendar.getInstance()
|
||||
var dayCardView: DayCardView
|
||||
private fun addTimeTable() = GlobalScope.launch(Dispatchers.Default) {
|
||||
|
||||
uiThread {
|
||||
withContext(Dispatchers.Main) {
|
||||
|
||||
if (timetables.isNotEmpty() && dayIndex < 6) {
|
||||
|
||||
// first check the current day
|
||||
dayCardView = addDayTimetable(timetables[0].days[dayIndex])
|
||||
dayCardView.setDayHeading(resources.getString(R.string.today_date, formatter.format(cal.time)))
|
||||
|
||||
// if there are no lessons try to find the next day with a lesson
|
||||
if (dayCardView.getLinLayoutDay().childCount <= 1)
|
||||
dayCardView = findNextDay(0, dayIndex + 1)
|
||||
|
||||
linLayout_Home.addView(dayCardView)
|
||||
} else if (dayIndex == 6) {
|
||||
// if that's the case it's sunday
|
||||
dayCardView = findNextDay(1, 0)
|
||||
linLayout_Home.addView(dayCardView)
|
||||
} else {
|
||||
MaterialDialog(context!!)
|
||||
.title(R.string.error)
|
||||
.message(R.string.timetable_error)
|
||||
.show()
|
||||
// TODO log the error and send feedback
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* add the timetable of one day to the home-screen
|
||||
* @param dayTimetable the day you wish to add
|
||||
*/
|
||||
private fun addDayTimetable(dayTimetable: TimetableDay) : DayCardView{
|
||||
var helpLesson = LessonLinearLayout(context)
|
||||
val dayCardView = DayCardView(context!!)
|
||||
|
||||
for ((tsIndex, timeslot) in dayTimetable.timeslots.withIndex()) {
|
||||
|
||||
for(lesson in timeslot) {
|
||||
if(lesson.lessonSubject.isNotEmpty()) {
|
||||
|
||||
val lessonLayout = LessonLinearLayout(context)
|
||||
lessonLayout.setLesson(lesson, DataTypes().getTime()[tsIndex])
|
||||
dayCardView.getLinLayoutDay().addView(lessonLayout)
|
||||
|
||||
if (lesson != timeslot.last()) {
|
||||
lessonLayout.disableDivider()
|
||||
}
|
||||
|
||||
helpLesson = lessonLayout
|
||||
if (isAdded && TimetableController.timetable.isNotEmpty()) {
|
||||
try {
|
||||
val dayCardView = findNextDay(NotRetardedCalendar.getDayOfWeekIndex())
|
||||
linLayout_Home.addView(dayCardView)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(className, "could not load timetable", ex) // TODO send feedback
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
helpLesson.disableDivider()
|
||||
|
||||
return dayCardView
|
||||
}
|
||||
|
||||
/**
|
||||
* find the next day with a lesson
|
||||
* @param startWeekIndex the week you want to start searching
|
||||
* start at week 0, startDayIndex and search every cached week until we find a) a day with a timetable
|
||||
* or b) we find no timetable and add a no lesson card
|
||||
* @param startDayIndex the day index you want to start searching
|
||||
* @return a DayCardView with all lessons added
|
||||
*/
|
||||
private fun findNextDay(startWeekIndex: Int, startDayIndex: Int) : DayCardView{
|
||||
val cal = Calendar.getInstance()
|
||||
var dayCardView = DayCardView(context!!)
|
||||
private fun findNextDay(startDayIndex: Int): DayCardView {
|
||||
val dayCardView = DayCardView(context!!)
|
||||
var dayTimetable: TimetableDay? = null
|
||||
var dayIndexSearch = startDayIndex
|
||||
var weekIndexSearch = startWeekIndex
|
||||
loop@ while (dayTimetable == null && weekIndexSearch <= timetables.size) {
|
||||
for (i in (dayIndexSearch) ..5) {
|
||||
dayTimetable = timetables[weekIndexSearch].days[i]
|
||||
cal.add(Calendar.DATE, 1)
|
||||
var weekIndexSearch = 0
|
||||
|
||||
// add the timetable to the card, if it contains at least one lesson break!
|
||||
dayCardView = addDayTimetable(dayTimetable)
|
||||
dayCardView.setDayHeading(formatter.format(cal.time))
|
||||
while (dayTimetable == null && weekIndexSearch < TimetableController.timetable.size) {
|
||||
for (dayIndex in dayIndexSearch..5) {
|
||||
dayTimetable = TimetableController.timetable[weekIndexSearch].days[dayIndex]
|
||||
|
||||
// some wired calendar magic, calculate the correct date to be shown ((timetable week - current week * 7) + (dayIndex - dayIndex of current week)
|
||||
val daysToAdd = ((TimetableController.timetable[weekIndexSearch].weekNumberYear - NotRetardedCalendar.getWeekOfYear())
|
||||
* 7 + (dayIndex - NotRetardedCalendar.getDayOfWeekIndex()))
|
||||
dayCardView.addTimetableDay(dayTimetable, daysToAdd)
|
||||
|
||||
// if there are no lessons don't show the dayCardView
|
||||
if (dayCardView.getLinLayoutDay().childCount > 1)
|
||||
return dayCardView
|
||||
}
|
||||
dayIndexSearch = 0
|
||||
|
||||
weekIndexSearch++
|
||||
cal.add(Calendar.DATE, 1)
|
||||
dayIndexSearch = 0
|
||||
}
|
||||
|
||||
// there was no day found in the cached weeks, add no lesson card
|
||||
dayCardView.setDayHeading(formatter.format(Calendar.getInstance().time))
|
||||
dayCardView.getLinLayoutDay().addView(getNoCard(resources.getString(R.string.no_lesson_today))) // if there is no lecture at all show the no lesson card
|
||||
return dayCardView
|
||||
@ -222,11 +176,10 @@ class HomeFragment : Fragment() {
|
||||
private fun getNoCard(text: String): TextView {
|
||||
val noLesson = TextView(context)
|
||||
noLesson.text = text
|
||||
noLesson.setTextColor(ContextCompat.getColor(context!!, R.color.textPrimary))
|
||||
noLesson.textSize = 18.0F
|
||||
noLesson.setTypeface(null, Typeface.BOLD)
|
||||
noLesson.textAlignment = View.TEXT_ALIGNMENT_CENTER
|
||||
noLesson.setPadding(7,7,7,7)
|
||||
noLesson.setPadding(7, 7, 7, 7)
|
||||
|
||||
return noLesson
|
||||
}
|
||||
|
@ -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
|
||||
@ -28,17 +28,20 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import kotlinx.android.synthetic.main.fragment_mensa.*
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
import org.mosad.seil0.projectlaogai.controller.CacheController
|
||||
import org.mosad.seil0.projectlaogai.controller.CacheController.Companion.mensaCurrentWeek
|
||||
import org.mosad.seil0.projectlaogai.controller.CacheController.Companion.mensaNextWeek
|
||||
import org.mosad.seil0.projectlaogai.controller.PreferencesController.Companion.cShowBuffet
|
||||
import org.mosad.seil0.projectlaogai.controller.TCoRAPIController
|
||||
import org.mosad.seil0.projectlaogai.hsoparser.MensaWeek
|
||||
import org.mosad.seil0.projectlaogai.hsoparser.NotRetardedCalendar
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.mosad.seil0.projectlaogai.R
|
||||
import org.mosad.seil0.projectlaogai.controller.cache.CacheController
|
||||
import org.mosad.seil0.projectlaogai.controller.cache.CacheController.Companion.mensaMenu
|
||||
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences
|
||||
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.cShowBuffet
|
||||
import org.mosad.seil0.projectlaogai.uicomponents.DayCardView
|
||||
import org.mosad.seil0.projectlaogai.uicomponents.MealLinearLayout
|
||||
import org.mosad.seil0.projectlaogai.uicomponents.TextViewInfo
|
||||
import org.mosad.seil0.projectlaogai.util.MensaWeek
|
||||
import org.mosad.seil0.projectlaogai.util.NotRetardedCalendar
|
||||
|
||||
/**
|
||||
* The mensa controller class
|
||||
@ -47,22 +50,31 @@ import org.mosad.seil0.projectlaogai.uicomponents.MealLinearLayout
|
||||
class MensaFragment : Fragment() {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_mensa, container, false)
|
||||
}
|
||||
|
||||
val view: View = inflater.inflate(org.mosad.seil0.projectlaogai.R.layout.fragment_mensa, container, false)
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// init actions
|
||||
refreshAction()
|
||||
refreshLayout_Mensa.setProgressBackgroundColorSchemeColor(Preferences.themeSecondary)
|
||||
|
||||
// add the current week (week starts on sunday)
|
||||
val dayCurrent = if(NotRetardedCalendar().getDayOfWeekIndex() == 6) 0 else NotRetardedCalendar().getDayOfWeekIndex()
|
||||
addWeek(mensaCurrentWeek, dayCurrent).get()
|
||||
refreshAction() // init actions
|
||||
|
||||
// add the next week
|
||||
addWeek(mensaNextWeek, 0)
|
||||
GlobalScope.launch(Dispatchers.Default) {
|
||||
val dayCurrent = if(NotRetardedCalendar.getDayOfWeekIndex() == 6) 0 else NotRetardedCalendar.getDayOfWeekIndex()
|
||||
addWeek(mensaMenu.currentWeek, dayCurrent).join()
|
||||
|
||||
// TODO should we show a info if there is no more food this & next week?
|
||||
// add the next week
|
||||
addWeek(mensaMenu.nextWeek, 0)
|
||||
}
|
||||
|
||||
return view
|
||||
// show a info if there are no more menus
|
||||
if (linLayout_Mensa.childCount == 0) {
|
||||
val txtViewInfo = TextViewInfo(context!!).set {
|
||||
txt = resources.getString(R.string.no_more_meals)
|
||||
}
|
||||
linLayout_Mensa.addView(txtViewInfo)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -70,10 +82,9 @@ class MensaFragment : Fragment() {
|
||||
* @param menusWeek menu of type MensaWeek you want to add
|
||||
* @param dayStart the first day of the week to add
|
||||
*/
|
||||
private fun addWeek(menusWeek: MensaWeek, dayStart: Int) = doAsync {
|
||||
|
||||
uiThread {
|
||||
private fun addWeek(menusWeek: MensaWeek, dayStart: Int) = GlobalScope.launch(Dispatchers.Default) {
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
// only add the days dayStart to Fri since the mensa is closed on Sat/Sun
|
||||
for (dayIndex in dayStart..4) {
|
||||
var helpMeal = MealLinearLayout(context)
|
||||
@ -94,10 +105,9 @@ class MensaFragment : Fragment() {
|
||||
|
||||
helpMeal.disableDivider()
|
||||
|
||||
if(dayCardView.getLinLayoutDay().childCount > 1)
|
||||
if(dayCardView.getLinLayoutDay().childCount > 2)
|
||||
linLayout_Mensa.addView(dayCardView)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -105,8 +115,8 @@ class MensaFragment : Fragment() {
|
||||
/**
|
||||
* initialize the refresh action
|
||||
*/
|
||||
private fun refreshAction() = doAsync {
|
||||
uiThread {
|
||||
private fun refreshAction() = GlobalScope.launch(Dispatchers.Default) {
|
||||
withContext(Dispatchers.Main) {
|
||||
// set the refresh listener
|
||||
refreshLayout_Mensa.setOnRefreshListener {
|
||||
updateMensaScreen()
|
||||
@ -118,23 +128,29 @@ class MensaFragment : Fragment() {
|
||||
/**
|
||||
* refresh the mensa cache and update the mensa screen
|
||||
*/
|
||||
private fun updateMensaScreen() = doAsync {
|
||||
// update the cache
|
||||
TCoRAPIController.getMensa(context!!).get() // blocking since we want the new data
|
||||
CacheController.readMensa(context!!)
|
||||
private fun updateMensaScreen() = GlobalScope.launch(Dispatchers.Default) {
|
||||
CacheController.updateMensaMenu(context!!).join() // blocking since we want the new data
|
||||
|
||||
uiThread {
|
||||
withContext(Dispatchers.Main) {
|
||||
// remove all menus from the layout
|
||||
linLayout_Mensa.removeAllViews()
|
||||
|
||||
// add the refreshed menus
|
||||
val dayCurrent = if (NotRetardedCalendar().getDayOfWeekIndex() == 6) 0 else NotRetardedCalendar().getDayOfWeekIndex()
|
||||
addWeek(mensaCurrentWeek, dayCurrent).get()
|
||||
val dayCurrent = if (NotRetardedCalendar.getDayOfWeekIndex() == 6) 0 else NotRetardedCalendar.getDayOfWeekIndex()
|
||||
addWeek(mensaMenu.currentWeek, dayCurrent).join()
|
||||
|
||||
// add the next week
|
||||
addWeek(mensaNextWeek, 0)
|
||||
addWeek(mensaMenu.nextWeek, 0)
|
||||
|
||||
refreshLayout_Mensa.isRefreshing = false
|
||||
|
||||
// show a info if there are no more menus
|
||||
if (linLayout_Mensa.childCount == 0) {
|
||||
val txtViewInfo = TextViewInfo(context!!).set {
|
||||
txt = resources.getString(R.string.no_more_meals)
|
||||
}
|
||||
linLayout_Mensa.addView(txtViewInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
@ -42,8 +42,11 @@ class MoodleFragment : Fragment() {
|
||||
private lateinit var webSettings: WebSettings
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_moodle, container, false)
|
||||
}
|
||||
|
||||
val view: View = inflater.inflate(R.layout.fragment_moodle, container, false)
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
webView = view.findViewById(R.id.webView)
|
||||
webView.loadUrl("https://elearning.hs-offenburg.de/moodle/")
|
||||
@ -52,7 +55,5 @@ class MoodleFragment : Fragment() {
|
||||
//webSettings.setJavaScriptEnabled(true) // Enable Javascript
|
||||
|
||||
webView.webViewClient = WebViewClient() // Force links and redirects to open in the WebView instead of in a browser
|
||||
|
||||
return view
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
@ -24,31 +24,40 @@ package org.mosad.seil0.projectlaogai.fragments
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Switch
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.widget.SwitchCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.afollestad.aesthetic.Aesthetic
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.WhichButton
|
||||
import com.afollestad.materialdialogs.actions.getActionButton
|
||||
import com.afollestad.materialdialogs.callbacks.onDismiss
|
||||
import com.afollestad.materialdialogs.color.colorChooser
|
||||
import com.afollestad.materialdialogs.customview.customView
|
||||
import com.afollestad.materialdialogs.list.listItems
|
||||
import com.afollestad.materialdialogs.list.listItemsMultiChoice
|
||||
import com.afollestad.materialdialogs.list.listItemsSingleChoice
|
||||
import de.psdev.licensesdialog.LicensesDialog
|
||||
import kotlinx.android.synthetic.main.fragment_settings.*
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
import kotlinx.coroutines.*
|
||||
import org.mosad.seil0.projectlaogai.BuildConfig
|
||||
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.PreferencesController.Companion.cCourse
|
||||
import org.mosad.seil0.projectlaogai.controller.PreferencesController.Companion.cShowBuffet
|
||||
import org.mosad.seil0.projectlaogai.R
|
||||
import org.mosad.seil0.projectlaogai.controller.CacheController
|
||||
import org.mosad.seil0.projectlaogai.controller.CacheController.Companion.coursesList
|
||||
import org.mosad.seil0.projectlaogai.controller.TCoRAPIController
|
||||
import org.mosad.seil0.projectlaogai.hsoparser.DataTypes
|
||||
import org.mosad.seil0.projectlaogai.controller.cache.CacheController
|
||||
import org.mosad.seil0.projectlaogai.controller.cache.CacheController.Companion.coursesList
|
||||
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences
|
||||
import org.mosad.seil0.projectlaogai.controller.cache.TimetableController
|
||||
import org.mosad.seil0.projectlaogai.controller.preferences.EncryptedPreferences
|
||||
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.cColorAccent
|
||||
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.cColorPrimary
|
||||
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.cCourse
|
||||
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.cShowBuffet
|
||||
import org.mosad.seil0.projectlaogai.uicomponents.dialogs.LoginDialog
|
||||
import org.mosad.seil0.projectlaogai.util.DataTypes
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@ -59,34 +68,68 @@ class SettingsFragment : Fragment() {
|
||||
|
||||
private lateinit var linLayoutUser: LinearLayout
|
||||
private lateinit var linLayoutCourse: LinearLayout
|
||||
private lateinit var linLayoutManageLessons: LinearLayout
|
||||
private lateinit var linLayoutAbout: LinearLayout
|
||||
private lateinit var linLayoutLicence: LinearLayout
|
||||
private lateinit var linLayoutTheme: LinearLayout
|
||||
private lateinit var linLayoutPrimaryColor: LinearLayout
|
||||
private lateinit var linLayoutAccentColor: LinearLayout
|
||||
private lateinit var switchBuffet: Switch
|
||||
private lateinit var switchBuffet: SwitchCompat
|
||||
private lateinit var txtViewCourse: TextView
|
||||
|
||||
private var selectedTheme = DataTypes.Theme.Light
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_settings, container, false)
|
||||
}
|
||||
|
||||
val view: View = inflater.inflate(R.layout.fragment_settings, container, false)
|
||||
/**
|
||||
* initialize the settings gui
|
||||
*/
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
linLayoutUser = view.findViewById(R.id.linLayout_User)
|
||||
linLayoutCourse = view.findViewById(R.id.linLayout_Course)
|
||||
linLayoutManageLessons = view.findViewById(R.id.linLayout_ManageLessons)
|
||||
linLayoutAbout = view.findViewById(R.id.linLayout_About)
|
||||
linLayoutLicence = view.findViewById(R.id.linLayout_Licence)
|
||||
linLayoutTheme = view.findViewById(R.id.linLayout_Theme)
|
||||
linLayoutPrimaryColor = view.findViewById(R.id.linLayout_PrimaryColor)
|
||||
linLayoutAccentColor = view.findViewById(R.id.linLayout_AccentColor)
|
||||
switchBuffet = view.findViewById(R.id.switch_buffet)
|
||||
|
||||
// if we call txtView_Course via KAE view binding it'll result in a NPE in the onDismissed call
|
||||
txtViewCourse = view.findViewById(R.id.txtView_Course)
|
||||
|
||||
initActions()
|
||||
|
||||
// Inflate the layout for this fragment
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
txtView_User.text = EncryptedPreferences.email.ifEmpty { resources.getString(R.string.sample_user) }
|
||||
txtView_Course.text = cCourse.courseName
|
||||
txtView_AboutDesc.text = resources.getString(R.string.about_version, BuildConfig.VERSION_NAME, getString(R.string.build_time))
|
||||
switch_buffet.isChecked = cShowBuffet // init switch
|
||||
|
||||
val outValue = TypedValue()
|
||||
activity!!.theme.resolveAttribute(R.attr.themeName, outValue, true)
|
||||
when(outValue.string) {
|
||||
"light" -> {
|
||||
switch_buffet.setTextColor(activity!!.resources.getColor(R.color.textPrimaryLight, activity!!.theme))
|
||||
selectedTheme = DataTypes.Theme.Light
|
||||
selectedTheme.string = resources.getString(R.string.themeLight)
|
||||
}
|
||||
"dark" -> {
|
||||
switch_buffet.setTextColor(activity!!.resources.getColor(R.color.textPrimaryDark, activity!!.theme))
|
||||
selectedTheme = DataTypes.Theme.Dark
|
||||
selectedTheme.string = resources.getString(R.string.themeDark)
|
||||
}
|
||||
"black" -> {
|
||||
switch_buffet.setTextColor(activity!!.resources.getColor(R.color.textPrimaryDark, activity!!.theme))
|
||||
selectedTheme = DataTypes.Theme.Black
|
||||
selectedTheme.string = resources.getString(R.string.themeBlack)
|
||||
}
|
||||
}
|
||||
txtView_SelectedTheme.text = selectedTheme.string
|
||||
}
|
||||
|
||||
/**
|
||||
@ -95,11 +138,51 @@ class SettingsFragment : Fragment() {
|
||||
private fun initActions() {
|
||||
linLayoutUser.setOnClickListener {
|
||||
// open a new dialog
|
||||
LoginDialog(context!!)
|
||||
.positiveButton {
|
||||
EncryptedPreferences.saveCredentials(email, password, context)
|
||||
}
|
||||
.show {
|
||||
email = EncryptedPreferences.email
|
||||
password = ""
|
||||
}
|
||||
}
|
||||
|
||||
linLayoutUser.setOnLongClickListener {
|
||||
Preferences.oGiants = true // enable easter egg
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
|
||||
linLayoutCourse.setOnClickListener {
|
||||
selectCourse(context!!)
|
||||
txtView_Course.text = cCourse.courseName // update txtView
|
||||
selectCourse(context!!).show {
|
||||
onDismiss {
|
||||
txtViewCourse.text = cCourse.courseName // update txtView after the dialog is dismissed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
linLayoutManageLessons.setOnClickListener {
|
||||
val lessons = ArrayList<String>()
|
||||
TimetableController.subjectMap.forEach { pair ->
|
||||
pair.value.forEach {
|
||||
lessons.add("${pair.key} - $it")
|
||||
}
|
||||
}
|
||||
|
||||
MaterialDialog(context!!).show {
|
||||
title(R.string.manage_lessons)
|
||||
positiveButton(R.string.delete)
|
||||
negativeButton(R.string.cancel)
|
||||
getActionButton(WhichButton.POSITIVE).updateTextColor(cColorAccent)
|
||||
getActionButton(WhichButton.NEGATIVE).updateTextColor(cColorAccent)
|
||||
|
||||
listItemsMultiChoice(items = lessons) { _, _, items ->
|
||||
items.forEach {
|
||||
val list = it.split(" - ")
|
||||
TimetableController.removeSubject(list[0], list[1], context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
linLayoutAbout.setOnClickListener {
|
||||
@ -110,10 +193,58 @@ class SettingsFragment : Fragment() {
|
||||
.show()
|
||||
}
|
||||
|
||||
linLayoutLicence.setOnClickListener {
|
||||
// do the theme magic, as the lib's theme support is broken
|
||||
val outValue = TypedValue()
|
||||
context!!.theme.resolveAttribute(R.attr.themeName, outValue, true)
|
||||
|
||||
val dialogCss = when (outValue.string) {
|
||||
"light" -> R.string.license_dialog_style_light
|
||||
else -> R.string.license_dialog_style_dark
|
||||
}
|
||||
|
||||
val themeId = when (outValue.string) {
|
||||
"light" -> R.style.AppTheme_Light
|
||||
else -> R.style.LicensesDialogTheme_Dark
|
||||
}
|
||||
|
||||
// open a new license dialog
|
||||
LicensesDialog.Builder(context!!)
|
||||
.setNotices(R.raw.notices)
|
||||
.setTitle(R.string.licenses)
|
||||
.setIncludeOwnLicense(true)
|
||||
.setThemeResourceId(themeId)
|
||||
.setNoticesCssStyle(dialogCss)
|
||||
.build()
|
||||
.show()
|
||||
}
|
||||
|
||||
linLayoutTheme.setOnClickListener {
|
||||
val themes = listOf(
|
||||
resources.getString(R.string.themeLight),
|
||||
resources.getString(R.string.themeDark),
|
||||
resources.getString(R.string.themeBlack)
|
||||
)
|
||||
|
||||
MaterialDialog(context!!).show {
|
||||
title(R.string.theme)
|
||||
listItemsSingleChoice(items = themes, initialSelection = selectedTheme.ordinal) { _, index, _ ->
|
||||
Aesthetic.config {
|
||||
when (index) {
|
||||
0 -> activityTheme(R.style.AppTheme_Light)
|
||||
1 -> activityTheme(R.style.AppTheme_Dark)
|
||||
2 -> activityTheme(R.style.AppTheme_Black)
|
||||
else -> activityTheme(R.style.AppTheme_Light)
|
||||
}
|
||||
apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
linLayoutPrimaryColor.setOnClickListener {
|
||||
// open a new color chooser dialog
|
||||
MaterialDialog(context!!)
|
||||
.title(R.string.primary_color)
|
||||
.colorChooser(DataTypes().primaryColors, allowCustomArgb = true, initialSelection = cColorPrimary) { _, color ->
|
||||
view_PrimaryColor.setBackgroundColor(color)
|
||||
Aesthetic.config {
|
||||
@ -121,18 +252,19 @@ class SettingsFragment : Fragment() {
|
||||
colorPrimaryDark(color)
|
||||
apply()
|
||||
}
|
||||
|
||||
cColorPrimary = color
|
||||
PreferencesController.save(context!!)
|
||||
Preferences.saveColorPrimary(context!!, color)
|
||||
}
|
||||
.positiveButton(R.string.select)
|
||||
.show()
|
||||
.show {
|
||||
title(R.string.primary_color)
|
||||
positiveButton(R.string.select)
|
||||
getActionButton(WhichButton.POSITIVE).updateTextColor(cColorAccent)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
linLayoutAccentColor.setOnClickListener {
|
||||
// open a new color chooser dialog
|
||||
MaterialDialog(context!!)
|
||||
.title(R.string.accent_color)
|
||||
.colorChooser(DataTypes().accentColors, allowCustomArgb = true, initialSelection = cColorAccent) { _, color ->
|
||||
view_AccentColor.setBackgroundColor(color)
|
||||
Aesthetic.config {
|
||||
@ -140,55 +272,53 @@ class SettingsFragment : Fragment() {
|
||||
apply()
|
||||
}
|
||||
|
||||
cColorAccent = color
|
||||
PreferencesController.save(context!!)
|
||||
Preferences.saveColorAccent(context!!, color)
|
||||
}
|
||||
.show{
|
||||
title(R.string.accent_color)
|
||||
positiveButton(R.string.select)
|
||||
getActionButton(WhichButton.POSITIVE).updateTextColor(cColorAccent)
|
||||
}
|
||||
.positiveButton(R.string.select)
|
||||
.show()
|
||||
}
|
||||
|
||||
switchBuffet.setOnClickListener {
|
||||
cShowBuffet = switchBuffet.isChecked
|
||||
PreferencesController.save(context!!)
|
||||
println(switchBuffet.isChecked)
|
||||
Preferences.saveShowBuffet(context!!, switchBuffet.isChecked)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun selectCourse(context: Context) {
|
||||
fun selectCourse(context: Context) : MaterialDialog {
|
||||
val courseNameList = ArrayList<String>()
|
||||
coursesList.forEach { (_, courseName) ->
|
||||
courseNameList.add(courseName)
|
||||
}
|
||||
|
||||
// open a new dialog
|
||||
MaterialDialog(context)
|
||||
// return a new course selection dialog
|
||||
return MaterialDialog(context)
|
||||
.title(R.string.select_course)
|
||||
.listItems(items = courseNameList) { _, index, _ ->
|
||||
|
||||
val dialog = MaterialDialog(context).cancelable(false)
|
||||
val loadingDialog = MaterialDialog(context).cancelable(false)
|
||||
.cancelOnTouchOutside(false)
|
||||
.customView(R.layout.dialog_loading)
|
||||
dialog.show()
|
||||
loadingDialog.show()
|
||||
|
||||
doAsync {
|
||||
cCourse = coursesList[index] // set the course
|
||||
PreferencesController.save(context)
|
||||
GlobalScope.launch(Dispatchers.Default) {
|
||||
Preferences.saveCourse(context, coursesList[index]) // save the course
|
||||
|
||||
// update current & next weeks timetable
|
||||
TCoRAPIController.getTimetable(cCourse.courseName, 0, context).get() // blocking since we want the new data
|
||||
TCoRAPIController.getTimetable(cCourse.courseName, 1, context).get() // blocking since we want the new data
|
||||
val threads = listOf(
|
||||
CacheController.updateTimetable(cCourse.courseName, 0, context),
|
||||
CacheController.updateTimetable(cCourse.courseName, 1, context)
|
||||
)
|
||||
threads.joinAll() // blocking since we want the new data
|
||||
|
||||
CacheController.readTimetable(cCourse.courseName, 0, context)
|
||||
CacheController.readTimetable(cCourse.courseName, 1, context)
|
||||
|
||||
uiThread {
|
||||
dialog.dismiss()
|
||||
withContext(Dispatchers.Main) {
|
||||
loadingDialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
@ -26,22 +26,19 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ScrollView
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import kotlinx.android.synthetic.main.fragment_timetable.*
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
import kotlinx.coroutines.*
|
||||
import org.mosad.seil0.projectlaogai.R
|
||||
import org.mosad.seil0.projectlaogai.controller.CacheController
|
||||
import org.mosad.seil0.projectlaogai.controller.CacheController.Companion.timetables
|
||||
import org.mosad.seil0.projectlaogai.controller.PreferencesController.Companion.cCourse
|
||||
import org.mosad.seil0.projectlaogai.controller.TCoRAPIController
|
||||
import org.mosad.seil0.projectlaogai.hsoparser.DataTypes
|
||||
import org.mosad.seil0.projectlaogai.hsoparser.NotRetardedCalendar
|
||||
import org.mosad.seil0.projectlaogai.hsoparser.TimetableWeek
|
||||
import org.mosad.seil0.projectlaogai.uicomponents.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import org.mosad.seil0.projectlaogai.controller.cache.TimetableController
|
||||
import org.mosad.seil0.projectlaogai.controller.cache.TimetableController.Companion.timetable
|
||||
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences
|
||||
import org.mosad.seil0.projectlaogai.uicomponents.dialogs.AddSubjectDialog
|
||||
import org.mosad.seil0.projectlaogai.uicomponents.DayCardView
|
||||
import org.mosad.seil0.projectlaogai.uicomponents.TextViewInfo
|
||||
import org.mosad.seil0.projectlaogai.util.NotRetardedCalendar
|
||||
|
||||
/**
|
||||
* The timetable controller class
|
||||
@ -49,115 +46,102 @@ import java.util.*
|
||||
*/
|
||||
class TimeTableFragment : Fragment() {
|
||||
|
||||
private val formatter = SimpleDateFormat("E dd.MM", Locale.getDefault()) // TODO change to android call when min api is 24
|
||||
private lateinit var scrollViewTimetable: ScrollView
|
||||
private lateinit var faBtnAddSubject: FloatingActionButton
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_timetable, container, false)
|
||||
}
|
||||
|
||||
val view: View = inflater.inflate(R.layout.fragment_timetable, container, false)
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// init actions
|
||||
refreshAction()
|
||||
scrollViewTimetable = view.findViewById(R.id.scrollView_Timetable)
|
||||
faBtnAddSubject = view.findViewById(R.id.faBtnAddSubject)
|
||||
refreshLayout_Timetable.setProgressBackgroundColorSchemeColor(Preferences.themeSecondary)
|
||||
|
||||
if (timetables[0].days.isNotEmpty() && timetables[1].days.isNotEmpty()) {
|
||||
addInitWeeks()
|
||||
initActions() // init actions
|
||||
|
||||
if (timetable.size > 1 && timetable[0].days.isNotEmpty() && timetable[1].days.isNotEmpty()) {
|
||||
initTimetable()
|
||||
} else {
|
||||
MaterialDialog(context!!)
|
||||
.title(R.string.error)
|
||||
.message(R.string.timetable_error)
|
||||
.show()
|
||||
val txtViewInfo = TextViewInfo(context!!).set {
|
||||
txt = resources.getString(R.string.timetable_generic_error)
|
||||
}.showImage()
|
||||
linLayout_Timetable.addView(txtViewInfo)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* initialize the actions
|
||||
*/
|
||||
private fun initActions() = GlobalScope.launch(Dispatchers.Main) {
|
||||
|
||||
refreshLayout_Timetable.setOnRefreshListener {
|
||||
runBlocking { TimetableController.update(context!!).joinAll() }
|
||||
reloadTimetableUI()
|
||||
}
|
||||
|
||||
// show the AddLessonDialog if the ftaBtn is clicked
|
||||
faBtnAddSubject.setOnClickListener {
|
||||
AddSubjectDialog(context!!)
|
||||
.positiveButton {
|
||||
TimetableController.addSubject(selectedCourse, selectedSubject, context)
|
||||
runBlocking { reloadTimetableUI() }
|
||||
}.show()
|
||||
}
|
||||
|
||||
// hide the btnCardValue if the user is scrolling down
|
||||
scrollViewTimetable.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
|
||||
if (scrollY > oldScrollY) {
|
||||
faBtnAddSubject.hide()
|
||||
} else {
|
||||
faBtnAddSubject.show()
|
||||
}
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
/**
|
||||
* add the current and next weeks lessons
|
||||
*/
|
||||
private fun addInitWeeks() = doAsync {
|
||||
val dayIndex = NotRetardedCalendar().getDayOfWeekIndex()
|
||||
val calendar = Calendar.getInstance()
|
||||
private fun initTimetable() = GlobalScope.launch(Dispatchers.Default) {
|
||||
val currentDayIndex = NotRetardedCalendar.getDayOfWeekIndex()
|
||||
|
||||
// add current week
|
||||
addWeek(dayIndex, 5, timetables[0], calendar).get()
|
||||
|
||||
// add next week
|
||||
addWeek(0, dayIndex - 1, timetables[1], calendar)
|
||||
addTimetableWeek(currentDayIndex, 5, 0).join() // add current week
|
||||
addTimetableWeek(0, currentDayIndex - 1, 1) // add next week
|
||||
}
|
||||
|
||||
private fun addWeek(dayStart: Int, dayEnd: Int, timetable: TimetableWeek, calendar: Calendar) = doAsync {
|
||||
uiThread {
|
||||
private fun addTimetableWeek(dayBegin: Int, dayEnd: Int, week: Int) = GlobalScope.launch(Dispatchers.Main) {
|
||||
for (dayIndex in dayBegin..dayEnd) {
|
||||
val dayCardView = DayCardView(context!!)
|
||||
|
||||
for (day in dayStart..dayEnd) {
|
||||
var helpLesson = LessonLinearLayout(context)
|
||||
val dayCardView = DayCardView(context!!)
|
||||
dayCardView.setDayHeading(formatter.format(calendar.time))
|
||||
// some wired calendar magic, calculate the correct date to be shown
|
||||
// ((timetable week - current week * 7) + (dayIndex - dayIndex of current week)
|
||||
val daysToAdd = ((timetable[week].weekNumberYear - NotRetardedCalendar.getWeekOfYear())
|
||||
* 7 + (dayIndex - NotRetardedCalendar.getDayOfWeekIndex()))
|
||||
dayCardView.addTimetableDay(timetable[week].days[dayIndex], daysToAdd)
|
||||
|
||||
// for each timeslot of the day
|
||||
for ((tsIndex, timeslot) in timetable.days[day].timeslots.withIndex()) {
|
||||
// if there are no lessons don't show the dayCardView
|
||||
if (dayCardView.getLinLayoutDay().childCount > 1)
|
||||
linLayout_Timetable.addView(dayCardView)
|
||||
|
||||
for(lesson in timeslot) {
|
||||
|
||||
if(lesson.lessonSubject.isNotEmpty()) {
|
||||
|
||||
val lessonLayout = LessonLinearLayout(context)
|
||||
lessonLayout.setLesson(lesson, DataTypes().getTime()[tsIndex])
|
||||
dayCardView.getLinLayoutDay().addView(lessonLayout)
|
||||
|
||||
if (lesson != timeslot.last())
|
||||
lessonLayout.disableDivider()
|
||||
|
||||
helpLesson = lessonLayout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
helpLesson.disableDivider()
|
||||
calendar.add(Calendar.DATE, 1)
|
||||
|
||||
// if there are no lessons don't show the dayCardView
|
||||
if (dayCardView.getLinLayoutDay().childCount > 1)
|
||||
linLayout_Timetable.addView(dayCardView)
|
||||
|
||||
}
|
||||
|
||||
calendar.add(Calendar.DATE, 1) // before this we are at a sunday (no lecture on sundays!)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* initialize the refresh action
|
||||
* clear linLayout_Timetable, add the updated timetable
|
||||
*/
|
||||
private fun refreshAction() = doAsync {
|
||||
uiThread {
|
||||
// set the refresh listener
|
||||
refreshLayout_Timetable.setOnRefreshListener {
|
||||
updateTimetableScreen()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTimetableScreen() = doAsync {
|
||||
// update the cache
|
||||
TCoRAPIController.getTimetable(cCourse.courseName, 0, context!!).get() // blocking since we want the new data
|
||||
TCoRAPIController.getTimetable(cCourse.courseName, 1, context!!).get() // blocking since we want the new data
|
||||
|
||||
CacheController.readTimetable(cCourse.courseName, 0, context!!)
|
||||
CacheController.readTimetable(cCourse.courseName, 1, context!!)
|
||||
|
||||
uiThread {
|
||||
// remove all menus from the layout
|
||||
private fun reloadTimetableUI() = GlobalScope.launch(Dispatchers.Default) {
|
||||
withContext(Dispatchers.Main) {
|
||||
// remove all lessons from the layout
|
||||
linLayout_Timetable.removeAllViews()
|
||||
|
||||
// add the refreshed timetables
|
||||
val dayIndex = NotRetardedCalendar().getDayOfWeekIndex()
|
||||
val calendar = Calendar.getInstance()
|
||||
val dayIndex = NotRetardedCalendar.getDayOfWeekIndex()
|
||||
|
||||
// add current week
|
||||
addWeek(dayIndex, 5, timetables[0], calendar).get()
|
||||
|
||||
// add next week
|
||||
addWeek(0, dayIndex - 1, timetables[1], calendar)
|
||||
addTimetableWeek(dayIndex, 5, 0).join() // add current week
|
||||
addTimetableWeek(0, dayIndex - 1, 1) // add next week
|
||||
|
||||
refreshLayout_Timetable.isRefreshing = false
|
||||
}
|
||||
|
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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.onboarding
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.viewpager.widget.PagerAdapter
|
||||
import org.mosad.seil0.projectlaogai.OnboardingActivity.Companion.layouts
|
||||
|
||||
/**
|
||||
* a PagerAdapter
|
||||
*/
|
||||
class ViewPagerAdapter(cont: Context) : PagerAdapter() {
|
||||
|
||||
private val context = cont
|
||||
|
||||
override fun isViewFromObject(view: View, `object`: Any): Boolean {
|
||||
return view === `object`
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
return layouts.size
|
||||
}
|
||||
|
||||
override fun instantiateItem(container: ViewGroup, position: Int): Any {
|
||||
val view = LayoutInflater.from(context).inflate(layouts[position], container, false)
|
||||
container.addView(view)
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
|
||||
val view = `object` as View
|
||||
container.removeView(view)
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
@ -28,9 +28,15 @@ import android.widget.LinearLayout
|
||||
import androidx.cardview.widget.CardView
|
||||
import kotlinx.android.synthetic.main.cardview_day.view.*
|
||||
import org.mosad.seil0.projectlaogai.R
|
||||
import org.mosad.seil0.projectlaogai.util.DataTypes
|
||||
import org.mosad.seil0.projectlaogai.util.TimetableDay
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class DayCardView(context: Context) : CardView(context) {
|
||||
|
||||
private val formatter = SimpleDateFormat("E dd.MM", Locale.getDefault())
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.cardview_day,this)
|
||||
|
||||
@ -46,4 +52,38 @@ class DayCardView(context: Context) : CardView(context) {
|
||||
txtView_DayHeading.text = heading
|
||||
}
|
||||
|
||||
/**
|
||||
* add the lessons of one day to the dayCardView
|
||||
* @param timetable a timetable containing the day (and it's lessons) to be added
|
||||
*/
|
||||
fun addTimetableDay(timetable: TimetableDay, daysToAdd: Int) {
|
||||
var lastLesson = LessonLinearLayout(context)
|
||||
|
||||
// set the heading
|
||||
val cal = Calendar.getInstance()
|
||||
cal.add(Calendar.DATE, daysToAdd)
|
||||
txtView_DayHeading.text = formatter.format(cal.time)
|
||||
|
||||
// for every timeslot of that timetable
|
||||
timetable.timeslots.forEachIndexed { tsIndex, timeslot ->
|
||||
timeslot.forEach { lesson ->
|
||||
if (lesson.lessonSubject.isNotEmpty()) {
|
||||
|
||||
val lessonLayout = LessonLinearLayout(context)
|
||||
lessonLayout.setLesson(lesson, DataTypes().times[tsIndex])
|
||||
linLayout_Day.addView(lessonLayout)
|
||||
|
||||
if (lesson != timeslot.last()) {
|
||||
lessonLayout.disableDivider()
|
||||
}
|
||||
|
||||
lastLesson = lessonLayout
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
lastLesson.disableDivider() // disable the divider for the last lesson of the day
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 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.uicomponents
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import kotlinx.android.synthetic.main.linearlayout_grade.view.*
|
||||
import org.mosad.seil0.projectlaogai.R
|
||||
|
||||
class GradeLinearLayout(context: Context?): LinearLayout(context) {
|
||||
|
||||
var subjectName = ""
|
||||
var grade = ""
|
||||
var subSubjectName = ""
|
||||
var subGrade = ""
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.linearlayout_grade, this)
|
||||
}
|
||||
|
||||
fun set(func: GradeLinearLayout.() -> Unit): GradeLinearLayout = apply {
|
||||
func()
|
||||
|
||||
txtView_subject.text = subjectName
|
||||
txtView_grade.text = grade
|
||||
txtView_sub_subject.text = subSubjectName
|
||||
txtView_sub_grade.text = subGrade
|
||||
}
|
||||
|
||||
fun disableDivider() {
|
||||
divider_grade.visibility = View.GONE
|
||||
}
|
||||
|
||||
fun disableSubSubject() {
|
||||
linLayout_sub_subject.visibility = View.GONE
|
||||
}
|
||||
}
|
@ -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
|
||||
@ -25,15 +25,14 @@ package org.mosad.seil0.projectlaogai.uicomponents
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import androidx.cardview.widget.CardView
|
||||
import kotlinx.android.synthetic.main.linearlayout_lesson.view.*
|
||||
import org.mosad.seil0.projectlaogai.R
|
||||
import org.mosad.seil0.projectlaogai.hsoparser.Lesson
|
||||
import org.mosad.seil0.projectlaogai.util.Lesson
|
||||
|
||||
class LessonLinearLayout(context: Context?) : LinearLayout(context) {
|
||||
|
||||
init {
|
||||
CardView.inflate(context, R.layout.linearlayout_lesson, this)
|
||||
inflate(context, R.layout.linearlayout_lesson, this)
|
||||
}
|
||||
|
||||
fun setLesson(lesson: Lesson, time: String) {
|
||||
|
@ -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
|
||||
@ -25,15 +25,14 @@ package org.mosad.seil0.projectlaogai.uicomponents
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import androidx.cardview.widget.CardView
|
||||
import kotlinx.android.synthetic.main.linearlayout_meal.view.*
|
||||
import org.mosad.seil0.projectlaogai.R
|
||||
import org.mosad.seil0.projectlaogai.hsoparser.Meal
|
||||
import org.mosad.seil0.projectlaogai.util.Meal
|
||||
|
||||
class MealLinearLayout(context: Context?): LinearLayout(context) {
|
||||
|
||||
init {
|
||||
CardView.inflate(context, R.layout.linearlayout_meal, this)
|
||||
inflate(context, R.layout.linearlayout_meal, this)
|
||||
}
|
||||
|
||||
fun setMeal(meal: Meal) {
|
||||
|
@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 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.uicomponents
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import org.mosad.seil0.projectlaogai.R
|
||||
|
||||
|
||||
class TextViewInfo(context: Context?): AppCompatTextView(context!!) {
|
||||
|
||||
var txt = ""
|
||||
var txtSize = 15F
|
||||
var txtAlignment = View.TEXT_ALIGNMENT_CENTER
|
||||
var drawable = R.drawable.ic_error_outline_black_24dp
|
||||
var params = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
|
||||
|
||||
init {
|
||||
params.setMargins(0,200,0,0)
|
||||
|
||||
}
|
||||
|
||||
fun set(func: TextViewInfo.() -> Unit): TextViewInfo = apply {
|
||||
func()
|
||||
|
||||
text = txt
|
||||
layoutParams = params
|
||||
textSize = txtSize
|
||||
textAlignment = txtAlignment
|
||||
}
|
||||
|
||||
fun showImage(): TextViewInfo = apply {
|
||||
val img = ResourcesCompat.getDrawable(resources, drawable, context.theme)?.apply {
|
||||
bounds = Rect(0, 0, 75, 75)
|
||||
}
|
||||
setCompoundDrawables(null, null, null, img)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,164 @@
|
||||
/**
|
||||
* 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.uicomponents.dialogs
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Spinner
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.WhichButton
|
||||
import com.afollestad.materialdialogs.actions.getActionButton
|
||||
import com.afollestad.materialdialogs.bottomsheets.BottomSheet
|
||||
import com.afollestad.materialdialogs.bottomsheets.setPeekHeight
|
||||
import com.afollestad.materialdialogs.customview.customView
|
||||
import com.afollestad.materialdialogs.customview.getCustomView
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.mosad.seil0.projectlaogai.R
|
||||
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences
|
||||
import org.mosad.seil0.projectlaogai.controller.cache.CacheController
|
||||
import org.mosad.seil0.projectlaogai.controller.TCoRAPIController
|
||||
import org.mosad.seil0.projectlaogai.util.Course
|
||||
import java.util.stream.Collectors
|
||||
|
||||
/**
|
||||
* This class creates a new AddLessonDialog.
|
||||
*/
|
||||
class AddSubjectDialog(val context: Context) {
|
||||
|
||||
private val dialog = MaterialDialog(context, BottomSheet())
|
||||
|
||||
private val spinnerCourses: Spinner
|
||||
private val spinnerSubjects: Spinner
|
||||
|
||||
private val subjectsList = ArrayList<String>()
|
||||
private val courseNamesList = getCourseNames()
|
||||
|
||||
var selectedCourse = ""
|
||||
var selectedSubject = ""
|
||||
|
||||
init {
|
||||
dialog.title(R.string.add_lesson)
|
||||
.message(R.string.add_lesson_desc)
|
||||
.customView(R.layout.dialog_add_lesson)
|
||||
.positiveButton(R.string.add)
|
||||
.negativeButton(R.string.cancel)
|
||||
.setPeekHeight(900)
|
||||
|
||||
spinnerCourses = dialog.getCustomView().findViewById(R.id.spinner_Courses)
|
||||
spinnerSubjects = dialog.getCustomView().findViewById(R.id.spinner_Lessons)
|
||||
|
||||
// fix not working accent color
|
||||
dialog.getActionButton(WhichButton.POSITIVE).updateTextColor(Preferences.cColorAccent)
|
||||
dialog.getActionButton(WhichButton.NEGATIVE).updateTextColor(Preferences.cColorAccent)
|
||||
|
||||
initSpinners()
|
||||
}
|
||||
|
||||
fun positiveButton(func: AddSubjectDialog.() -> Unit): AddSubjectDialog = apply {
|
||||
dialog.positiveButton {
|
||||
func()
|
||||
}
|
||||
}
|
||||
|
||||
fun show() {
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
fun show(func: AddSubjectDialog.() -> Unit): AddSubjectDialog = apply {
|
||||
func()
|
||||
this.show()
|
||||
}
|
||||
|
||||
private fun initSpinners() {
|
||||
setArrayAdapter(spinnerCourses, courseNamesList)
|
||||
val lessonsAdapter = setArrayAdapter(spinnerSubjects, subjectsList)
|
||||
|
||||
// don't call onItemSelected() on spinnerCourses.onItemSelectedListener
|
||||
spinnerCourses.setSelection(0,false)
|
||||
spinnerCourses.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>, view: View, pos: Int, id: Long) {
|
||||
selectedCourse = parent.getItemAtPosition(pos).toString()
|
||||
|
||||
// TODO show loading dialog while loading
|
||||
val lessonSubjects = runBlocking {
|
||||
TCoRAPIController.getSubjectListAsync(parent.getItemAtPosition(pos).toString(), 0).await()
|
||||
}
|
||||
|
||||
lessonsAdapter.clear()
|
||||
lessonsAdapter.addAll(lessonSubjects)
|
||||
lessonsAdapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>) {
|
||||
// Another interface callback
|
||||
}
|
||||
}
|
||||
|
||||
// don't call onItemSelected() on spinnerCourses.onItemSelectedListener
|
||||
spinnerSubjects.setSelection(0,false)
|
||||
spinnerSubjects.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>, view: View, pos: Int, id: Long) {
|
||||
selectedSubject = parent.getItemAtPosition(pos).toString()
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>) {
|
||||
// Another interface callback
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* set a new ArrayAdapter for a spinner with a list
|
||||
* @param spinner the spinner you wish to set the adapter for
|
||||
* @param list the list to set the adapter to
|
||||
*/
|
||||
private fun setArrayAdapter(spinner: Spinner, list: List<String>): ArrayAdapter<String> {
|
||||
return ArrayAdapter(context, android.R.layout.simple_spinner_item, list)
|
||||
.also { adapter ->
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
spinner.adapter = adapter
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get all course names from coursesList
|
||||
* @return a list, containing all course names
|
||||
*/
|
||||
private fun getCourseNames(): List<String> {
|
||||
val coursesNameList: List<String>
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
coursesNameList = CacheController.coursesList.stream().map(Course::courseName).collect(
|
||||
Collectors.toList())
|
||||
} else {
|
||||
coursesNameList = ArrayList()
|
||||
CacheController.coursesList.forEach { course ->
|
||||
coursesNameList.add(course.courseName)
|
||||
}
|
||||
}
|
||||
|
||||
return coursesNameList
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 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.uicomponents.dialogs
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.EditText
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.WhichButton
|
||||
import com.afollestad.materialdialogs.actions.getActionButton
|
||||
import com.afollestad.materialdialogs.bottomsheets.BottomSheet
|
||||
import com.afollestad.materialdialogs.bottomsheets.setPeekHeight
|
||||
import com.afollestad.materialdialogs.customview.customView
|
||||
import com.afollestad.materialdialogs.customview.getCustomView
|
||||
import org.mosad.seil0.projectlaogai.R
|
||||
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences
|
||||
|
||||
class LoginDialog(val context: Context) {
|
||||
|
||||
private val dialog = MaterialDialog(context, BottomSheet())
|
||||
|
||||
private val editTextEmail: EditText
|
||||
private val editTextPassword: EditText
|
||||
|
||||
var email = ""
|
||||
var password = ""
|
||||
|
||||
init {
|
||||
dialog.title(R.string.login_heading)
|
||||
.message(R.string.login_desc_on)
|
||||
.customView(R.layout.dialog_login)
|
||||
.positiveButton(R.string.save)
|
||||
.negativeButton(R.string.cancel)
|
||||
.setPeekHeight(900)
|
||||
|
||||
editTextEmail = dialog.getCustomView().findViewById(R.id.editText_email)
|
||||
editTextPassword = dialog.getCustomView().findViewById(R.id.editText_password)
|
||||
|
||||
// fix not working accent color
|
||||
dialog.getActionButton(WhichButton.POSITIVE).updateTextColor(Preferences.cColorAccent)
|
||||
dialog.getActionButton(WhichButton.NEGATIVE).updateTextColor(Preferences.cColorAccent)
|
||||
}
|
||||
|
||||
fun positiveButton(func: LoginDialog.() -> Unit): LoginDialog = apply {
|
||||
dialog.positiveButton {
|
||||
email = editTextEmail.text.toString()
|
||||
password = editTextPassword.text.toString()
|
||||
|
||||
func()
|
||||
}
|
||||
}
|
||||
|
||||
fun negativeButton(func: LoginDialog.() -> Unit): LoginDialog = apply {
|
||||
dialog.negativeButton {
|
||||
func()
|
||||
}
|
||||
}
|
||||
|
||||
fun show(func: LoginDialog.() -> Unit): LoginDialog = apply {
|
||||
func()
|
||||
|
||||
editTextEmail.setText(email)
|
||||
editTextPassword.setText(password)
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
@ -20,14 +20,13 @@
|
||||
*
|
||||
*/
|
||||
|
||||
package org.mosad.seil0.projectlaogai.hsoparser
|
||||
package org.mosad.seil0.projectlaogai.util
|
||||
|
||||
import android.graphics.Color
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
class DataTypes {
|
||||
val times = arrayOf("8.00 - 9.30", "9.45 - 11.15" ,"11.35 - 13.05", "14.00 -15.30", "15.45 - 17.15", "17.30 - 19.00")
|
||||
val times = arrayOf("8.00 - 9.30", "9.45 - 11.15", "11.35 - 13.05", "14.00 -15.30", "15.45 - 17.15", "17.30 - 19.00")
|
||||
|
||||
val primaryColors = intArrayOf(
|
||||
Color.parseColor("#E53935"),
|
||||
@ -59,7 +58,7 @@ class DataTypes {
|
||||
Color.parseColor("#3F51B5"),
|
||||
Color.parseColor("#3D5AFE"),
|
||||
Color.parseColor("#2979FF"),
|
||||
Color.parseColor("#00B0FF"),
|
||||
Color.parseColor("#0096FF"),
|
||||
Color.parseColor("#00E5FF"),
|
||||
Color.parseColor("#1DE9B6"),
|
||||
Color.parseColor("#00E676"),
|
||||
@ -69,58 +68,52 @@ class DataTypes {
|
||||
Color.parseColor("#FF9100"),
|
||||
Color.parseColor("#FF3D00"),
|
||||
Color.parseColor("#000000")
|
||||
)
|
||||
)
|
||||
|
||||
init {
|
||||
// do something
|
||||
}
|
||||
|
||||
fun getTime(): Array<String> {
|
||||
return times
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class NotRetardedCalendar {
|
||||
private val calendar = Calendar.getInstance()!!
|
||||
|
||||
fun getDayOfWeekIndex(): Int {
|
||||
return when(calendar.get(Calendar.DAY_OF_WEEK)) {
|
||||
Calendar.MONDAY -> 0
|
||||
Calendar.TUESDAY -> 1
|
||||
Calendar.WEDNESDAY -> 2
|
||||
Calendar.THURSDAY -> 3
|
||||
Calendar.FRIDAY -> 4
|
||||
Calendar.SATURDAY -> 5
|
||||
Calendar.SUNDAY -> 6
|
||||
else -> 7
|
||||
}
|
||||
}
|
||||
|
||||
fun getTomorrowWeekIndex(): Int {
|
||||
return when(calendar.get(Calendar.DAY_OF_WEEK)) {
|
||||
Calendar.MONDAY -> 1
|
||||
Calendar.TUESDAY -> 2
|
||||
Calendar.WEDNESDAY -> 3
|
||||
Calendar.THURSDAY -> 4
|
||||
Calendar.FRIDAY -> 5
|
||||
Calendar.SATURDAY -> 6
|
||||
Calendar.SUNDAY -> 0
|
||||
else -> 7
|
||||
}
|
||||
enum class Theme(var string: String) {
|
||||
Light("Light"),
|
||||
Dark("Dark"),
|
||||
Black("Black")
|
||||
}
|
||||
}
|
||||
|
||||
// data classes for the course part
|
||||
data class Course(val courseLink: String, val courseName: String)
|
||||
|
||||
data class CoursesMeta(val updateTime: Long = 0, val totalCourses: Int = 0)
|
||||
|
||||
data class CoursesList(val meta: CoursesMeta = CoursesMeta(), val courses: ArrayList<Course> = ArrayList())
|
||||
|
||||
// data classes for the Mensa part
|
||||
data class Meal(val day: String, val heading: String, val parts: ArrayList<String>, val additives: String)
|
||||
|
||||
data class Meals(val meals: ArrayList<Meal>)
|
||||
|
||||
data class MensaWeek(val days: Array<Meals> = Array(7) { Meals(ArrayList()) })
|
||||
|
||||
data class Lesson(val lessonSubject: String, val lessonTeacher: String, val lessonRoom:String, val lessonRemark: String)
|
||||
data class MensaMeta(val updateTime: Long = 0, val mensaName: String = "")
|
||||
|
||||
data class TimetableDay( val timeslots: Array<ArrayList<Lesson>> = Array(6) { ArrayList<Lesson>()})
|
||||
data class MensaMenu(val meta: MensaMeta = MensaMeta(), val currentWeek: MensaWeek = MensaWeek(), val nextWeek: MensaWeek = MensaWeek())
|
||||
|
||||
data class TimetableWeek(val days: Array<TimetableDay> = Array(6) { TimetableDay() })
|
||||
// data classes for the timetable part
|
||||
data class Lesson(
|
||||
val lessonID: String,
|
||||
val lessonSubject: String,
|
||||
val lessonTeacher: String,
|
||||
val lessonRoom: String,
|
||||
val lessonRemark: String
|
||||
)
|
||||
|
||||
data class TimetableDay(val timeslots: Array<ArrayList<Lesson>> = Array(6) { ArrayList() })
|
||||
|
||||
data class TimetableWeek(val weekIndex: Int = 0, val weekNumberYear: Int = 0, val days: Array<TimetableDay> = Array(6) { TimetableDay() })
|
||||
|
||||
// data classes for the qispos part
|
||||
data class GradeSubject(val id: String = "", val name: String = "", val semester: String = "", val grade: String = "", val credits: String = "")
|
||||
|
||||
// TCoR
|
||||
data class TimetableWeekTCoR(val days: Array<TimetableDay> = Array(6) { TimetableDay() })
|
||||
|
||||
data class TimetableCourseMeta(val updateTime: Long = 0, val courseName: String = "", val weekIndex: Int = 0, val weekNumberYear: Int = 0, val link: String = "")
|
||||
|
||||
data class TimetableCourseWeek(val meta: TimetableCourseMeta = TimetableCourseMeta(), var timetable: TimetableWeekTCoR = TimetableWeekTCoR())
|
@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 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.util
|
||||
|
||||
import java.util.*
|
||||
|
||||
class NotRetardedCalendar {
|
||||
companion object {
|
||||
private val calendar = Calendar.getInstance()
|
||||
|
||||
fun getDayOfWeekIndex(): Int {
|
||||
return when (calendar.get(Calendar.DAY_OF_WEEK)) {
|
||||
Calendar.MONDAY -> 0
|
||||
Calendar.TUESDAY -> 1
|
||||
Calendar.WEDNESDAY -> 2
|
||||
Calendar.THURSDAY -> 3
|
||||
Calendar.FRIDAY -> 4
|
||||
Calendar.SATURDAY -> 5
|
||||
Calendar.SUNDAY -> 6
|
||||
else -> 7
|
||||
}
|
||||
}
|
||||
|
||||
fun getTomorrowWeekIndex(): Int {
|
||||
return when (calendar.get(Calendar.DAY_OF_WEEK)) {
|
||||
Calendar.MONDAY -> 1
|
||||
Calendar.TUESDAY -> 2
|
||||
Calendar.WEDNESDAY -> 3
|
||||
Calendar.THURSDAY -> 4
|
||||
Calendar.FRIDAY -> 5
|
||||
Calendar.SATURDAY -> 6
|
||||
Calendar.SUNDAY -> 0
|
||||
else -> 7
|
||||
}
|
||||
}
|
||||
|
||||
fun getWeekOfYear(): Int {
|
||||
return when (calendar.get(Calendar.DAY_OF_WEEK)) {
|
||||
Calendar.SUNDAY -> Calendar.getInstance().get(Calendar.WEEK_OF_YEAR) - 1
|
||||
else -> Calendar.getInstance().get(Calendar.WEEK_OF_YEAR)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item android:drawable="@color/colorPrimary"/>
|
||||
<item android:drawable="@android:color/black"/>
|
||||
|
||||
<!--TODO when minAPI is 23 use this-->
|
||||
<!--<item android:gravity="center" android:width="144dp" android:height="144dp">-->
|
||||
<item android:gravity="center">
|
||||
<item android:gravity="center" android:width="144dp" android:height="144dp">
|
||||
<bitmap
|
||||
android:gravity="fill_horizontal|fill_vertical"
|
||||
android:src="@mipmap/ic_laogai_icon_splash"/>
|
||||
android:src="@drawable/ic_splash_logo"/>
|
||||
</item>
|
||||
|
||||
</layer-list>
|
9
app/src/main/res/drawable/ic_add_black_24dp.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M11,15h2v2h-2zM11,7h2v6h-2zM11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_grading_black_24dp.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M4,7h16v2H4V7zM4,13h16v-2H4V13zM4,17h7v-2H4V17zM4,21h7v-2H4V21zM15.41,18.17L14,16.75l-1.41,1.41L15.41,21L20,16.42L18.58,15L15.41,18.17zM4,3v2h16V3H4z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z"/>
|
||||
</vector>
|
@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
|
||||
</vector>
|
@ -1,9 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:autoMirrored="true"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0">
|
||||
<path android:fillColor="#FF000000"
|
||||
android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"/>
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z" />
|
||||
</vector>
|
||||
|
BIN
app/src/main/res/drawable/ic_splash_logo.png
Normal file
After Width: | Height: | Size: 7.0 KiB |
@ -1,9 +0,0 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<gradient
|
||||
android:angle="135"
|
||||
android:centerColor="?colorPrimary"
|
||||
android:endColor="?colorPrimaryDark"
|
||||
android:startColor="?colorPrimary"
|
||||
android:type="linear"/>
|
||||
</shape>
|
@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".fragments.MensaFragment">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/refreshLayout_Mensa">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:animateLayoutChanges="false"
|
||||
android:background="@color/themePrimary"
|
||||
android:id="@+id/linLayout_Mensa"/>
|
||||
</ScrollView>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
|
||||
</FrameLayout>
|
@ -1,185 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".fragments.SettingsFragment">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
|
||||
android:background="@android:color/background_light">
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="9dp" android:id="@+id/cardView_Info" app:cardElevation="5dp"
|
||||
app:cardUseCompatPadding="true">
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<TextView
|
||||
android:text="@string/info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/txtView_Info"
|
||||
android:typeface="sans"
|
||||
android:textSize="18sp" android:textStyle="bold" android:fontFamily="sans-serif"
|
||||
android:layout_margin="7dp" android:textColor="?colorAccent"/>
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:padding="7dp" android:id="@+id/linLayout_User">
|
||||
<TextView
|
||||
android:text="@string/sample_user"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/txtView_User"
|
||||
android:textSize="16sp" android:textStyle="bold"
|
||||
android:textColor="@android:color/primary_text_light"/>
|
||||
<TextView
|
||||
android:text="@string/user"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/txtView_UserDesc"/>
|
||||
</LinearLayout>
|
||||
<View
|
||||
android:id="@+id/divider2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?android:attr/listDivider"
|
||||
/>
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:layout_margin="7dp"
|
||||
android:id="@+id/linLayout_Course">
|
||||
<TextView
|
||||
android:text="@string/sample_course"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/txtView_Course"
|
||||
android:textSize="16sp" android:textColor="@android:color/primary_text_light"
|
||||
android:textStyle="bold"/>
|
||||
<TextView
|
||||
android:text="@string/course_desc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/txtView_CourseDesc"/>
|
||||
</LinearLayout>
|
||||
<View
|
||||
android:id="@+id/divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?android:attr/listDivider"
|
||||
/>
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" android:layout_margin="7dp"
|
||||
android:id="@+id/linLayout_About">
|
||||
<TextView
|
||||
android:text="@string/about_txtView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/txtView_About"
|
||||
android:textStyle="bold" android:textSize="16sp"
|
||||
android:textColor="@android:color/primary_text_light"/>
|
||||
<TextView
|
||||
android:text="@string/about_version"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/txtView_AboutDesc"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/cardView_Settings" app:cardElevation="5dp"
|
||||
app:cardUseCompatPadding="true">
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/linLayout_Settings" android:padding="7dp">
|
||||
<TextView
|
||||
android:text="@string/settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/txtView_Settings"
|
||||
android:typeface="sans"
|
||||
android:textSize="18sp" android:textColor="?colorAccent"
|
||||
android:textStyle="bold" android:paddingTop="3dp"/>
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical|end"
|
||||
android:clickable="true" android:id="@+id/linLayout_PrimaryColor" android:focusable="true">
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
<TextView
|
||||
android:text="@string/primary_color"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/txtView_PrimaryColor"
|
||||
android:textSize="16sp" android:textStyle="bold"
|
||||
android:textColor="@android:color/primary_text_light"/>
|
||||
<TextView
|
||||
android:text="@string/primary_color_desc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/txtView_PrimaryColorDesc"/>
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:layout_margin="7dp"
|
||||
android:gravity="end">
|
||||
<View
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp" android:id="@+id/view_PrimaryColor"
|
||||
android:background="?colorPrimary"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical|end"
|
||||
android:clickable="true" android:id="@+id/linLayout_AccentColor" android:focusable="true">
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
<TextView
|
||||
android:text="@string/accent_color"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/txtView_AccentColor"
|
||||
android:textSize="16sp" android:textStyle="bold"
|
||||
android:textColor="@android:color/primary_text_light"/>
|
||||
<TextView
|
||||
android:text="@string/accent_color_desc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/txtView_AccentColorDesc"/>
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:layout_margin="7dp"
|
||||
android:gravity="end">
|
||||
<View
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp" android:id="@+id/view_AccentColor"
|
||||
android:background="?colorAccent"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
<Switch
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/switch_buffet"
|
||||
android:textSize="16sp"
|
||||
android:textColor="@android:color/primary_text_light" android:textStyle="bold"
|
||||
android:text="@string/show_buffet"/>
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</FrameLayout>
|
@ -1,25 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".fragments.TimeTableFragment">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/refreshLayout_Timetable">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:animateLayoutChanges="false"
|
||||
android:background="@color/themePrimary"
|
||||
android:id="@+id/linLayout_Timetable"/>
|
||||
</ScrollView>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</FrameLayout>
|
@ -1,35 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/linLayout_lesson" android:padding="7dp">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" android:id="@+id/txtView_lessonSubject" android:layout_weight="1"
|
||||
android:textSize="15sp" android:textColor="@color/textPrimary"/>
|
||||
<TextView
|
||||
android:text="@string/a_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" android:id="@+id/txtView_lessonTime" android:layout_weight="1"
|
||||
android:textColor="@color/textSecondary" android:gravity="end"/>
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/txtView_lessonTeacher" android:textSize="15sp"
|
||||
android:textColor="@color/textPrimary"/>
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/txtView_lessonRoom"
|
||||
android:textColor="@color/textPrimary"/>
|
||||
<View
|
||||
android:id="@+id/divider_lesson"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?android:attr/listDivider"
|
||||
/>
|
||||
</LinearLayout>
|
@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" android:id="@+id/linLayout_Meal" android:padding="7dp">
|
||||
|
||||
<TextView
|
||||
android:text="@string/meal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/txtView_MealHeading" android:textSize="18sp"
|
||||
android:textColor="@color/textPrimary" android:textAlignment="center" android:paddingBottom="5dp"
|
||||
android:textStyle="bold"/>
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/txtView_Meal" android:textSize="15sp"
|
||||
android:textColor="@color/textPrimary" android:textAlignment="center"/>
|
||||
<View
|
||||
android:id="@+id/divider_meal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?android:attr/listDivider"
|
||||
/>
|
||||
</LinearLayout>
|
@ -20,8 +20,9 @@
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
android:fitsSystemWindows="true"
|
||||
android:background="?themeSecondary"
|
||||
app:headerLayout="@layout/nav_header_main"
|
||||
app:itemTextColor="?colorAccent"
|
||||
app:menu="@menu/activity_main_drawer"/>
|
||||
|
||||
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
android:id="@+id/viewPager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" >
|
||||
|
||||
</androidx.viewpager.widget.ViewPager>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linLayout_Dots"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="30dp"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal" />
|
||||
|
||||
<View
|
||||
android:id="@+id/view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="1dp"
|
||||
android:layout_above="@id/linLayout_Dots"
|
||||
android:alpha="0.5"
|
||||
android:background="@android:color/white" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_Next"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:background="@null"
|
||||
android:onClick="btnNextClick"
|
||||
android:text="@string/next"
|
||||
android:visibility="gone" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_Skip"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:background="@null"
|
||||
android:onClick="btnSkipClick"
|
||||
android:text="@string/skip"
|
||||
tools:visibility="gone" />
|
||||
|
||||
</RelativeLayout>
|
@ -2,8 +2,10 @@
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" app:cardUseCompatPadding="true"
|
||||
app:cardElevation="5dp" app:cardBackgroundColor="@color/themeSecondary"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardUseCompatPadding="true"
|
||||
app:cardElevation="5dp"
|
||||
app:cardBackgroundColor="?themeSecondary"
|
||||
>
|
||||
|
||||
<LinearLayout
|
30
app/src/main/res/layouts/activities/layout/dialog_login.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/linLayout_login"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingEnd="24dp">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/editText_email"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="7dp"
|
||||
android:ems="10"
|
||||
android:hint="@string/email"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="textEmailAddress" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/editText_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="7dp"
|
||||
android:ems="10"
|
||||
android:hint="@string/password"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="textPassword" />
|
||||
|
||||
</LinearLayout>
|
@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/linLayout_grade"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="7dp"
|
||||
android:paddingTop="2dp"
|
||||
android:paddingRight="7dp"
|
||||
android:paddingBottom="3dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linLayout_subject"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_subject"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/sample_subject"
|
||||
android:textSize="15sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_grade"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/sample_grade"
|
||||
android:textAlignment="textEnd"
|
||||
android:textSize="15sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linLayout_sub_subject"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="7dp"
|
||||
android:paddingTop="3dp"
|
||||
android:paddingEnd="0dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_sub_subject"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/sample_sub_subject" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_sub_grade"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/sample_grade_state"
|
||||
android:textAlignment="textEnd" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/divider_grade"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="3dp"
|
||||
android:background="?dividerColor" />
|
||||
</LinearLayout>
|
@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/linLayout_lesson"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="7dp"
|
||||
android:paddingTop="2dp"
|
||||
android:paddingRight="7dp"
|
||||
android:paddingBottom="3dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_lessonSubject"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textSize="15sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_lessonTime"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="end"
|
||||
android:text="@string/a_time"
|
||||
android:textColor="@color/textSecondaryLight" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_lessonTeacher"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="15sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_lessonRoom"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<View
|
||||
android:id="@+id/divider_lesson"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:background="?dividerColor" />
|
||||
|
||||
</LinearLayout>
|
@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/linLayout_Meal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="7dp"
|
||||
android:paddingTop="2dp"
|
||||
android:paddingRight="7dp"
|
||||
android:paddingBottom="2dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_MealHeading"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:text="@string/meal"
|
||||
android:textAlignment="center"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_Meal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:textSize="15sp" />
|
||||
|
||||
<View
|
||||
android:id="@+id/divider_meal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="3dp"
|
||||
android:background="?dividerColor" />
|
||||
</LinearLayout>
|
@ -1,38 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/nav_header_main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/nav_header_height"
|
||||
android:background="@color/colorPrimary"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark"
|
||||
android:orientation="vertical"
|
||||
android:background="@android:color/black"
|
||||
android:gravity="bottom"
|
||||
android:id="@+id/nav_header_main">
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/app_name"
|
||||
android:paddingTop="@dimen/nav_header_vertical_spacing"
|
||||
app:srcCompat="@mipmap/ic_laogai_icon"
|
||||
android:contentDescription="@string/nav_header_desc"
|
||||
android:id="@+id/imageView"/>
|
||||
app:srcCompat="@mipmap/ic_laogai_icon" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_nav_header_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="@dimen/nav_header_vertical_spacing"
|
||||
android:text="@string/nav_header_title"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1" android:id="@+id/txtView_nav_header_title"/>
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
android:textColor="#FFFFFF" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/nav_header_subtitle"
|
||||
android:id="@+id/textView"/>
|
||||
android:textColor="#ffffff" />
|
||||
|
||||
</LinearLayout>
|
33
app/src/main/res/layouts/activities/values/css_styles.xml
Normal file
@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="license_dialog_style_light" translatable="false">
|
||||
body {
|
||||
background-color: #ffffff;
|
||||
color: #000000;
|
||||
font-family: sans-serif;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
pre {
|
||||
background-color: #eeeeee;
|
||||
padding: 1em;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</string>
|
||||
|
||||
<string name="license_dialog_style_dark" translatable="false">
|
||||
body {
|
||||
background-color: #303030;
|
||||
color: #ffffff;
|
||||
font-family: sans-serif;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
pre {
|
||||
background-color: #424242;
|
||||
padding: 1em;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
li a {
|
||||
color: #21a3df;
|
||||
}
|
||||
</string>
|
||||
</resources>
|
43
app/src/main/res/layouts/activities/xml/shortcuts.xml
Normal file
@ -0,0 +1,43 @@
|
||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<shortcut
|
||||
android:shortcutId="mensa"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_local_dining_black_24dp"
|
||||
android:shortcutShortLabel="@string/shortcut_mensa_short"
|
||||
android:shortcutLongLabel="@string/shortcut_mensa_long"
|
||||
android:shortcutDisabledMessage="@string/shortcut_mensa_disabled">
|
||||
<intent
|
||||
android:action="org.mosad.seil0.projectlaogai.fragments.MensaFragment"
|
||||
android:targetPackage="org.mosad.seil0.projectlaogai"
|
||||
android:targetClass="org.mosad.seil0.projectlaogai.MainActivity" />
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>
|
||||
|
||||
<shortcut
|
||||
android:shortcutId="timetable"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_baseline_calendar_today_24dp"
|
||||
android:shortcutShortLabel="@string/shortcut_timetable_short"
|
||||
android:shortcutLongLabel="@string/shortcut_timetable_long"
|
||||
android:shortcutDisabledMessage="@string/shortcut_timetable_disabled">
|
||||
<intent
|
||||
android:action="org.mosad.seil0.projectlaogai.fragments.TimeTableFragment"
|
||||
android:targetPackage="org.mosad.seil0.projectlaogai"
|
||||
android:targetClass="org.mosad.seil0.projectlaogai.MainActivity" />
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>
|
||||
|
||||
<shortcut
|
||||
android:shortcutId="moodle"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_school_black_24dp"
|
||||
android:shortcutShortLabel="@string/shortcut_moodle_short"
|
||||
android:shortcutLongLabel="@string/shortcut_moodle_long"
|
||||
android:shortcutDisabledMessage="@string/shortcut_moodle_disabled">
|
||||
<intent
|
||||
android:action="org.mosad.seil0.projectlaogai.fragments.MoodleFragment"
|
||||
android:targetPackage="org.mosad.seil0.projectlaogai"
|
||||
android:targetClass="org.mosad.seil0.projectlaogai.MainActivity" />
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>
|
||||
</shortcuts>
|
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/linLayout_addlesson"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingEnd="24dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_Course"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="7dp"
|
||||
android:text="@string/courses"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_Courses"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:entries="@array/courses"
|
||||
android:paddingBottom="10dp"
|
||||
android:popupBackground="?themeSecondary"
|
||||
android:spinnerMode="dropdown" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_Lesson"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="7dp"
|
||||
android:text="@string/lessons"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_Lessons"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:entries="@array/lessons"
|
||||
android:paddingBottom="10dp"
|
||||
android:popupBackground="?themeSecondary" />
|
||||
</LinearLayout>
|
@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/linLayout_mensacredit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_Heading"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/mensa_credit"
|
||||
android:textAlignment="center"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_current"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="7dp"
|
||||
android:layout_marginBottom="7dp"
|
||||
android:text="@string/mensa_current"
|
||||
android:textAlignment="center"
|
||||
android:textSize="20sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_last"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/mensa_last"
|
||||
android:textAlignment="center"
|
||||
android:textSize="16sp" />
|
||||
</LinearLayout>
|
@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".fragments.GradesFragment">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/refreshLayout_Grades"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?themePrimary">
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scrollView_Grades"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linLayout_Grades"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_Loading"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="100dp"
|
||||
android:padding="7dp"
|
||||
android:text="@string/loading_from_hs"
|
||||
android:textAlignment="center"
|
||||
android:textSize="15sp" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
</FrameLayout>
|
@ -3,7 +3,8 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".fragments.HomeFragment">
|
||||
tools:context=".fragments.HomeFragment"
|
||||
android:background="?themePrimary">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
29
app/src/main/res/layouts/fragments/layout/fragment_mensa.xml
Normal file
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".fragments.MensaFragment"
|
||||
android:background="?themePrimary">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/refreshLayout_Mensa"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scrollView_Mensa"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linLayout_Mensa"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:animateLayoutChanges="false"
|
||||
android:orientation="vertical" />
|
||||
</ScrollView>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
|
||||
</FrameLayout>
|
@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imgCourse"
|
||||
android:layout_width="256dp"
|
||||
android:layout_height="256dp"
|
||||
android:contentDescription="@string/timetable"
|
||||
android:scaleType="fitCenter"
|
||||
app:layout_constraintBottom_toTopOf="@+id/linearLayout2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_baseline_calendar_today_24dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linearLayout2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="60dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:padding="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_Course"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/course_heading"
|
||||
android:textAlignment="center"
|
||||
android:textSize="26sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_CourseDesc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="7dp"
|
||||
android:text="@string/course_desc_on"
|
||||
android:textAlignment="center"
|
||||
android:textSize="18sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnSelectCourse"
|
||||
style="@style/Widget.AppCompat.Button.Colored"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="20dp"
|
||||
android:onClick="btnSelectCourseClick"
|
||||
android:text="@string/select_course"
|
||||
android:textColor="#FFFFFFFF"
|
||||
android:textStyle="bold" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</LinearLayout>
|
@ -0,0 +1,93 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imgGrades"
|
||||
android:layout_width="256dp"
|
||||
android:layout_height="256dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:contentDescription="@string/app_name"
|
||||
android:scaleType="fitCenter"
|
||||
app:layout_constraintBottom_toTopOf="@+id/linearLayout"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_school_black_24dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linearLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="60dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:padding="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_Grades"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/login_heading"
|
||||
android:textAlignment="center"
|
||||
android:textSize="26sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_GradesDesc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="7dp"
|
||||
android:text="@string/login_desc_on"
|
||||
android:textAlignment="center"
|
||||
android:textSize="18sp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/editText_email"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="7dp"
|
||||
android:ems="10"
|
||||
android:hint="@string/email"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="textEmailAddress" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/editText_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="7dp"
|
||||
android:ems="10"
|
||||
android:hint="@string/password"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="textPassword" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnLogin"
|
||||
style="@style/Widget.AppCompat.Button.Colored"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="20dp"
|
||||
android:onClick="btnLoginClick"
|
||||
android:text="@string/login"
|
||||
android:textColor="#FFFFFFFF"
|
||||
android:textStyle="bold" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</LinearLayout>
|
@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/logo"
|
||||
android:layout_width="256dp"
|
||||
android:layout_height="256dp"
|
||||
android:contentDescription="@string/app_name"
|
||||
android:scaleType="fitCenter"
|
||||
app:layout_constraintBottom_toTopOf="@+id/linearLayout3"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@mipmap/ic_laogai_icon_foreground" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linearLayout3"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="60dp"
|
||||
android:orientation="vertical"
|
||||
android:padding="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtAppName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_name"
|
||||
android:textAlignment="center"
|
||||
android:textSize="26sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtWelcome"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="7dp"
|
||||
android:text="@string/welcome"
|
||||
android:textAlignment="center"
|
||||
android:textSize="18sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnGetStarted"
|
||||
style="@style/Widget.AppCompat.Button.Colored"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="20dp"
|
||||
android:onClick="btnNextClick"
|
||||
android:text="@string/get_started"
|
||||
android:textColor="#FFFFFFFF"
|
||||
android:textStyle="bold" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</LinearLayout>
|
348
app/src/main/res/layouts/fragments/layout/fragment_settings.xml
Normal file
@ -0,0 +1,348 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?themePrimary"
|
||||
tools:context=".fragments.SettingsFragment">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/cardView_Info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="9dp"
|
||||
app:cardBackgroundColor="?themeSecondary"
|
||||
app:cardElevation="5dp"
|
||||
app:cardUseCompatPadding="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_Info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="7dp"
|
||||
android:fontFamily="sans-serif"
|
||||
android:text="@string/info"
|
||||
android:textColor="?colorAccent"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:typeface="sans" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linLayout_User"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="7dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_User"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/sample_user"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_UserDesc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/user_desc" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/divider2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?dividerColor" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linLayout_Course"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="7dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_Course"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/sample_course"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_CourseDesc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/course_desc" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?dividerColor" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linLayout_ManageLessons"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="7dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_ManageLessons"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/manage_lessons"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_ManageLessonsDesc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/manage_lessons_desc" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/divider7"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?dividerColor" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linLayout_About"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="7dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_About"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/about_txtView"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_AboutDesc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/about_version" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/divider6"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?dividerColor" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linLayout_Licence"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="7dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView3"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/licenses"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/cardView_Settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardBackgroundColor="?themeSecondary"
|
||||
app:cardElevation="5dp"
|
||||
app:cardUseCompatPadding="true">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linLayout_Settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_Settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="7dp"
|
||||
android:paddingTop="3dp"
|
||||
android:text="@string/settings"
|
||||
android:textColor="?colorAccent"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:typeface="sans" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linLayout_Theme"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="7dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_Theme"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/theme"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_SelectedTheme"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/themeLight" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/divider3"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?dividerColor" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linLayout_PrimaryColor"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="7dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center_vertical|end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_PrimaryColor"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/primary_color"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_PrimaryColorDesc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/primary_color_desc" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="7dp"
|
||||
android:gravity="end"
|
||||
android:orientation="vertical">
|
||||
|
||||
<View
|
||||
android:id="@+id/view_PrimaryColor"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="?colorPrimary" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/divider4"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?dividerColor" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linLayout_AccentColor"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="7dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center_vertical|end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_AccentColor"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/accent_color"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtView_AccentColorDesc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/accent_color_desc" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="7dp"
|
||||
android:gravity="end"
|
||||
android:orientation="vertical">
|
||||
|
||||
<View
|
||||
android:id="@+id/view_AccentColor"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="?colorAccent" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/divider5"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?android:attr/listDivider" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/switch_buffet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="7dp"
|
||||
android:text="@string/show_buffet"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</FrameLayout>
|
@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".fragments.TimeTableFragment"
|
||||
android:background="?themePrimary">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/refreshLayout_Timetable">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" android:id="@+id/scrollView_Timetable">
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:animateLayoutChanges="false"
|
||||
android:id="@+id/linLayout_Timetable"/>
|
||||
</ScrollView>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/faBtnAddSubject"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|center|end"
|
||||
android:layout_marginEnd="11dp"
|
||||
android:layout_marginBottom="11dp"
|
||||
android:clickable="true"
|
||||
android:elevation="7dp"
|
||||
android:focusable="true"
|
||||
android:src="@drawable/ic_add_black_24dp"
|
||||
android:visibility="visible"
|
||||
app:backgroundTint="?colorAccent"
|
||||
app:fabSize="auto" />
|
||||
</FrameLayout>
|
@ -20,6 +20,10 @@
|
||||
android:id="@+id/nav_moodle"
|
||||
android:icon="@drawable/ic_school_black_24dp"
|
||||
android:title="@string/moodle"/>
|
||||
<item
|
||||
android:id="@+id/nav_grades"
|
||||
android:icon="@drawable/ic_grading_black_24dp"
|
||||
android:title="@string/grades" />
|
||||
<item
|
||||
android:id="@+id/nav_settings"
|
||||
android:icon="@drawable/ic_settings_black_24dp"
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_laogai_icon_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_laogai_icon_foreground"/>
|
||||
</adaptive-icon>
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.2 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_laogai_icon_foreground.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.0 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_laogai_icon_foreground.png
Normal file
After Width: | Height: | Size: 755 B |
Before Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.5 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_laogai_icon_foreground.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.0 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_laogai_icon_foreground.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_laogai_icon_foreground.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 8.2 KiB |
BIN
app/src/main/res/raw/notenverwaltung_hs_offenburg_de.crt
Normal file
51
app/src/main/res/raw/notices.xml
Normal file
@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<notices>
|
||||
<notice>
|
||||
<name>Material Dialogs</name>
|
||||
<url>https://github.com/afollestad/material-dialogs</url>
|
||||
<copyright>Copyright Aidan Follestad</copyright>
|
||||
<license>Apache Software License 2.0</license>
|
||||
</notice>
|
||||
<notice>
|
||||
<name>Aesthetic</name>
|
||||
<url>https://github.com/afollestad/aesthetic</url>
|
||||
<copyright>Copyright Aidan Follestad</copyright>
|
||||
<license>Apache Software License 2.0</license>
|
||||
</notice>
|
||||
<notice>
|
||||
<name>Gson</name>
|
||||
<url>https://github.com/google/gson</url>
|
||||
<copyright>Copyright 2008 Google Inc.</copyright>
|
||||
<license>Apache Software License 2.0</license>
|
||||
</notice>
|
||||
<notice>
|
||||
<name>Jsoup</name>
|
||||
<url>https://jsoup.org/</url>
|
||||
<copyright>Copyright 2009 - 2020 Jonathan Hedley</copyright>
|
||||
<license>MIT License</license>
|
||||
</notice>
|
||||
<notice>
|
||||
<name>kotlinx.coroutines</name>
|
||||
<url>https://github.com/Kotlin/kotlinx.coroutines</url>
|
||||
<copyright>Copyright JetBrains</copyright>
|
||||
<license>Apache Software License 2.0</license>
|
||||
</notice>
|
||||
<notice>
|
||||
<name>FareBot part for desfire cards</name>
|
||||
<url>https://github.com/codebutler/farebot</url>
|
||||
<copyright>Copyright 2011-2012, 2014, 2016 Eric Butler</copyright>
|
||||
<license>GNU General Public License 3.0</license>
|
||||
</notice>
|
||||
<notice>
|
||||
<name>AndroidX</name>
|
||||
<url>https://developer.android.com/jetpack/androidx</url>
|
||||
<copyright>Copyright 2018 The Android Open Source Project</copyright>
|
||||
<license>Apache Software License 2.0</license>
|
||||
</notice>
|
||||
<notice>
|
||||
<name>Material design icons</name>
|
||||
<url>https://github.com/google/material-design-icons</url>
|
||||
<copyright>Copyright Google Inc.</copyright>
|
||||
<license>Apache Software License 2.0</license>
|
||||
</notice>
|
||||
</notices>
|
@ -1,30 +1,100 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="navigation_drawer_close">Navigationsleiste schließen</string>
|
||||
<string name="navigation_drawer_open">Navigationsleiste öffnen</string>
|
||||
|
||||
<!-- nav-view -->
|
||||
<string name="home">Home</string>
|
||||
<string name="mensa">Mensa</string>
|
||||
<string name="timetable">Stundenplan</string>
|
||||
<string name="moodle">Moodle</string>
|
||||
<string name="grades">Noten</string>
|
||||
<string name="settings">Einstellungen</string>
|
||||
|
||||
<!-- Onboarding -->
|
||||
<string name="skip">überspringen</string>
|
||||
<string name="next">weiter</string>
|
||||
<string name="start">fertig</string>
|
||||
<string name="welcome">Willkommen bei Project Laogai.\nBevor wir loslegen können, richte bitte die App ein.</string>
|
||||
<string name="get_started">los geht\'s</string>
|
||||
<string name="course_heading">Studiengang</string>
|
||||
<string name="course_desc_on">Wähle deinen aktuellen Studiengang.\nZusätzliche Vorlesungen können später hinzugefügt werden.</string>
|
||||
<string name="login_heading">Login</string>
|
||||
<string name="login_desc_on">Project Laogai kann auf die Notenverwaltung zugreifen. Deine Login-Daten werden verschlüsselt auf deinem Gerät gespeichert. Diese Funktion wird "as is" und ohne Garantie bereitgestellt.</string>
|
||||
<string name="email">E-Mail</string>
|
||||
<string name="password">Passwort</string>
|
||||
<string name="login">login</string>
|
||||
|
||||
<!-- fragment_home -->
|
||||
<string name="meal">Essen</string>
|
||||
<string name="today_date">Heute, %1$s</string>
|
||||
<string name="tomorrow_date">Morgen, %1$s</string>
|
||||
<string name="mensa_closed">keine Essensausgabe</string>
|
||||
<string name="no_more_food">Diese Woche keine weitere Essensausgabe</string>
|
||||
<string name="no_lesson_today">heute keine Vorlesung!</string>
|
||||
<string name="error">Fehler</string>
|
||||
<string name="timetable_error">Stundenplan konnte nicht geladen werden!</string>
|
||||
|
||||
<!-- fragment_mensa -->
|
||||
<string name="no_more_meals">Für diese und nächste Woche ist der Speiseplan leer.</string>
|
||||
|
||||
<!-- fragment_timetable -->
|
||||
<string name="add_lesson">Eine Vorlesung hinzufügen</string>
|
||||
<string name="add_lesson_desc">Füge eine Vorlesung eines anderen Studiengangs zu deinem Stundenplan hinzu.</string>
|
||||
<string name="courses">Studiengänge:</string>
|
||||
<string name="lessons">Vorlesungen:</string>
|
||||
<string name="timetable_generic_error">Beim laden des Stundenplans ist ein Fehler aufgetreten.\n</string>
|
||||
|
||||
<!-- fragment_grades -->
|
||||
<string name="loading_from_hs">Lade Daten von den Hochschul Servern.\nDas kann eine Weile dauern.</string>
|
||||
<string name="credentials_missing">Diese Funktion benötigt deine Login-Daten. Bitte logge dich über die Einstellungen ein.</string>
|
||||
<string name="qispos_unavailable">Die Notenverwaltung ist zur Zeit nicht ereichbar.\nVersuche es später noch einmal.\n</string>
|
||||
<string name="qispos_generic_error">Error.\nVersuche es später noch einmal.\n</string>
|
||||
<string name="without_guarantee">Alle Angaben ohne Gewähr.</string>
|
||||
|
||||
<!-- fragment_settings -->
|
||||
<string name="info">Info</string>
|
||||
<string name="user">Benutzer</string>
|
||||
<string name="course_desc">Tippen, um den Kurs zu ändern</string>
|
||||
<string name="primary_color">Hauptfarbe</string>
|
||||
<string name="primary_color_desc">Die Primärfarbe, Standard ist Schwarz.</string>
|
||||
<string name="accent_color">Akzentfarbe</string>
|
||||
<string name="accent_color_desc">Die Akzentfarbe, Standard ist indigo</string>
|
||||
<string name="select">auswählen</string>
|
||||
<string name="user_desc">Zum bearbeiten tippen</string>
|
||||
<string name="course_desc">Tippe um den Kurs zu ändern</string>
|
||||
<string name="manage_lessons">Bearbeite Zusätzliche Vorlesungen</string>
|
||||
<string name="manage_lessons_desc">Tippe um zusätzliche Vorlesungen zu bearbeiten</string>
|
||||
<string name="about_dialog_heading">Über</string>
|
||||
<string name="loading_timetable">lade Stundenplan …</string>
|
||||
<string name="navigation_drawer_close">Navigationsleiste schließen</string>
|
||||
<string name="navigation_drawer_open">Navigationsleiste öffnen</string>
|
||||
<string name="licenses">Lizenzen</string>
|
||||
<string name="theme">Design</string>
|
||||
<string name="themeLight">Hell</string>
|
||||
<string name="themeDark">Dunkel</string>
|
||||
<string name="themeBlack">Schwarz</string>
|
||||
<string name="primary_color">Primärfarbe</string>
|
||||
<string name="primary_color_desc">Zum Ändern tippen, Standard ist Blaugrün.</string>
|
||||
<string name="accent_color">Akzentfarbe</string>
|
||||
<string name="accent_color_desc">Zum Ändern tippen, Standard ist Hellblau.</string>
|
||||
<string name="show_buffet">Buffet immer anzeigen</string>
|
||||
<string name="select_course">Wähle deinen Studiengang aus</string>
|
||||
|
||||
<!-- dialogs -->
|
||||
<string name="select_course">Wähle deinen Studiengang</string>
|
||||
<string name="loading_timetable">lade Stundenplan …</string>
|
||||
<string name="add">hinzufügen</string>
|
||||
<string name="delete">löschen</string>
|
||||
<string name="cancel">abbrechen</string>
|
||||
<string name="select">auswählen</string>
|
||||
<string name="save">speichern</string>
|
||||
<string name="mensa_credit">Mensa-Guthaben</string>
|
||||
<string name="mensa_current">aktuell: %1$s\n</string>
|
||||
<string name="mensa_last">letzte Abbuchung: %1$s</string>
|
||||
|
||||
<!-- errors -->
|
||||
<string name="error">Fehler</string>
|
||||
<string name="timetable_error">Der Stundenplan konnte nicht geladen werden.</string>
|
||||
|
||||
<!-- shortcuts -->
|
||||
<string name="shortcut_mensa_short">Mensa</string>
|
||||
<string name="shortcut_mensa_long">Mensa</string>
|
||||
<string name="shortcut_mensa_disabled">Mensa deaktiviert</string>
|
||||
|
||||
<string name="shortcut_timetable_short">Stundenplan</string>
|
||||
<string name="shortcut_timetable_long">Stundenplan</string>
|
||||
<string name="shortcut_timetable_disabled">Stundenplan deaktiviert</string>
|
||||
|
||||
<string name="shortcut_moodle_short">Moodle</string>
|
||||
<string name="shortcut_moodle_long">Moodle</string>
|
||||
<string name="shortcut_moodle_disabled">Moodle deaktiviert</string>
|
||||
|
||||
</resources>
|
||||
|