diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..cfb7155 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,9 @@ +kind: pipeline +name: default + +steps: +- name: assembleRelease + image: gradle:jdk8 + commands: + - gradle assembleRelease + diff --git a/README.md b/README.md index d4fdedf..b72a3f5 100644 --- a/README.md +++ b/README.md @@ -4,18 +4,19 @@ # ProjectLaogai "hso App" ProjectLaogai is a app to access the timetable and the mensa menu of Hochschule Offenburg. -[](https://f-droid.org/packages/org.mosad.seil0.projectlaogai/) +[](https://f-droid.org/packages/org.mosad.seil0.projectlaogai/) ## Features * check out the mensa menu of this and next week * access your timetable +* check the current balance of your mensa card * open moodle * probably some funny bugs ## Screenshots -[](https://github.com/Seil0/Seil0.github.io/blob/master/images/Project_Laogai/ProjectLaogai_HomeScreen.png) -[](https://github.com/Seil0/Seil0.github.io/blob/master/images/Project_Laogai/ProjectLaogai_Mensa.png) -[](https://github.com/Seil0/Seil0.github.io/blob/master/images/Project_Laogai/ProjectLaogai_Timetable.png) -[](https://github.com/Seil0/Seil0.github.io/blob/master/images/Project_Laogai/ProjectLaogai_Settings.png) -[](https://github.com/Seil0/Seil0.github.io/blob/master/images/Project_Laogai/ProjectLaogai_NavDrawer.png) +[](https://www.mosad.xyz/images/Project_Laogai/ProjectLaogai_HomeScreen.png) +[](https://www.mosad.xyz/images/Project_Laogai/ProjectLaogai_Mensa.png) +[](https://www.mosad.xyz/images/Project_Laogai/ProjectLaogai_Timetable.png) +[](https://www.mosad.xyz/images/Project_Laogai/ProjectLaogai_Settings.png) +[](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) \ No newline at end of file +ProjectLaogai © 2019 [@Seil0](https://git.mosad.xyz/Seil0), a [mosad](http://www.mosad.xyz) Project diff --git a/app/build.gradle b/app/build.gradle index 430579e..c63918f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,13 +7,13 @@ 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 14 + versionName "0.5.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "build_time", buildTime() setProperty("archivesBaseName", "projectlaogai-$versionName") @@ -25,9 +25,24 @@ android { shrinkResources false 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' + ] + } } } @@ -35,20 +50,22 @@ 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 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha5' + implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta2' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0' implementation 'com.google.android.material:material:1.0.0' implementation 'com.google.code.gson:gson:2.8.5' implementation '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.1.1' + implementation 'com.afollestad.material-dialogs:color:3.1.1' + implementation 'de.psdev.licensesdialog:licensesdialog:2.1.0' + implementation 'org.apache.commons:commons-lang3:3.9' testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' } static def buildTime() { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d81709c..2693fae 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ + + android:theme="@style/AppTheme.Light"> + @@ -26,8 +28,18 @@ + android:theme="@style/AppTheme.Light" + android:screenOrientation="portrait" + android:launchMode="singleTop"> + + + + + + + + diff --git a/app/src/main/java/com/codebutler/farebot/Utils.kt b/app/src/main/java/com/codebutler/farebot/Utils.kt new file mode 100644 index 0000000..5abf037 --- /dev/null +++ b/app/src/main/java/com/codebutler/farebot/Utils.kt @@ -0,0 +1,227 @@ +/** + * Utils.kt + * + * Copyright (C) 2011 Eric Butler + * Copyright (C) 2019 + * + * Authors: + * Eric Butler + * + * 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 . + */ + +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 findInList(list: List, matcher: Matcher): T? { + for (item in list) { + if (matcher.matches(item)) { + return item + } + } + return null + } + + interface Matcher { + 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 + } + + } + } + +} diff --git a/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireException.kt b/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireException.kt new file mode 100644 index 0000000..aa7de73 --- /dev/null +++ b/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireException.kt @@ -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) +} diff --git a/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireFile.kt b/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireFile.kt new file mode 100644 index 0000000..cb70096 --- /dev/null +++ b/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireFile.kt @@ -0,0 +1,104 @@ +/** + * DesfireFile.kt + * + * Copyright (C) 2011 Eric Butler + * Copyright (C) 2019 + * + * Authors: + * Eric Butler + * + * 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 . + */ + +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 + + init { + + val settings = fileSettings as RecordDesfireFileSettings + + val records = arrayOfNulls(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 = object : Parcelable.Creator { + override fun createFromParcel(source: Parcel): DesfireFile { + val fileId = source.readInt() + + val isError = source.readInt() == 1 + + return if (!isError) { + val fileSettings = + source.readParcelable(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 { + return arrayOfNulls(size) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireFileSettings.kt b/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireFileSettings.kt new file mode 100644 index 0000000..5e1266d --- /dev/null +++ b/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireFileSettings.kt @@ -0,0 +1,248 @@ +/** + * DesfireFileSettings.kt + * + * Copyright (C) 2011 Eric Butler + * Copyright (C) 2019 + * + * Authors: + * Eric Butler + * + * 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 . + */ + +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 = object : Parcelable.Creator { + 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 { + return arrayOfNulls(size) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireManufacturingData.kt b/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireManufacturingData.kt new file mode 100644 index 0000000..cbbbd56 --- /dev/null +++ b/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireManufacturingData.kt @@ -0,0 +1,181 @@ +/** + * DesfireManufacturingData.kt + * + * Copyright (C) 2011 Eric Butler + * Copyright (C) 2019 + * + * Authors: + * Eric Butler + * + * 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 . + */ + +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 = + object : Parcelable.Creator { + override fun createFromParcel(source: Parcel): DesfireManufacturingData { + return DesfireManufacturingData(source) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireProtocol.kt b/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireProtocol.kt new file mode 100644 index 0000000..e205734 --- /dev/null +++ b/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireProtocol.kt @@ -0,0 +1,214 @@ +/** + * DesfireProtocol.kt + * + * Copyright (C) 2011 Eric Butler + * Copyright (C) 2019 + * + * Authors: + * Eric Butler + * + * 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 . + */ + +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() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireRecord.kt b/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireRecord.kt new file mode 100644 index 0000000..b328183 --- /dev/null +++ b/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireRecord.kt @@ -0,0 +1,26 @@ +/* + * DesfireRecord.kt + * + * Copyright (C) 2011 Eric Butler + * Copyright (C) 2019 + * + * Authors: + * Eric Butler + * + * 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 . + */ + +package com.codebutler.farebot.card.desfire + +class DesfireRecord(val data: ByteArray) \ No newline at end of file diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/MainActivity.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/MainActivity.kt index 8d2aa82..1ef92a1 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/MainActivity.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/MainActivity.kt @@ -22,7 +22,13 @@ 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.view.Menu import android.view.MenuItem @@ -32,27 +38,29 @@ 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.NFCMensaCard import org.mosad.seil0.projectlaogai.controller.PreferencesController import org.mosad.seil0.projectlaogai.controller.PreferencesController.Companion.cColorAccent import org.mosad.seil0.projectlaogai.controller.PreferencesController.Companion.cColorPrimary -import org.mosad.seil0.projectlaogai.controller.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.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 lateinit var adapter: NfcAdapter + private lateinit var pendingIntent: PendingIntent + private lateinit var intentFiltersArray: Array + private lateinit var techListsArray: Array> + private var useNFC = false + override fun onCreate(savedInstanceState: Bundle?) { Aesthetic.attach(this) super.onCreate(savedInstanceState) @@ -61,26 +69,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() - } - } + initAesthetic() + initForegroundDispatch() //init home fragment val fragmentTransaction: FragmentTransaction = supportFragmentManager.beginTransaction() @@ -94,22 +84,40 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte toggle.syncState() nav_view.setNavigationItemSelectedListener(this) + + // if we get an NFC read intent while the app is closed call readBalance + if (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action) + NFCMensaCard.readBalance(intent, this) } + 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 +140,13 @@ 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_settings -> SettingsFragment() + else -> HomeFragment() } val fragmentTransaction: FragmentTransaction = supportFragmentManager.beginTransaction() @@ -155,6 +154,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte fragmentTransaction.commit() drawer_layout.closeDrawer(GravityCompat.START) + return true } @@ -163,56 +163,48 @@ 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) - } + PreferencesController.load(this) // load the settings, must be finished before doing anything else + CacheController(this) // load the cache } println("startup completed in $startupTime ms") } + private fun initAesthetic() { + // If we haven't set any defaults, do that now + if (Aesthetic.isFirstTime) { + // this is executed on the first app start, use this to show tutorial etc. + Aesthetic.config { + activityTheme(R.style.AppTheme_Light) + apply() + } + + SettingsFragment().selectCourse(this) // FIXME this is not working + } + + Aesthetic.config { + colorPrimary(cColorPrimary) + colorPrimaryDark(cColorPrimary) + colorAccent(cColorAccent) + navigationViewMode(NavigationViewMode.SELECTED_ACCENT) + apply() + } + + } + + 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 + ) + } + } + } diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/CacheController.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/controller/CacheController.kt index bd8c065..09c6aba 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/CacheController.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/controller/CacheController.kt @@ -24,27 +24,91 @@ package org.mosad.seil0.projectlaogai.controller import android.content.Context import com.google.gson.Gson +import com.google.gson.GsonBuilder import com.google.gson.JsonParser -import org.mosad.seil0.projectlaogai.hsoparser.Course -import org.mosad.seil0.projectlaogai.hsoparser.MensaWeek -import org.mosad.seil0.projectlaogai.hsoparser.TimetableWeek +import com.google.gson.reflect.TypeToken +import org.mosad.seil0.projectlaogai.controller.PreferencesController.Companion.cCourse +import org.mosad.seil0.projectlaogai.hsoparser.* import java.io.BufferedReader import java.io.File import java.io.FileReader -import com.google.gson.reflect.TypeToken +import java.util.* +import kotlin.collections.ArrayList class CacheController(cont: Context) { private val context = cont + init { + val cal = Calendar.getInstance() + val currentDay = Calendar.getInstance().get(Calendar.DAY_OF_WEEK) + val currentTime = System.currentTimeMillis() / 1000 + + // check if we need to update the mensa data before displaying it + readMensa(context) + cal.time = Date(mensaMenu.meta.updateTime * 1000) + + // if a) it's monday and the last cache update was on sunday or b) the cache is older than 24hr, update blocking + if ((currentDay == Calendar.MONDAY && cal.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY) || (currentTime - mensaMenu.meta.updateTime) > 86400) { + println("update mensa blocking") + TCoRAPIController.getMensa(context).get() + } + + // check if we need to update the timetables before displaying them + readTimetable(cCourse.courseName, 0, context) + cal.time = Date(timetables[0].meta.updateTime * 1000) + + // if a) it`s monday and the last cache update was not on a sunday or b) the cache is older than 24hr, update blocking + if((currentDay == Calendar.MONDAY && cal.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY) || (currentTime - timetables[0].meta.updateTime) > 86400) { + println("updating timetable after sunday!") + val jobA = TCoRAPIController.getTimetable(cCourse.courseName, 0, context) + val jobB = TCoRAPIController.getTimetable(cCourse.courseName, 1, context) + + jobA.get() + jobB.get() + } + + // check if an update is necessary, not blocking + if(currentTime - PreferencesController.coursesCacheTime > 86400) + TCoRAPIController.getCoursesList(context) + + if(currentTime - PreferencesController.mensaCacheTime > 10800) + TCoRAPIController.getMensa(context) + + if(currentTime - PreferencesController.timetableCacheTime > 10800) { + TCoRAPIController.getTimetable(cCourse.courseName, 0, context) + TCoRAPIController.getTimetable(cCourse.courseName, 1, context) + } + + readStartCache(cCourse.courseName) + } + companion object { var coursesList = ArrayList() - var mensaCurrentWeek = MensaWeek() - var mensaNextWeek = MensaWeek() - var timetables = ArrayList() + var timetables = ArrayList() + var mensaMenu = MensaMenu() /** - * read current and next weeks mensa menus from the cached file + * read the courses list from the cached file + * add them to the coursesList object + */ + private fun readCoursesList(context: Context) { + val file = File(context.filesDir, "courses.json") + + // make sure the file exists + if (!file.exists()) + TCoRAPIController.getCoursesList(context).get() + + val fileReader = FileReader(file) + val bufferedReader = BufferedReader(fileReader) + val coursesObject = JsonParser().parse(bufferedReader.readLine()).asJsonObject + + coursesList = Gson().fromJson(coursesObject.getAsJsonArray("courses"), object : TypeToken>() {}.type) + } + + /** + * get the MensaMenu object from the cached json, + * if cache is empty create the cache file */ fun readMensa(context: Context) { val file = File(context.filesDir, "mensa.json") @@ -54,16 +118,11 @@ class CacheController(cont: Context) { 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) + mensaMenu = GsonBuilder().create().fromJson(mensaObject, MensaMenu().javaClass) } /** @@ -83,10 +142,10 @@ class CacheController(cont: Context) { 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) + if (timetables.size > week) { + timetables[week] = (Gson().fromJson(timetableObject, TimetableCourseWeek().javaClass)) + } else { + timetables.add(Gson().fromJson(timetableObject, TimetableCourseWeek().javaClass)) } } } @@ -96,28 +155,10 @@ class CacheController(cont: Context) { * @param courseName the course name (e.g AI1) */ fun readStartCache(courseName: String) { - readCoursesList() + readCoursesList(context) 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>() {}.type) - } - } \ No newline at end of file diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/NFCMensaCard.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/controller/NFCMensaCard.kt new file mode 100644 index 0000000..e3fbb77 --- /dev/null +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/controller/NFCMensaCard.kt @@ -0,0 +1,105 @@ +/** + * ProjectLaogai + * + * Copyright 2019 + * + * 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 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.i(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 (!PreferencesController.oGiants) { + String.format("%.2f €", (currentRaw.toFloat() / 1000)) + } else { + String.format("%.4f shm", (currentRaw.toFloat() * 0.0000075)) + } + + val last = if (!PreferencesController.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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/PreferencesController.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/controller/PreferencesController.kt index 4901306..d6a43fa 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/PreferencesController.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/controller/PreferencesController.kt @@ -42,6 +42,7 @@ class PreferencesController { var cColorAccent: Int = Color.parseColor("#3F51B5") var cCourse = Course("https://www.hs-offenburg.de/index.php?id=6627&class=class&iddV=DA64F6FE-9DDB-429E-A677-05D0D40CB636&week=0", "AI3") var cShowBuffet = true + var oGiants = false // the save function fun save(context: Context) { diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/TCoRAPIController.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/controller/TCoRAPIController.kt index af16d67..2d03274 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/TCoRAPIController.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/controller/TCoRAPIController.kt @@ -23,76 +23,98 @@ package org.mosad.seil0.projectlaogai.controller import android.content.Context +import android.util.Log import org.jetbrains.anko.doAsync import org.json.JSONObject import org.mosad.seil0.projectlaogai.controller.PreferencesController.Companion.coursesCacheTime import org.mosad.seil0.projectlaogai.controller.PreferencesController.Companion.mensaCacheTime import org.mosad.seil0.projectlaogai.controller.PreferencesController.Companion.timetableCacheTime -import java.io.* +import java.io.BufferedWriter +import java.io.File +import java.io.FileWriter import java.net.URL +import kotlin.Exception -class TCoRAPIController(cont: Context) { - private val context = cont +class TCoRAPIController { companion object { + private const val className = "TCoRAPIController" + private const val tcorBaseURL = "https://tcor.mosad.xyz" + + /** + * get the json object from tcor api and write it as file (cache) + */ + fun getCoursesList(context: Context) = doAsync { + try { + val url = URL("$tcorBaseURL/courseList") + val file = File(context.filesDir, "courses.json") + + // read data from the API + val coursesObject = JSONObject(url.readText()) //JSONObject(inReader.readLine()) + + // write the json object to a file + val writer = BufferedWriter(FileWriter(file)) + writer.write(coursesObject.toString()) + writer.close() + + // update cache time + coursesCacheTime = System.currentTimeMillis() / 1000 + PreferencesController.save(context) + } catch (ex: Exception) { + Log.e(className, "failed to get /courseList", ex) + } + + } + /** * get the json object from tcor api and write it as file (cache) */ fun getMensa(context: Context) = doAsync { - val url = URL("https://tcor.mosad.xyz/mensamenu") - val file = File(context.filesDir, "mensa.json") + try { + val url = URL("$tcorBaseURL/mensamenu") + val file = File(context.filesDir, "mensa.json") - // read data from the API - val mensaObject = JSONObject(url.readText()) //JSONObject(inReader.readLine()) + // 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() + // write the json object to a file + val writer = BufferedWriter(FileWriter(file)) + writer.write(mensaObject.toString()) + writer.close() + + // update cache time + mensaCacheTime = System.currentTimeMillis() / 1000 + PreferencesController.save(context) + } catch (ex: Exception) { + Log.e(className, "failed to get /mensamenu", ex) + } - // update cache time - mensaCacheTime = System.currentTimeMillis() / 1000 - PreferencesController.save(context) } /** * get the json object from tcor api and write it as file (cache) */ 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") + try { + val url = URL("$tcorBaseURL/timetable?courseName=$courseName&week=$week") + val file = File(context.filesDir, "timetable-$courseName-$week.json") - // read data from the API - val mensaObject = JSONObject(url.readText()) //JSONObject(inReader.readLine()) + // 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() + // write the json object to a file + val writer = BufferedWriter(FileWriter(file)) + writer.write(mensaObject.toString()) + writer.close() + + // update cache time + timetableCacheTime = System.currentTimeMillis() / 1000 + PreferencesController.save(context) + } catch (ex: Exception) { + Log.e(className, "failed to get /timetable", ex) + } - // update cache time - timetableCacheTime = System.currentTimeMillis() / 1000 - PreferencesController.save(context) } } - /** - * 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) - } } \ No newline at end of file diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/HomeFragment.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/HomeFragment.kt index ab5ac75..3973212 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/HomeFragment.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/HomeFragment.kt @@ -24,25 +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 org.mosad.seil0.projectlaogai.R -import org.mosad.seil0.projectlaogai.controller.CacheController.Companion.mensaCurrentWeek +import org.mosad.seil0.projectlaogai.controller.CacheController.Companion.mensaMenu 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.uicomponents.DayCardView -import org.mosad.seil0.projectlaogai.uicomponents.LessonLinearLayout import org.mosad.seil0.projectlaogai.uicomponents.MealLinearLayout import java.text.SimpleDateFormat import java.util.* @@ -53,6 +50,7 @@ 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? { @@ -60,7 +58,7 @@ class HomeFragment : Fragment() { val view: View = inflater.inflate(R.layout.fragment_home, container, false) addMensaMenu().get() - addTimeTable() + addTimeTable().get() // Inflate the layout for this fragment return view @@ -77,38 +75,41 @@ class HomeFragment : Fragment() { uiThread { - 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) } } @@ -117,99 +118,52 @@ 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 uiThread { - 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 && timetables.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 < timetables.size) { + for (dayIndex in dayIndexSearch..5) { + dayTimetable = timetables[weekIndexSearch].timetable.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 =(timetables[weekIndexSearch].meta.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,7 +176,6 @@ 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 diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/MensaFragment.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/MensaFragment.kt index 109ae0f..771637e 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/MensaFragment.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/MensaFragment.kt @@ -30,9 +30,9 @@ 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.R 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.CacheController.Companion.mensaMenu import org.mosad.seil0.projectlaogai.controller.PreferencesController.Companion.cShowBuffet import org.mosad.seil0.projectlaogai.controller.TCoRAPIController import org.mosad.seil0.projectlaogai.hsoparser.MensaWeek @@ -48,17 +48,17 @@ class MensaFragment : Fragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - val view: View = inflater.inflate(org.mosad.seil0.projectlaogai.R.layout.fragment_mensa, container, false) + val view: View = inflater.inflate(R.layout.fragment_mensa, container, false) // init actions refreshAction() // add the current week (week starts on sunday) - 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).get() // add the next week - addWeek(mensaNextWeek, 0) + addWeek(mensaMenu.nextWeek, 0) // TODO should we show a info if there is no more food this & next week? @@ -128,11 +128,11 @@ class MensaFragment : Fragment() { 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).get() // add the next week - addWeek(mensaNextWeek, 0) + addWeek(mensaMenu.nextWeek, 0) refreshLayout_Mensa.isRefreshing = false } diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/SettingsFragment.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/SettingsFragment.kt index 972e243..50565b9 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/SettingsFragment.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/SettingsFragment.kt @@ -24,6 +24,7 @@ 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 @@ -35,22 +36,25 @@ import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.color.colorChooser import com.afollestad.materialdialogs.customview.customView import com.afollestad.materialdialogs.list.listItems +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 org.mosad.seil0.projectlaogai.BuildConfig +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.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 java.util.* + /** * The settings controller class * contains all needed parts to display and the settings screen @@ -60,6 +64,8 @@ class SettingsFragment : Fragment() { private lateinit var linLayoutUser: LinearLayout private lateinit var linLayoutCourse: 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 @@ -71,6 +77,8 @@ class SettingsFragment : Fragment() { linLayoutUser = view.findViewById(R.id.linLayout_User) linLayoutCourse = view.findViewById(R.id.linLayout_Course) 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) @@ -84,9 +92,28 @@ class SettingsFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + // initialize the settings gui 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)) + txtView_SelectedTheme.text = resources.getString(R.string.themeLight) + } + "dark" -> { + switch_buffet.setTextColor(activity!!.resources.getColor(R.color.textPrimaryDark, activity!!.theme)) + txtView_SelectedTheme.text = resources.getString(R.string.themeDark) + } + "black" -> { + switch_buffet.setTextColor(activity!!.resources.getColor(R.color.textPrimaryDark, activity!!.theme)) + txtView_SelectedTheme.text = resources.getString(R.string.themeBlack) + } + } + } /** @@ -97,9 +124,13 @@ class SettingsFragment : Fragment() { // open a new dialog } + linLayoutUser.setOnLongClickListener { + PreferencesController.oGiants = true // enable easter egg + return@setOnLongClickListener true + } + linLayoutCourse.setOnClickListener { selectCourse(context!!) - txtView_Course.text = cCourse.courseName // update txtView } linLayoutAbout.setOnClickListener { @@ -110,6 +141,52 @@ 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 { + listItemsSingleChoice(items = themes) { _, 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) + } + } + } + } + } + linLayoutPrimaryColor.setOnClickListener { // open a new color chooser dialog MaterialDialog(context!!) @@ -184,6 +261,7 @@ class SettingsFragment : Fragment() { uiThread { dialog.dismiss() + txtView_Course.text = cCourse.courseName // update txtView } } diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/TimeTableFragment.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/TimeTableFragment.kt index 824c706..d4a2dc5 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/TimeTableFragment.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/TimeTableFragment.kt @@ -26,8 +26,10 @@ 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 @@ -36,12 +38,8 @@ 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.uicomponents.DayCardView /** * The timetable controller class @@ -49,16 +47,19 @@ 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 faBtnAddLesson: FloatingActionButton override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view: View = inflater.inflate(R.layout.fragment_timetable, container, false) + scrollViewTimetable = view.findViewById(R.id.scrollView_Timetable) + faBtnAddLesson = view.findViewById(R.id.faBtnAddLesson) // init actions - refreshAction() + initActions() - if (timetables[0].days.isNotEmpty() && timetables[1].days.isNotEmpty()) { + if (timetables[0].timetable.days.isNotEmpty() && timetables[1].timetable.days.isNotEmpty()) { addInitWeeks() } else { MaterialDialog(context!!) @@ -70,70 +71,64 @@ class TimeTableFragment : Fragment() { return view } + /** + * initialize the actions + */ + private fun initActions() = doAsync { + + uiThread { + refreshLayout_Timetable.setOnRefreshListener { + updateTimetableScreen() + } + + faBtnAddLesson.setOnClickListener { + MaterialDialog(context!!) + .title(text = "Vorlesung hinzufügen") + .message(text = "wähle einen Studiengang aus:\n\nWähle eine Vorlesung aus: \n\n Diese Funktion ist noch nicht verfügbar") + .show() + } + + // hide the btnCardValue if the user is scrolling down + scrollViewTimetable.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> + if (scrollY > oldScrollY) { + faBtnAddLesson.hide() + } else { + faBtnAddLesson.show() + } + } + + } + + } + /** * add the current and next weeks lessons */ private fun addInitWeeks() = doAsync { - val dayIndex = NotRetardedCalendar().getDayOfWeekIndex() - val calendar = Calendar.getInstance() + val currentDayIndex = NotRetardedCalendar.getDayOfWeekIndex() - // add current week - addWeek(dayIndex, 5, timetables[0], calendar).get() - - // add next week - addWeek(0, dayIndex - 1, timetables[1], calendar) + addWeek(currentDayIndex, 5, 0).get() // add current week + addWeek(0, currentDayIndex - 1, 1) // add next week } - private fun addWeek(dayStart: Int, dayEnd: Int, timetable: TimetableWeek, calendar: Calendar) = doAsync { + private fun addWeek(startDayIndex: Int, dayEndIndex: Int, weekIndex: Int) = doAsync { + val timetable = timetables[weekIndex].timetable + val timetableMeta = timetables[weekIndex].meta + uiThread { - for (day in dayStart..dayEnd) { - var helpLesson = LessonLinearLayout(context) + for (dayIndex in startDayIndex..dayEndIndex) { val dayCardView = DayCardView(context!!) - dayCardView.setDayHeading(formatter.format(calendar.time)) - // for each timeslot of the day - for ((tsIndex, timeslot) in timetable.days[day].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 - } - } - } - - helpLesson.disableDivider() - calendar.add(Calendar.DATE, 1) + // some wired calendar magic, calculate the correct date to be shown ((timetable week - current week * 7) + (dayIndex - dayIndex of current week) + val daysToAdd =(timetableMeta.weekNumberYear - NotRetardedCalendar.getWeekOfYear()) * 7 + (dayIndex - NotRetardedCalendar.getDayOfWeekIndex()) + dayCardView.addTimetableDay(timetable.days[dayIndex], daysToAdd) // 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 - */ - private fun refreshAction() = doAsync { - uiThread { - // set the refresh listener - refreshLayout_Timetable.setOnRefreshListener { - updateTimetableScreen() - } - } } @@ -150,14 +145,13 @@ class TimeTableFragment : Fragment() { 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() + addWeek(dayIndex, 5, 0).get() // add next week - addWeek(0, dayIndex - 1, timetables[1], calendar) + addWeek(0, dayIndex - 1, 1) refreshLayout_Timetable.isRefreshing = false } diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/hsoparser/DataTypes.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/hsoparser/DataTypes.kt index acd972d..f3936f3 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/hsoparser/DataTypes.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/hsoparser/DataTypes.kt @@ -27,7 +27,8 @@ 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"), @@ -69,7 +70,7 @@ class DataTypes { Color.parseColor("#FF9100"), Color.parseColor("#FF3D00"), Color.parseColor("#000000") - ) + ) init { // do something @@ -82,45 +83,70 @@ class DataTypes { } class NotRetardedCalendar { - private val calendar = Calendar.getInstance()!! + 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 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 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) + } } } } +// data classes for the course part data class Course(val courseLink: String, val courseName: String) +// data classes for the Mensa part data class Meal(val day: String, val heading: String, val parts: ArrayList, val additives: String) data class Meals(val meals: ArrayList) data class MensaWeek(val days: Array = 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> = Array(6) { ArrayList()}) +data class MensaMenu(val meta: MensaMeta = MensaMeta(), val currentWeek: MensaWeek = MensaWeek(), val nextWeek: MensaWeek = MensaWeek()) -data class TimetableWeek(val days: Array = Array(6) { TimetableDay() }) \ No newline at end of file +// data classes for the timetable part +data class Lesson( + val lessonSubject: String, + val lessonTeacher: String, + val lessonRoom: String, + val lessonRemark: String +) + +data class TimetableDay(val timeslots: Array> = Array(6) { ArrayList() }) + +data class TimetableWeek(val days: Array = 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: TimetableWeek = TimetableWeek()) diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/uicomponents/DayCardView.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/uicomponents/DayCardView.kt index 7ab1287..b1e9a95 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/uicomponents/DayCardView.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/uicomponents/DayCardView.kt @@ -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.hsoparser.DataTypes +import org.mosad.seil0.projectlaogai.hsoparser.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 + for ((tsIndex, timeslot) in timetable.timeslots.withIndex()) { + + for (lesson in timeslot) { + if (lesson.lessonSubject.isNotEmpty()) { + + val lessonLayout = LessonLinearLayout(context) + lessonLayout.setLesson(lesson, DataTypes().getTime()[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 + } + } \ No newline at end of file diff --git a/app/src/main/res/drawable/background_splash.xml b/app/src/main/res/drawable/background_splash.xml index f9df9fc..9de6881 100644 --- a/app/src/main/res/drawable/background_splash.xml +++ b/app/src/main/res/drawable/background_splash.xml @@ -3,9 +3,7 @@ - - - + diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml deleted file mode 100644 index 6429411..0000000 --- a/app/src/main/res/layout/fragment_settings.xml +++ /dev/null @@ -1,185 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/linearlayout_lesson.xml b/app/src/main/res/layout/linearlayout_lesson.xml deleted file mode 100644 index 4012441..0000000 --- a/app/src/main/res/layout/linearlayout_lesson.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/linearlayout_meal.xml b/app/src/main/res/layout/linearlayout_meal.xml deleted file mode 100644 index 15752aa..0000000 --- a/app/src/main/res/layout/linearlayout_meal.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layouts/activities/layout/activity_main.xml similarity index 95% rename from app/src/main/res/layout/activity_main.xml rename to app/src/main/res/layouts/activities/layout/activity_main.xml index 7912343..6259fe8 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layouts/activities/layout/activity_main.xml @@ -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"/> - + diff --git a/app/src/main/res/layout/app_bar_main.xml b/app/src/main/res/layouts/activities/layout/app_bar_main.xml similarity index 100% rename from app/src/main/res/layout/app_bar_main.xml rename to app/src/main/res/layouts/activities/layout/app_bar_main.xml diff --git a/app/src/main/res/layout/cardview_day.xml b/app/src/main/res/layouts/activities/layout/cardview_day.xml similarity index 81% rename from app/src/main/res/layout/cardview_day.xml rename to app/src/main/res/layouts/activities/layout/cardview_day.xml index 8c27625..1fdcca4 100644 --- a/app/src/main/res/layout/cardview_day.xml +++ b/app/src/main/res/layouts/activities/layout/cardview_day.xml @@ -2,8 +2,10 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layouts/activities/layout/linearlayout_meal.xml b/app/src/main/res/layouts/activities/layout/linearlayout_meal.xml new file mode 100644 index 0000000..519307e --- /dev/null +++ b/app/src/main/res/layouts/activities/layout/linearlayout_meal.xml @@ -0,0 +1,35 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/nav_header_main.xml b/app/src/main/res/layouts/activities/layout/nav_header_main.xml similarity index 96% rename from app/src/main/res/layout/nav_header_main.xml rename to app/src/main/res/layouts/activities/layout/nav_header_main.xml index 087f997..67d4683 100644 --- a/app/src/main/res/layout/nav_header_main.xml +++ b/app/src/main/res/layouts/activities/layout/nav_header_main.xml @@ -19,7 +19,7 @@ android:layout_height="wrap_content" android:paddingTop="@dimen/nav_header_vertical_spacing" app:srcCompat="@mipmap/ic_laogai_icon" - android:contentDescription="@string/nav_header_desc" + android:contentDescription="@string/app_name" android:id="@+id/imageView"/> + + + body { + background-color: #ffffff; + color: #000000; + font-family: sans-serif; + overflow-wrap: break-word; + } + pre { + background-color: #eeeeee; + padding: 1em; + white-space: pre-wrap; + } + + + + 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; + } + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_loading.xml b/app/src/main/res/layouts/dialogs/layout/dialog_loading.xml similarity index 100% rename from app/src/main/res/layout/dialog_loading.xml rename to app/src/main/res/layouts/dialogs/layout/dialog_loading.xml diff --git a/app/src/main/res/layouts/dialogs/layout/dialog_mensa_credit.xml b/app/src/main/res/layouts/dialogs/layout/dialog_mensa_credit.xml new file mode 100644 index 0000000..9eeee12 --- /dev/null +++ b/app/src/main/res/layouts/dialogs/layout/dialog_mensa_credit.xml @@ -0,0 +1,34 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layouts/fragments/layout/fragment_home.xml similarity index 90% rename from app/src/main/res/layout/fragment_home.xml rename to app/src/main/res/layouts/fragments/layout/fragment_home.xml index 7d10427..0971b9c 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layouts/fragments/layout/fragment_home.xml @@ -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"> + tools:context=".fragments.MensaFragment" + android:background="?themePrimary"> + android:layout_height="match_parent" + android:id="@+id/scrollView_Mensa"> diff --git a/app/src/main/res/layout/fragment_moodle.xml b/app/src/main/res/layouts/fragments/layout/fragment_moodle.xml similarity index 100% rename from app/src/main/res/layout/fragment_moodle.xml rename to app/src/main/res/layouts/fragments/layout/fragment_moodle.xml diff --git a/app/src/main/res/layouts/fragments/layout/fragment_settings.xml b/app/src/main/res/layouts/fragments/layout/fragment_settings.xml new file mode 100644 index 0000000..c33c639 --- /dev/null +++ b/app/src/main/res/layouts/fragments/layout/fragment_settings.xml @@ -0,0 +1,283 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_timetable.xml b/app/src/main/res/layouts/fragments/layout/fragment_timetable.xml similarity index 57% rename from app/src/main/res/layout/fragment_timetable.xml rename to app/src/main/res/layouts/fragments/layout/fragment_timetable.xml index 86982a6..94e22d4 100644 --- a/app/src/main/res/layout/fragment_timetable.xml +++ b/app/src/main/res/layouts/fragments/layout/fragment_timetable.xml @@ -3,7 +3,8 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".fragments.TimeTableFragment"> + tools:context=".fragments.TimeTableFragment" + android:background="?themePrimary"> + android:layout_height="match_parent" android:id="@+id/scrollView_Timetable"> + \ No newline at end of file diff --git a/app/src/main/res/raw/notices.xml b/app/src/main/res/raw/notices.xml new file mode 100644 index 0000000..99c8b86 --- /dev/null +++ b/app/src/main/res/raw/notices.xml @@ -0,0 +1,39 @@ + + + + Material Dialogs + https://github.com/afollestad/material-dialogs + Copyright (C) Aidan Follestad + Apache Software License 2.0 + + + Aesthetic + https://github.com/afollestad/aesthetic + Copyright Aidan Follestad + Apache Software License 2.0 + + + gson + https://github.com/google/gson + Copyright 2008 Google Inc. + Apache Software License 2.0 + + + anko + https://github.com/Kotlin/anko + Copyright JetBrains + Apache Software License 2.0 + + + farebot part for desfire cards + https://github.com/codebutler/farebot + Copyright 2011-2012, 2014, 2016 Eric Butler + GNU General Public License 3.0 + + + Android Support Libraries + http://developer.android.com/tools/support-library/index.html + Copyright (C) 2016 The Android Open Source Project + Apache Software License 2.0 + + diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index 85d7943..64a267b 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -1,30 +1,50 @@ + Navigationsleiste schließen + Navigationsleiste öffnen + + Home Mensa Stundenplan Moodle Einstellungen + + Essen Heute, %1$s Morgen, %1$s keine Essensausgabe Diese Woche keine weitere Essensausgabe heute keine Vorlesung! - Fehler - Stundenplan konnte nicht geladen werden! + + Info Benutzer Tippen, um den Kurs zu ändern + Über + Lizenzen + Design + Hell + Dunkel + Schwarz Hauptfarbe Die Primärfarbe, Standard ist Schwarz. Akzentfarbe Die Akzentfarbe, Standard ist indigo - auswählen - Über - lade Stundenplan … - Navigationsleiste schließen - Navigationsleiste öffnen Buffet immer anzeigen - Wähle deinen Studiengang aus + + + Wähle deinen Studiengang + lade Stundenplan … + auswählen + schließen + Mensa-Guthaben + aktuell: %1$s\n + letzte Abbuchung: %1$s + + + Fehler + Stundenplan konnte nicht geladen werden! + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml new file mode 100644 index 0000000..da9cee6 --- /dev/null +++ b/app/src/main/res/values/attrs.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 7f52220..92bab79 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -2,28 +2,27 @@ #000000 #000000 - #3F51B5 - #FFFFFF - - #FFFFFF - #FFFFFF - #FAFAFA + #3f51b5 + #ffffff + #ffffff + #ffffff #000000 #818181 - #FFFFFF - #E0E0E0 - #000000 - #818181 - #000000 #303030 #424242 + #ffffff + #e0e0e0 + #232323 + + #ffffff + #ffffff + #000000 + #818181 + #e0e0e0 - #FFFFFF - #FFFFFF - #424242 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aadff31..57efb0b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,45 +4,62 @@ Close navigation drawer Project Laogai seil0@mosad.xyz - Project Laogai + Home Mensa Timetable Moodle Settings + Meal Today, %1$s Tomorrow, %1$s the Mensa is closed No more Food this week - "No lecture today!" - Error - Could not load timetable!" - SampleUser@stud.hs-offenburg.de + + Info + User + Tap to change course + About + "This software is made by @Seil0 and is published under the terms and conditions of GPL 3. For further information visit \ngit.mosad.xyz/Seil0/ProjectLaogai \n\n© 2018-2019 seil0@mosad.xyz " + hso App by @Seil0 + Version %1$s (%2$s) + Licenses + Theme + Light + Dark + Black + Primary color + The primary color, default is black. + Accent color + The accent color, default is indigo. + Always show buffet + + + Select your course + loading timetable … + select + close + Mensa credit + current: %1$s\n + last: %1$s + + + + spinefield@stud.hs-offenburg.de SampleCourse 3 Montag, 30.02 0.00 – 23.59 - Info - User - Tap to change course - Select your course - primary color - The primary color, default is black. - accent color - The accent color, default is indigo. - always show buffet - select - hso App by @Seil0 - version %1$s (%2$s) - About - "This software is made by @Seil0 and is published under the terms and conditions of GPL 3. For further information visit \ngit.mosad.xyz/Seil0/ProjectLaogai \n\n© 2018-2019 seil0@mosad.xyz " - loading timetable … + + Error + Could not load timetable!" + org.mosad.seil0.projectlaogai.course org.mosad.seil0.projectlaogai.courseTTLink org.mosad.seil0.projectlaogai.colorPrimary diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index a27a731..8981642 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,11 +1,50 @@ - + + + + + + + + diff --git a/app/src/main/res/xml/nfc_tech_filter.xml b/app/src/main/res/xml/nfc_tech_filter.xml new file mode 100644 index 0000000..eb44974 --- /dev/null +++ b/app/src/main/res/xml/nfc_tech_filter.xml @@ -0,0 +1,6 @@ + + + + android.nfc.tech.NfcA + + diff --git a/build.gradle b/build.gradle index 3cedbfe..8d482e6 100644 --- a/build.gradle +++ b/build.gradle @@ -1,13 +1,13 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.3.31' + ext.kotlin_version = '1.3.50' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.4.0' + classpath 'com.android.tools.build:gradle:3.5.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong diff --git a/fastlane/metadata/android/de-DE/full_description.txt b/fastlane/metadata/android/de-DE/full_description.txt new file mode 100644 index 0000000..71eca07 --- /dev/null +++ b/fastlane/metadata/android/de-DE/full_description.txt @@ -0,0 +1,8 @@ +ProjectLaogai ist eine App um den Vorlesungsplan und den Mensa-Speiseplan der Hochschule Offenburg anzuzeigen. + +Features: +* schaue was es diese und nächste Woche in der Mensa zu Essen gibt +* schaue dir deinen Vorlesungsplan an +* lass dir das aktuelle Guthaben der Mensakarte anzeigen +* öffne moodle direkt in der App +* viele lustige Bugs diff --git a/fastlane/metadata/android/de/short_description.txt b/fastlane/metadata/android/de-DE/short_description.txt similarity index 100% rename from fastlane/metadata/android/de/short_description.txt rename to fastlane/metadata/android/de-DE/short_description.txt diff --git a/fastlane/metadata/android/de/full_description.txt b/fastlane/metadata/android/de/full_description.txt deleted file mode 100644 index 8acd636..0000000 --- a/fastlane/metadata/android/de/full_description.txt +++ /dev/null @@ -1,7 +0,0 @@ -ProjectLaogai ist eine App um den Stundenplan und das Mensa-Essen der Hochschule Offenburg anzuzeigen. - -Features: -* schaue was es diese und nächste WOche in der Mensa zu Essen gibt -* schaue dir deinen Stundenplan an -* öffne moodle direkt in der App -* viele lustige Bugs diff --git a/fastlane/metadata/android/en-US/changelogs/14.txt b/fastlane/metadata/android/en-US/changelogs/14.txt new file mode 100644 index 0000000..b3cceea --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/14.txt @@ -0,0 +1,8 @@ +This release 0.5.0 is called "artistical Apollon". + +* new: it's now possible to choose a theme (light, dark or black) +* new: you can now check your current mensa card balance +* change: updated some libs, updated kotlin to 1.3.41 +* change: added a license dialog for all used libraries +* fix: the mensa should now show the correct meals on sunday/monday +* fix: the timetable should now show the correct on the sunday/monday change diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index aa3f7ce..c080ed0 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -3,5 +3,6 @@ ProjectLaogai is an app to access the timetable and the mensa menu of Hochschule Features: * check out the mensa menu of this and next week * access your timetable +* check the current balance of your mensa card * open moodle * probably some funny bugs diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/ProjectLaogai_Mensa_dark.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/ProjectLaogai_Mensa_dark.png new file mode 100644 index 0000000..714d246 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/ProjectLaogai_Mensa_dark.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/ProjectLaogai_Settings_dark.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/ProjectLaogai_Settings_dark.png new file mode 100644 index 0000000..c19082d Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/ProjectLaogai_Settings_dark.png differ diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index f6b961f..5c2d1cf 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f59e369..7c4388a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Thu Apr 25 17:58:09 WEST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip diff --git a/gradlew b/gradlew index cccdd3d..8e25e6c 100755 --- a/gradlew +++ b/gradlew @@ -1,5 +1,21 @@ #!/usr/bin/env sh +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + ############################################################################## ## ## Gradle start up script for UN*X @@ -28,7 +44,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" diff --git a/gradlew.bat b/gradlew.bat index e95643d..24467a1 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome