diff --git a/app/build.gradle b/app/build.gradle index 57982f62..7ee81956 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -58,6 +58,7 @@ android { dataBinding = true } compileOptions { + coreLibraryDesugaringEnabled true sourceCompatibility '1.8' targetCompatibility '1.8' } @@ -105,6 +106,8 @@ tasks.whenTaskAdded { task -> dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1' + kapt "androidx.room:room-compiler:${versions.room}" debugImplementation "com.amitshekhar.android:debug-db:1.0.5" @@ -181,6 +184,7 @@ dependencies { //implementation "org.redundent:kotlin-xml-builder:1.5.3" implementation "io.github.wulkanowy:signer-android:0.1.1" + implementation 'com.github.wulkanowy.uonet-request-signer:hebe-jvm:a99ca50a31' implementation "androidx.work:work-runtime-ktx:${versions.work}" diff --git a/app/src/main/assets/pl-changelog.html b/app/src/main/assets/pl-changelog.html index 50650cdd..165582ec 100644 --- a/app/src/main/assets/pl-changelog.html +++ b/app/src/main/assets/pl-changelog.html @@ -1,10 +1,8 @@ -

Wersja 4.4.3, 2020-10-16

+

Wersja 4.5-beta.2, 2021-02-21



Dzięki za korzystanie ze Szkolnego!
-© Kuba Szczodrzyński, Kacper Ziubryniewicz 2020 +© Kuba Szczodrzyński, Kacper Ziubryniewicz 2021 diff --git a/app/src/main/cpp/szkolny-signing.cpp b/app/src/main/cpp/szkolny-signing.cpp index 4861d399..b2b15406 100644 --- a/app/src/main/cpp/szkolny-signing.cpp +++ b/app/src/main/cpp/szkolny-signing.cpp @@ -9,7 +9,7 @@ /*secret password - removed for source code publication*/ static toys AES_IV[16] = { - 0xaa, 0x6d, 0x87, 0x46, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; + 0x42, 0xf5, 0x8e, 0x53, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat); diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/App.kt b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt index 9a2f84d6..88bdc603 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/App.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt @@ -294,6 +294,19 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { "Vulcan" ) + val pushVulcanHebeApp = FirebaseApp.initializeApp( + this@App, + FirebaseOptions.Builder() + .setProjectId("dzienniczekplus") + .setStorageBucket("dzienniczekplus.appspot.com") + .setDatabaseUrl("https://dzienniczekplus.firebaseio.com") + .setGcmSenderId("987828170337") + .setApiKey("AIzaSyDW8MUtanHy64_I0oCpY6cOxB3jrvJd_iA") + .setApplicationId("1:987828170337:android:7e16404b9e5deaaa") + .build(), + "VulcanHebe" + ) + try { FirebaseInstanceId.getInstance().instanceId.addOnSuccessListener { instanceIdResult -> val token = instanceIdResult.token @@ -324,6 +337,14 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { config.sync.tokenVulcanList = listOf() } } + FirebaseInstanceId.getInstance(pushVulcanHebeApp).instanceId.addOnSuccessListener { instanceIdResult -> + val token = instanceIdResult.token + d("Firebase", "Got VulcanHebe token: $token") + if (token != config.sync.tokenVulcanHebe) { + config.sync.tokenVulcanHebe = token + config.sync.tokenVulcanHebeList = listOf() + } + } FirebaseMessaging.getInstance().subscribeToTopic(packageName) } catch (e: IllegalStateException) { e.printStackTrace() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt index f9e9d474..d4c6a2e0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt @@ -96,30 +96,30 @@ fun List.byNameFDotSpaceLast(nameFDotSpaceLast: String) = firstOrNull { fun JsonObject?.get(key: String): JsonElement? = this?.get(key) -fun JsonObject?.getBoolean(key: String): Boolean? = get(key)?.let { if (it.isJsonNull) null else it.asBoolean } -fun JsonObject?.getString(key: String): String? = get(key)?.let { if (it.isJsonNull) null else it.asString } -fun JsonObject?.getInt(key: String): Int? = get(key)?.let { if (it.isJsonNull) null else it.asInt } -fun JsonObject?.getLong(key: String): Long? = get(key)?.let { if (it.isJsonNull) null else it.asLong } -fun JsonObject?.getFloat(key: String): Float? = get(key)?.let { if(it.isJsonNull) null else it.asFloat } -fun JsonObject?.getChar(key: String): Char? = get(key)?.let { if(it.isJsonNull) null else it.asCharacter } +fun JsonObject?.getBoolean(key: String): Boolean? = get(key)?.let { if (!it.isJsonPrimitive) null else it.asBoolean } +fun JsonObject?.getString(key: String): String? = get(key)?.let { if (!it.isJsonPrimitive) null else it.asString } +fun JsonObject?.getInt(key: String): Int? = get(key)?.let { if (!it.isJsonPrimitive) null else it.asInt } +fun JsonObject?.getLong(key: String): Long? = get(key)?.let { if (!it.isJsonPrimitive) null else it.asLong } +fun JsonObject?.getFloat(key: String): Float? = get(key)?.let { if(!it.isJsonPrimitive) null else it.asFloat } +fun JsonObject?.getChar(key: String): Char? = get(key)?.let { if(!it.isJsonPrimitive) null else it.asCharacter } fun JsonObject?.getJsonObject(key: String): JsonObject? = get(key)?.let { if (it.isJsonObject) it.asJsonObject else null } fun JsonObject?.getJsonArray(key: String): JsonArray? = get(key)?.let { if (it.isJsonArray) it.asJsonArray else null } -fun JsonObject?.getBoolean(key: String, defaultValue: Boolean): Boolean = get(key)?.let { if (it.isJsonNull) defaultValue else it.asBoolean } ?: defaultValue -fun JsonObject?.getString(key: String, defaultValue: String): String = get(key)?.let { if (it.isJsonNull) defaultValue else it.asString } ?: defaultValue -fun JsonObject?.getInt(key: String, defaultValue: Int): Int = get(key)?.let { if (it.isJsonNull) defaultValue else it.asInt } ?: defaultValue -fun JsonObject?.getLong(key: String, defaultValue: Long): Long = get(key)?.let { if (it.isJsonNull) defaultValue else it.asLong } ?: defaultValue -fun JsonObject?.getFloat(key: String, defaultValue: Float): Float = get(key)?.let { if(it.isJsonNull) defaultValue else it.asFloat } ?: defaultValue -fun JsonObject?.getChar(key: String, defaultValue: Char): Char = get(key)?.let { if(it.isJsonNull) defaultValue else it.asCharacter } ?: defaultValue +fun JsonObject?.getBoolean(key: String, defaultValue: Boolean): Boolean = get(key)?.let { if (!it.isJsonPrimitive) defaultValue else it.asBoolean } ?: defaultValue +fun JsonObject?.getString(key: String, defaultValue: String): String = get(key)?.let { if (!it.isJsonPrimitive) defaultValue else it.asString } ?: defaultValue +fun JsonObject?.getInt(key: String, defaultValue: Int): Int = get(key)?.let { if (!it.isJsonPrimitive) defaultValue else it.asInt } ?: defaultValue +fun JsonObject?.getLong(key: String, defaultValue: Long): Long = get(key)?.let { if (!it.isJsonPrimitive) defaultValue else it.asLong } ?: defaultValue +fun JsonObject?.getFloat(key: String, defaultValue: Float): Float = get(key)?.let { if(!it.isJsonPrimitive) defaultValue else it.asFloat } ?: defaultValue +fun JsonObject?.getChar(key: String, defaultValue: Char): Char = get(key)?.let { if(!it.isJsonPrimitive) defaultValue else it.asCharacter } ?: defaultValue fun JsonObject?.getJsonObject(key: String, defaultValue: JsonObject): JsonObject = get(key)?.let { if (it.isJsonObject) it.asJsonObject else defaultValue } ?: defaultValue fun JsonObject?.getJsonArray(key: String, defaultValue: JsonArray): JsonArray = get(key)?.let { if (it.isJsonArray) it.asJsonArray else defaultValue } ?: defaultValue -fun JsonArray.getBoolean(key: Int): Boolean? = if (key >= size()) null else get(key)?.let { if (it.isJsonNull) null else it.asBoolean } -fun JsonArray.getString(key: Int): String? = if (key >= size()) null else get(key)?.let { if (it.isJsonNull) null else it.asString } -fun JsonArray.getInt(key: Int): Int? = if (key >= size()) null else get(key)?.let { if (it.isJsonNull) null else it.asInt } -fun JsonArray.getLong(key: Int): Long? = if (key >= size()) null else get(key)?.let { if (it.isJsonNull) null else it.asLong } -fun JsonArray.getFloat(key: Int): Float? = if (key >= size()) null else get(key)?.let { if(it.isJsonNull) null else it.asFloat } -fun JsonArray.getChar(key: Int): Char? = if (key >= size()) null else get(key)?.let { if(it.isJsonNull) null else it.asCharacter } +fun JsonArray.getBoolean(key: Int): Boolean? = if (key >= size()) null else get(key)?.let { if (!it.isJsonPrimitive) null else it.asBoolean } +fun JsonArray.getString(key: Int): String? = if (key >= size()) null else get(key)?.let { if (!it.isJsonPrimitive) null else it.asString } +fun JsonArray.getInt(key: Int): Int? = if (key >= size()) null else get(key)?.let { if (!it.isJsonPrimitive) null else it.asInt } +fun JsonArray.getLong(key: Int): Long? = if (key >= size()) null else get(key)?.let { if (!it.isJsonPrimitive) null else it.asLong } +fun JsonArray.getFloat(key: Int): Float? = if (key >= size()) null else get(key)?.let { if(!it.isJsonPrimitive) null else it.asFloat } +fun JsonArray.getChar(key: Int): Char? = if (key >= size()) null else get(key)?.let { if(!it.isJsonPrimitive) null else it.asCharacter } fun JsonArray.getJsonObject(key: Int): JsonObject? = if (key >= size()) null else get(key)?.let { if (it.isJsonObject) it.asJsonObject else null } fun JsonArray.getJsonArray(key: Int): JsonArray? = if (key >= size()) null else get(key)?.let { if (it.isJsonArray) it.asJsonArray else null } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt index 0bbc77f7..d5a1adde 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt @@ -38,6 +38,7 @@ import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import pl.droidsonroids.gif.GifDrawable +import pl.szczodrzynski.edziennik.data.api.ERROR_VULCAN_API_DEPRECATED import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.api.events.* import pl.szczodrzynski.edziennik.data.api.models.ApiError @@ -64,6 +65,7 @@ import pl.szczodrzynski.edziennik.ui.modules.base.MainSnackbar import pl.szczodrzynski.edziennik.ui.modules.behaviour.BehaviourFragment import pl.szczodrzynski.edziennik.ui.modules.debug.DebugFragment import pl.szczodrzynski.edziennik.ui.modules.debug.LabFragment +import pl.szczodrzynski.edziennik.ui.modules.error.ErrorDetailsDialog import pl.szczodrzynski.edziennik.ui.modules.error.ErrorSnackbar import pl.szczodrzynski.edziennik.ui.modules.feedback.FeedbackFragment import pl.szczodrzynski.edziennik.ui.modules.feedback.HelpFragment @@ -756,6 +758,9 @@ class MainActivity : AppCompatActivity(), CoroutineScope { } mainSnackbar.dismiss() errorSnackbar.addError(event.error).show() + if (event.error.errorCode == ERROR_VULCAN_API_DEPRECATED) { + ErrorDetailsDialog(this, listOf(event.error)) + } } @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) fun onAppManagerDetectedEvent(event: AppManagerDetectedEvent) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigSync.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigSync.kt index 2d7dcf78..068400a1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigSync.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigSync.kt @@ -99,6 +99,10 @@ class ConfigSync(private val config: Config) { var tokenVulcan: String? get() { mTokenVulcan = mTokenVulcan ?: config.values.get("tokenVulcan", null as String?); return mTokenVulcan } set(value) { config.set("tokenVulcan", value); mTokenVulcan = value } + private var mTokenVulcanHebe: String? = null + var tokenVulcanHebe: String? + get() { mTokenVulcanHebe = mTokenVulcanHebe ?: config.values.get("tokenVulcanHebe", null as String?); return mTokenVulcanHebe } + set(value) { config.set("tokenVulcanHebe", value); mTokenVulcanHebe = value } private var mTokenMobidziennikList: List? = null var tokenMobidziennikList: List @@ -112,6 +116,10 @@ class ConfigSync(private val config: Config) { var tokenVulcanList: List get() { mTokenVulcanList = mTokenVulcanList ?: config.values.getIntList("tokenVulcanList", listOf()); return mTokenVulcanList ?: listOf() } set(value) { config.set("tokenVulcanList", value); mTokenVulcanList = value } + private var mTokenVulcanHebeList: List? = null + var tokenVulcanHebeList: List + get() { mTokenVulcanHebeList = mTokenVulcanHebeList ?: config.values.getIntList("tokenVulcanHebeList", listOf()); return mTokenVulcanHebeList ?: listOf() } + set(value) { config.set("tokenVulcanHebeList", value); mTokenVulcanHebeList = value } private var mRegisterAvailability: Map? = null var registerAvailability: Map diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt index 5bc08159..614d5f3a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt @@ -95,7 +95,16 @@ const val VULCAN_API_APP_NAME = "VULCAN-Android-ModulUcznia" const val VULCAN_API_APP_VERSION = "20.5.1.470" const val VULCAN_API_PASSWORD = "CE75EA598C7743AD9B0B7328DED85B06" const val VULCAN_API_PASSWORD_FAKELOG = "012345678901234567890123456789AB" -val VULCAN_API_DEVICE_NAME = "Szkolny.eu ${Build.MODEL}" +const val VULCAN_HEBE_USER_AGENT = "Dart/2.10 (dart:io)" +const val VULCAN_HEBE_APP_NAME = "DzienniczekPlus 2.0" +const val VULCAN_HEBE_APP_VERSION = "21.02.09 (G)" +private const val VULCAN_API_DEVICE_NAME_PREFIX = "Szkolny.eu " +private const val VULCAN_API_DEVICE_NAME_SUFFIX = " - nie usuwać" +val VULCAN_API_DEVICE_NAME by lazy { + val base = "$VULCAN_API_DEVICE_NAME_PREFIX${Build.MODEL}" + val baseMaxLength = 50 - VULCAN_API_DEVICE_NAME_SUFFIX.length + base.take(baseMaxLength) + VULCAN_API_DEVICE_NAME_SUFFIX +} const val VULCAN_API_ENDPOINT_CERTIFICATE = "mobile-api/Uczen.v3.UczenStart/Certyfikat" const val VULCAN_API_ENDPOINT_STUDENT_LIST = "mobile-api/Uczen.v3.UczenStart/ListaUczniow" @@ -116,6 +125,17 @@ const val VULCAN_API_ENDPOINT_MESSAGES_ATTACHMENTS = "mobile-api/Uczen.v3.Uczen/ const val VULCAN_API_ENDPOINT_HOMEWORK_ATTACHMENTS = "mobile-api/Uczen.v3.Uczen/ZadaniaDomoweZalacznik" const val VULCAN_WEB_ENDPOINT_LUCKY_NUMBER = "Start.mvc/GetKidsLuckyNumbers" const val VULCAN_WEB_ENDPOINT_REGISTER_DEVICE = "RejestracjaUrzadzeniaToken.mvc/Get" +const val VULCAN_HEBE_ENDPOINT_REGISTER_NEW = "api/mobile/register/new" +const val VULCAN_HEBE_ENDPOINT_MAIN = "api/mobile/register/hebe" +const val VULCAN_HEBE_ENDPOINT_TIMETABLE = "api/mobile/schedule" +const val VULCAN_HEBE_ENDPOINT_TIMETABLE_CHANGES = "api/mobile/schedule/changes" +const val VULCAN_HEBE_ENDPOINT_ADDRESSBOOK = "api/mobile/addressbook" +const val VULCAN_HEBE_ENDPOINT_EXAMS = "api/mobile/exam" +const val VULCAN_HEBE_ENDPOINT_GRADES = "api/mobile/grade" +const val VULCAN_HEBE_ENDPOINT_HOMEWORK = "api/mobile/homework" +const val VULCAN_HEBE_ENDPOINT_ATTENDANCE = "api/mobile/lesson" +const val VULCAN_HEBE_ENDPOINT_MESSAGES = "api/mobile/message" +const val VULCAN_HEBE_ENDPOINT_MESSAGES_STATUS = "api/mobile/message/status" const val EDUDZIENNIK_USER_AGENT = "Szkolny.eu/${BuildConfig.VERSION_NAME}" diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt index 8301f62f..6809ed2e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt @@ -170,6 +170,8 @@ const val ERROR_VULCAN_WEB_LOGGED_OUT = 350 const val ERROR_VULCAN_WEB_CERTIFICATE_POST_FAILED = 351 const val ERROR_VULCAN_WEB_GRADUATE_ACCOUNT = 352 const val ERROR_VULCAN_WEB_NO_SCHOOLS = 353 +const val ERROR_VULCAN_HEBE_OTHER = 354 +const val ERROR_VULCAN_API_DEPRECATED = 390 const val ERROR_LOGIN_IDZIENNIK_WEB_INVALID_LOGIN = 401 const val ERROR_LOGIN_IDZIENNIK_WEB_INVALID_SCHOOL_NAME = 402 @@ -229,5 +231,6 @@ const val ERROR_ONEDRIVE_DOWNLOAD = 930 const val EXCEPTION_VULCAN_WEB_LOGIN = 931 const val EXCEPTION_VULCAN_WEB_REQUEST = 932 const val EXCEPTION_PODLASIE_API_REQUEST = 940 +const val EXCEPTION_VULCAN_HEBE_REQUEST = 950 const val LOGIN_NO_ARGUMENTS = 1201 diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/LoginMethods.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/LoginMethods.kt index 24444a58..d70dd54e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/LoginMethods.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/LoginMethods.kt @@ -17,6 +17,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.login.PodlasieLogi import pl.szczodrzynski.edziennik.data.api.edziennik.template.login.TemplateLoginApi import pl.szczodrzynski.edziennik.data.api.edziennik.template.login.TemplateLoginWeb import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginApi +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginHebe import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginWebMain import pl.szczodrzynski.edziennik.data.api.models.LoginMethod @@ -98,11 +99,13 @@ val mobidziennikLoginMethods = listOf( const val LOGIN_TYPE_VULCAN = 4 const val LOGIN_MODE_VULCAN_API = 0 const val LOGIN_MODE_VULCAN_WEB = 1 +const val LOGIN_MODE_VULCAN_HEBE = 2 const val LOGIN_METHOD_VULCAN_WEB_MAIN = 100 const val LOGIN_METHOD_VULCAN_WEB_NEW = 200 const val LOGIN_METHOD_VULCAN_WEB_OLD = 300 const val LOGIN_METHOD_VULCAN_WEB_MESSAGES = 400 const val LOGIN_METHOD_VULCAN_API = 500 +const val LOGIN_METHOD_VULCAN_HEBE = 600 val vulcanLoginMethods = listOf( LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_WEB_MAIN, VulcanLoginWebMain::class.java) .withIsPossible { _, loginStore -> loginStore.hasLoginData("webHost") } @@ -117,9 +120,19 @@ val vulcanLoginMethods = listOf( .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_VULCAN_WEB_MAIN },*/ LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_API, VulcanLoginApi::class.java) - .withIsPossible { _, _ -> true } + .withIsPossible { _, loginStore -> + loginStore.mode != LOGIN_MODE_VULCAN_HEBE + } .withRequiredLoginMethod { _, loginStore -> if (loginStore.mode == LOGIN_MODE_VULCAN_WEB) LOGIN_METHOD_VULCAN_WEB_MAIN else LOGIN_METHOD_NOT_NEEDED + }, + + LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_HEBE, VulcanLoginHebe::class.java) + .withIsPossible { _, loginStore -> + loginStore.mode != LOGIN_MODE_VULCAN_API + } + .withRequiredLoginMethod { _, loginStore -> + if (loginStore.mode == LOGIN_MODE_VULCAN_WEB) LOGIN_METHOD_VULCAN_WEB_MAIN else LOGIN_METHOD_NOT_NEEDED } ) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/DataVulcan.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/DataVulcan.kt index 882a2fa4..90ba3dd6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/DataVulcan.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/DataVulcan.kt @@ -4,16 +4,16 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan -import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.currentTimeUnix +import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_API +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_HEBE +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_WEB_MAIN +import pl.szczodrzynski.edziennik.data.api.LOGIN_MODE_VULCAN_API import pl.szczodrzynski.edziennik.data.api.models.Data import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Team -import pl.szczodrzynski.edziennik.isNotNullNorEmpty import pl.szczodrzynski.edziennik.utils.Utils -import pl.szczodrzynski.edziennik.values class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) { @@ -26,17 +26,27 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app && apiFingerprint[symbol].isNotNullNorEmpty() && apiPrivateKey[symbol].isNotNullNorEmpty() && symbol.isNotNullNorEmpty() + fun isHebeLoginValid() = hebePublicKey.isNotNullNorEmpty() + && hebePrivateKey.isNotNullNorEmpty() + && symbol.isNotNullNorEmpty() override fun satisfyLoginMethods() { loginMethods.clear() + if (isWebMainLoginValid()) { + loginMethods += LOGIN_METHOD_VULCAN_WEB_MAIN + } if (isApiLoginValid()) { loginMethods += LOGIN_METHOD_VULCAN_API } + if (isHebeLoginValid()) { + loginMethods += LOGIN_METHOD_VULCAN_HEBE + } } init { // during the first sync `profile.studentClassName` is already set - if (teamList.values().none { it.type == Team.TYPE_CLASS }) { + if (loginStore.mode == LOGIN_MODE_VULCAN_API + && teamList.values().none { it.type == Team.TYPE_CLASS }) { profile?.studentClassName?.also { name -> val id = Utils.crc16(name.toByteArray()).toLong() @@ -55,6 +65,17 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app override fun generateUserCode() = "$schoolCode:$studentId" + fun buildDeviceId(): String { + val deviceId = app.deviceId.padStart(16, '0') + val loginStoreId = loginStore.id.toString(16).padStart(4, '0') + val symbol = symbol?.crc16()?.toString(16)?.take(2) ?: "00" + return deviceId.substring(0..7) + + "-" + deviceId.substring(8..11) + + "-" + deviceId.substring(12..15) + + "-" + loginStoreId + + "-" + symbol + "6f72616e7a" + } + /** * A UONET+ client symbol. * @@ -139,6 +160,16 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app get() { mStudentSemesterId = mStudentSemesterId ?: profile?.getStudentData("studentSemesterId", 0); return mStudentSemesterId ?: 0 } set(value) { profile?.putStudentData("studentSemesterId", value) ?: return; mStudentSemesterId = value } + private var mStudentUnitId: Int? = null + var studentUnitId: Int + get() { mStudentUnitId = mStudentUnitId ?: profile?.getStudentData("studentUnitId", 0); return mStudentUnitId ?: 0 } + set(value) { profile?.putStudentData("studentUnitId", value) ?: return; mStudentUnitId = value } + + private var mStudentConstituentId: Int? = null + var studentConstituentId: Int + get() { mStudentConstituentId = mStudentConstituentId ?: profile?.getStudentData("studentConstituentId", 0); return mStudentConstituentId ?: 0 } + set(value) { profile?.putStudentData("studentConstituentId", value) ?: return; mStudentConstituentId = value } + private var mSemester1Id: Int? = null var semester1Id: Int get() { mSemester1Id = mSemester1Id ?: profile?.getStudentData("semester1Id", 0); return mSemester1Id ?: 0 } @@ -203,6 +234,32 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app get() { mApiPrivateKey = mApiPrivateKey ?: loginStore.getLoginData("apiPrivateKey", null)?.let { app.gson.fromJson(it, field.toMutableMap()::class.java) }; return mApiPrivateKey ?: mapOf() } set(value) { loginStore.putLoginData("apiPrivateKey", app.gson.toJson(value)); mApiPrivateKey = value } + /* _ _ _ _____ _____ + | | | | | | /\ | __ \_ _| + | |__| | ___| |__ ___ / \ | |__) || | + | __ |/ _ \ '_ \ / _ \ / /\ \ | ___/ | | + | | | | __/ |_) | __/ / ____ \| | _| |_ + |_| |_|\___|_.__/ \___| /_/ \_\_| |____*/ + private var mHebePublicKey: String? = null + var hebePublicKey: String? + get() { mHebePublicKey = mHebePublicKey ?: loginStore.getLoginData("hebePublicKey", null); return mHebePublicKey } + set(value) { loginStore.putLoginData("hebePublicKey", value); mHebePublicKey = value } + + private var mHebePrivateKey: String? = null + var hebePrivateKey: String? + get() { mHebePrivateKey = mHebePrivateKey ?: loginStore.getLoginData("hebePrivateKey", null); return mHebePrivateKey } + set(value) { loginStore.putLoginData("hebePrivateKey", value); mHebePrivateKey = value } + + private var mHebePublicHash: String? = null + var hebePublicHash: String? + get() { mHebePublicHash = mHebePublicHash ?: loginStore.getLoginData("hebePublicHash", null); return mHebePublicHash } + set(value) { loginStore.putLoginData("hebePublicHash", value); mHebePublicHash = value } + + private var mHebeContext: String? = null + var hebeContext: String? + get() { mHebeContext = mHebeContext ?: profile?.getStudentData("hebeContext", null); return mHebeContext } + set(value) { profile?.putStudentData("hebeContext", value) ?: return; mHebeContext = value } + val apiUrl: String? get() { val url = when (apiToken[symbol]?.substring(0, 3)) { @@ -227,7 +284,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app return if (url != null) "$url/$symbol/" else loginStore.getLoginData("apiUrl", null) } - val fullApiUrl: String? + val fullApiUrl: String get() { return "$apiUrl$schoolSymbol/" } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/Vulcan.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/Vulcan.kt index 9761343c..9dfc1572 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/Vulcan.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/Vulcan.kt @@ -13,6 +13,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanData import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api.VulcanApiAttachments import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api.VulcanApiMessagesChangeStatus import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api.VulcanApiSendMessage +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe.VulcanHebeMessagesChangeStatus import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.firstlogin.VulcanFirstLogin import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLogin import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent @@ -91,6 +92,20 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va } override fun getMessage(message: MessageFull) { + if (loginStore.mode != LOGIN_MODE_VULCAN_API) { + login(LOGIN_METHOD_VULCAN_HEBE) { + if (message.seen) { + EventBus.getDefault().postSticky(MessageGetEvent(message)) + completed() + return@login + } + VulcanHebeMessagesChangeStatus(data, message) { + completed() + } + } + return + } + login(LOGIN_METHOD_VULCAN_API) { if (message.attachmentIds != null) { VulcanApiMessagesChangeStatus(data, message) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/VulcanFeatures.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/VulcanFeatures.kt index ab032e0c..9e247575 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/VulcanFeatures.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/VulcanFeatures.kt @@ -20,25 +20,46 @@ const val ENDPOINT_VULCAN_API_ATTENDANCE = 1080 const val ENDPOINT_VULCAN_API_MESSAGES_INBOX = 1090 const val ENDPOINT_VULCAN_API_MESSAGES_SENT = 1100 const val ENDPOINT_VULCAN_WEB_LUCKY_NUMBERS = 2010 +const val ENDPOINT_VULCAN_HEBE_MAIN = 3000 +const val ENDPOINT_VULCAN_HEBE_ADDRESSBOOK = 3010 +const val ENDPOINT_VULCAN_HEBE_TIMETABLE = 3020 +const val ENDPOINT_VULCAN_HEBE_EXAMS = 3030 +const val ENDPOINT_VULCAN_HEBE_GRADES = 3040 +const val ENDPOINT_VULCAN_HEBE_HOMEWORK = 3060 +const val ENDPOINT_VULCAN_HEBE_ATTENDANCE = 3080 +const val ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX = 3090 +const val ENDPOINT_VULCAN_HEBE_MESSAGES_SENT = 3100 val VulcanFeatures = listOf( // timetable Feature(LOGIN_TYPE_VULCAN, FEATURE_TIMETABLE, listOf( ENDPOINT_VULCAN_API_TIMETABLE to LOGIN_METHOD_VULCAN_API ), listOf(LOGIN_METHOD_VULCAN_API)), + Feature(LOGIN_TYPE_VULCAN, FEATURE_TIMETABLE, listOf( + ENDPOINT_VULCAN_HEBE_TIMETABLE to LOGIN_METHOD_VULCAN_HEBE + ), listOf(LOGIN_METHOD_VULCAN_HEBE)), // agenda Feature(LOGIN_TYPE_VULCAN, FEATURE_AGENDA, listOf( ENDPOINT_VULCAN_API_EVENTS to LOGIN_METHOD_VULCAN_API ), listOf(LOGIN_METHOD_VULCAN_API)), + Feature(LOGIN_TYPE_VULCAN, FEATURE_AGENDA, listOf( + ENDPOINT_VULCAN_HEBE_EXAMS to LOGIN_METHOD_VULCAN_HEBE + ), listOf(LOGIN_METHOD_VULCAN_HEBE)), // grades Feature(LOGIN_TYPE_VULCAN, FEATURE_GRADES, listOf( ENDPOINT_VULCAN_API_GRADES to LOGIN_METHOD_VULCAN_API, ENDPOINT_VULCAN_API_GRADES_SUMMARY to LOGIN_METHOD_VULCAN_API ), listOf(LOGIN_METHOD_VULCAN_API)), + Feature(LOGIN_TYPE_VULCAN, FEATURE_GRADES, listOf( + ENDPOINT_VULCAN_HEBE_GRADES to LOGIN_METHOD_VULCAN_HEBE + ), listOf(LOGIN_METHOD_VULCAN_HEBE)), // homework Feature(LOGIN_TYPE_VULCAN, FEATURE_HOMEWORK, listOf( ENDPOINT_VULCAN_API_HOMEWORK to LOGIN_METHOD_VULCAN_API ), listOf(LOGIN_METHOD_VULCAN_API)), + Feature(LOGIN_TYPE_VULCAN, FEATURE_HOMEWORK, listOf( + ENDPOINT_VULCAN_HEBE_HOMEWORK to LOGIN_METHOD_VULCAN_HEBE + ), listOf(LOGIN_METHOD_VULCAN_HEBE)), // behaviour Feature(LOGIN_TYPE_VULCAN, FEATURE_BEHAVIOUR, listOf( ENDPOINT_VULCAN_API_NOTICES to LOGIN_METHOD_VULCAN_API @@ -47,6 +68,9 @@ val VulcanFeatures = listOf( Feature(LOGIN_TYPE_VULCAN, FEATURE_ATTENDANCE, listOf( ENDPOINT_VULCAN_API_ATTENDANCE to LOGIN_METHOD_VULCAN_API ), listOf(LOGIN_METHOD_VULCAN_API)), + Feature(LOGIN_TYPE_VULCAN, FEATURE_ATTENDANCE, listOf( + ENDPOINT_VULCAN_HEBE_ATTENDANCE to LOGIN_METHOD_VULCAN_HEBE + ), listOf(LOGIN_METHOD_VULCAN_HEBE)), // messages Feature(LOGIN_TYPE_VULCAN, FEATURE_MESSAGES_INBOX, listOf( ENDPOINT_VULCAN_API_MESSAGES_INBOX to LOGIN_METHOD_VULCAN_API @@ -54,6 +78,12 @@ val VulcanFeatures = listOf( Feature(LOGIN_TYPE_VULCAN, FEATURE_MESSAGES_SENT, listOf( ENDPOINT_VULCAN_API_MESSAGES_SENT to LOGIN_METHOD_VULCAN_API ), listOf(LOGIN_METHOD_VULCAN_API)), + Feature(LOGIN_TYPE_VULCAN, FEATURE_MESSAGES_INBOX, listOf( + ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX to LOGIN_METHOD_VULCAN_HEBE + ), listOf(LOGIN_METHOD_VULCAN_HEBE)), + Feature(LOGIN_TYPE_VULCAN, FEATURE_MESSAGES_SENT, listOf( + ENDPOINT_VULCAN_HEBE_MESSAGES_SENT to LOGIN_METHOD_VULCAN_HEBE + ), listOf(LOGIN_METHOD_VULCAN_HEBE)), // push config Feature(LOGIN_TYPE_VULCAN, FEATURE_PUSH_CONFIG, listOf( @@ -72,7 +102,11 @@ val VulcanFeatures = listOf( Feature(LOGIN_TYPE_VULCAN, FEATURE_ALWAYS_NEEDED, listOf( ENDPOINT_VULCAN_API_UPDATE_SEMESTER to LOGIN_METHOD_VULCAN_API, ENDPOINT_VULCAN_API_DICTIONARIES to LOGIN_METHOD_VULCAN_API - ), listOf(LOGIN_METHOD_VULCAN_API)) + ), listOf(LOGIN_METHOD_VULCAN_API)), + Feature(LOGIN_TYPE_VULCAN, FEATURE_ALWAYS_NEEDED, listOf( + ENDPOINT_VULCAN_HEBE_MAIN to LOGIN_METHOD_VULCAN_HEBE, + ENDPOINT_VULCAN_HEBE_ADDRESSBOOK to LOGIN_METHOD_VULCAN_HEBE + ), listOf(LOGIN_METHOD_VULCAN_HEBE)) /*Feature(LOGIN_TYPE_VULCAN, FEATURE_STUDENT_INFO, listOf( ENDPOINT_VULCAN_API to LOGIN_METHOD_VULCAN_WEB ), listOf(LOGIN_METHOD_VULCAN_WEB)), diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanData.kt index 491e8ea6..2fc21190 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanData.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanData.kt @@ -5,9 +5,13 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.ERROR_VULCAN_API_DEPRECATED +import pl.szczodrzynski.edziennik.data.api.LOGIN_MODE_VULCAN_API import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.* import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe.* import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.web.VulcanWebLuckyNumber +import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.utils.Utils class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) { @@ -15,9 +19,35 @@ class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) { private const val TAG = "VulcanData" } - init { - nextEndpoint(onSuccess) - } + private var firstSemesterSync = false + private val firstSemesterSyncExclude = listOf( + ENDPOINT_VULCAN_HEBE_MAIN, + ENDPOINT_VULCAN_HEBE_ADDRESSBOOK, + ENDPOINT_VULCAN_HEBE_TIMETABLE, + ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX, + ENDPOINT_VULCAN_HEBE_MESSAGES_SENT + ) + + init { run { + if (data.loginStore.mode == LOGIN_MODE_VULCAN_API) { + data.error(TAG, ERROR_VULCAN_API_DEPRECATED) + return@run + } + if (data.studentSemesterNumber == 2 && data.profile?.empty != false) { + firstSemesterSync = true + // set to sync 1st semester first + data.studentSemesterId = data.semester1Id + data.studentSemesterNumber = 1 + } + nextEndpoint { + if (firstSemesterSync) { + // at the end, set back 2nd semester + data.studentSemesterId = data.semester2Id + data.studentSemesterNumber = 2 + } + onSuccess() + } + }} private fun nextEndpoint(onSuccess: () -> Unit) { if (data.targetEndpointIds.isEmpty()) { @@ -30,7 +60,21 @@ class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) { } val id = data.targetEndpointIds.firstKey() val lastSync = data.targetEndpointIds.remove(id) - useEndpoint(id, lastSync) { endpointId -> + useEndpoint(id, lastSync) { + if (firstSemesterSync && id !in firstSemesterSyncExclude) { + // sync 2nd semester after every endpoint + data.studentSemesterId = data.semester2Id + data.studentSemesterNumber = 2 + useEndpoint(id, lastSync) { + // set 1st semester back for the next endpoint + data.studentSemesterId = data.semester1Id + data.studentSemesterNumber = 1 + // progress further + data.progress(data.progressStep) + nextEndpoint(onSuccess) + } + return@useEndpoint + } data.progress(data.progressStep) nextEndpoint(onSuccess) } @@ -91,6 +135,51 @@ class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) { data.startProgress(R.string.edziennik_progress_endpoint_lucky_number) VulcanWebLuckyNumber(data, lastSync, onSuccess) } + ENDPOINT_VULCAN_HEBE_MAIN -> { + if (data.profile == null) { + onSuccess(ENDPOINT_VULCAN_HEBE_MAIN) + return + } + data.startProgress(R.string.edziennik_progress_endpoint_student_info) + VulcanHebeMain(data, lastSync).getStudents( + profile = data.profile, + profileList = null + ) { + onSuccess(ENDPOINT_VULCAN_HEBE_MAIN) + } + } + ENDPOINT_VULCAN_HEBE_ADDRESSBOOK -> { + data.startProgress(R.string.edziennik_progress_endpoint_teachers) + VulcanHebeAddressbook(data, lastSync, onSuccess) + } + ENDPOINT_VULCAN_HEBE_TIMETABLE -> { + data.startProgress(R.string.edziennik_progress_endpoint_timetable) + VulcanHebeTimetable(data, lastSync, onSuccess) + } + ENDPOINT_VULCAN_HEBE_EXAMS -> { + data.startProgress(R.string.edziennik_progress_endpoint_exams) + VulcanHebeExams(data, lastSync, onSuccess) + } + ENDPOINT_VULCAN_HEBE_GRADES -> { + data.startProgress(R.string.edziennik_progress_endpoint_grades) + VulcanHebeGrades(data, lastSync, onSuccess) + } + ENDPOINT_VULCAN_HEBE_HOMEWORK -> { + data.startProgress(R.string.edziennik_progress_endpoint_homework) + VulcanHebeHomework(data, lastSync, onSuccess) + } + ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX -> { + data.startProgress(R.string.edziennik_progress_endpoint_messages_inbox) + VulcanHebeMessages(data, lastSync, onSuccess).getMessages(Message.TYPE_RECEIVED) + } + ENDPOINT_VULCAN_HEBE_MESSAGES_SENT -> { + data.startProgress(R.string.edziennik_progress_endpoint_messages_outbox) + VulcanHebeMessages(data, lastSync, onSuccess).getMessages(Message.TYPE_SENT) + } + ENDPOINT_VULCAN_HEBE_ATTENDANCE -> { + data.startProgress(R.string.edziennik_progress_endpoint_attendance) + VulcanHebeAttendance(data, lastSync, onSuccess) + } else -> onSuccess(endpointId) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanHebe.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanHebe.kt new file mode 100644 index 00000000..c61140c4 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanHebe.kt @@ -0,0 +1,360 @@ +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data + +import android.os.Build +import androidx.core.util.set +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import im.wangchao.mhttp.Request +import im.wangchao.mhttp.Response +import im.wangchao.mhttp.body.MediaTypeUtils +import im.wangchao.mhttp.callback.JsonCallbackHandler +import io.github.wulkanowy.signer.hebe.getSignatureHeaders +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe.HebeFilterType +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.db.entity.LessonRange +import pl.szczodrzynski.edziennik.data.db.entity.Subject +import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.data.db.entity.Team +import pl.szczodrzynski.edziennik.utils.Utils.d +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time +import java.net.HttpURLConnection +import java.net.URLEncoder +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.util.* + +open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) { + companion object { + const val TAG = "VulcanHebe" + } + + val profileId + get() = data.profile?.id ?: -1 + + val profile + get() = data.profile + + fun getDateTime(json: JsonObject?, key: String, default: Long = System.currentTimeMillis()): Long { + val date = json.getJsonObject(key) + return date.getLong("Timestamp") ?: return default + } + + fun getDate(json: JsonObject?, key: String): Date? { + val date = json.getJsonObject(key) + return date.getString("Date")?.let { Date.fromY_m_d(it) } + } + + fun getTeacherId(json: JsonObject?, key: String): Long? { + val teacher = json.getJsonObject(key) + val teacherId = teacher.getLong("Id") ?: return null + if (data.teacherList[teacherId] == null) { + data.teacherList[teacherId] = Teacher( + data.profileId, + teacherId, + teacher.getString("Name") ?: "", + teacher.getString("Surname") ?: "" + ) + } + return teacherId + } + + fun getSubjectId(json: JsonObject?, key: String): Long? { + val subject = json.getJsonObject(key) + val subjectId = subject.getLong("Id") ?: return null + if (data.subjectList[subjectId] == null) { + data.subjectList[subjectId] = Subject( + data.profileId, + subjectId, + subject.getString("Name") ?: "", + subject.getString("Kod") ?: "" + ) + } + return subjectId + } + + fun getTeamId(json: JsonObject?, key: String): Long? { + val team = json.getJsonObject(key) + val teamId = team.getLong("Id") ?: return null + if (data.teamList[teamId] == null) { + var name = team.getString("Shortcut") + ?: team.getString("Name") + ?: "" + name = "${profile?.studentClassName ?: ""} $name" + data.teamList[teamId] = Team( + data.profileId, + teamId, + name, + Team.TYPE_VIRTUAL, + "${data.schoolCode}:$name", + -1 + ) + } + return teamId + } + + fun getClassId(json: JsonObject?, key: String): Long? { + val team = json.getJsonObject(key) + val teamId = team.getLong("Id") ?: return null + if (data.teamList[teamId] == null) { + val name = data.profile?.studentClassName + ?: team.getString("Name") + ?: "" + data.teamList[teamId] = Team( + data.profileId, + teamId, + name, + Team.TYPE_CLASS, + "${data.schoolCode}:$name", + -1 + ) + } + return teamId + } + + fun getLessonRange(json: JsonObject?, key: String): LessonRange? { + val timeslot = json.getJsonObject(key) + val position = timeslot.getInt("Position") ?: return null + val start = timeslot.getString("Start") ?: return null + val end = timeslot.getString("End") ?: return null + val lessonRange = LessonRange( + data.profileId, + position, + Time.fromH_m(start), + Time.fromH_m(end) + ) + data.lessonRanges[position] = lessonRange + return lessonRange + } + + fun getSemester(json: JsonObject?): Int { + val periodId = json.getInt("PeriodId") ?: return 1 + return if (periodId == data.semester1Id) + 1 + else + 2 + } + + inline fun apiRequest( + tag: String, + endpoint: String, + method: Int = GET, + payload: JsonElement? = null, + baseUrl: Boolean = false, + firebaseToken: String? = null, + crossinline onSuccess: (json: T, response: Response?) -> Unit + ) { + val url = "${if (baseUrl) data.apiUrl else data.fullApiUrl}$endpoint" + + d(tag, "Request: Vulcan/Hebe - $url") + + val privateKey = data.hebePrivateKey + val publicHash = data.hebePublicHash + + if (privateKey == null || publicHash == null) { + data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING)) + return + } + + val timestamp = ZonedDateTime.now(ZoneId.of("GMT")) + val timestampMillis = timestamp.toInstant().toEpochMilli() + val timestampIso = timestamp.format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss")) + + val finalPayload = if (payload != null) { + JsonObject( + "AppName" to VULCAN_HEBE_APP_NAME, + "AppVersion" to VULCAN_HEBE_APP_VERSION, + "CertificateId" to publicHash, + "Envelope" to payload, + "FirebaseToken" to (firebaseToken ?: data.app.config.sync.tokenVulcanHebe), + "API" to 1, + "RequestId" to UUID.randomUUID().toString(), + "Timestamp" to timestampMillis, + "TimestampFormatted" to timestampIso + ) + } else null + val jsonString = finalPayload?.toString() + + val headers = getSignatureHeaders( + publicHash, + privateKey, + jsonString, + endpoint, + timestamp + ) + + val callback = object : JsonCallbackHandler() { + override fun onSuccess(json: JsonObject?, response: Response?) { + if (json == null) { + data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY) + .withResponse(response) + ) + return + } + + val status = json.getJsonObject("Status") + if (status?.getInt("Code") != 0) { + data.error(ApiError(tag, ERROR_VULCAN_HEBE_OTHER) + .withResponse(response) + .withApiResponse(json.toString())) + } + + val envelope = when (T::class.java) { + JsonObject::class.java -> json.getJsonObject("Envelope") as T + JsonArray::class.java -> json.getJsonArray("Envelope") as T + java.lang.Boolean::class.java -> json.getBoolean("Envelope") as T + else -> { + data.error(ApiError(tag, ERROR_RESPONSE_EMPTY) + .withResponse(response) + .withApiResponse(json) + ) + return + } + } + + try { + onSuccess(envelope, response) + } catch (e: Exception) { + data.error(ApiError(tag, EXCEPTION_VULCAN_HEBE_REQUEST) + .withResponse(response) + .withThrowable(e) + .withApiResponse(json) + ) + } + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + data.error(ApiError(tag, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable) + ) + } + } + + Request.builder() + .url(url) + .userAgent(VULCAN_HEBE_USER_AGENT) + .addHeader("vOS", "Android") + .addHeader("vDeviceModel", Build.MODEL) + .addHeader("vAPI", "1") + .apply { + if (data.hebeContext != null) + addHeader("vContext", data.hebeContext) + headers.forEach { + addHeader(it.key, it.value) + } + when (method) { + GET -> get() + POST -> { + post() + setTextBody(jsonString, MediaTypeUtils.APPLICATION_JSON) + } + } + } + .allowErrorCode(HttpURLConnection.HTTP_BAD_REQUEST) + .allowErrorCode(HttpURLConnection.HTTP_FORBIDDEN) + .allowErrorCode(HttpURLConnection.HTTP_UNAUTHORIZED) + .allowErrorCode(HttpURLConnection.HTTP_UNAVAILABLE) + .callback(callback) + .build() + .enqueue() + } + + inline fun apiGet( + tag: String, + endpoint: String, + query: Map = mapOf(), + baseUrl: Boolean = false, + firebaseToken: String? = null, + crossinline onSuccess: (json: T, response: Response?) -> Unit + ) { + val queryPath = query.map { + it.key + "=" + URLEncoder.encode(it.value, "UTF-8").replace("+", "%20") + }.join("&") + apiRequest( + tag, + if (query.isNotEmpty()) "$endpoint?$queryPath" else endpoint, + baseUrl = baseUrl, + firebaseToken = firebaseToken, + onSuccess = onSuccess + ) + } + + inline fun apiPost( + tag: String, + endpoint: String, + payload: JsonElement, + baseUrl: Boolean = false, + firebaseToken: String? = null, + crossinline onSuccess: (json: T, response: Response?) -> Unit + ) { + apiRequest( + tag, + endpoint, + method = POST, + payload, + baseUrl = baseUrl, + firebaseToken = firebaseToken, + onSuccess = onSuccess + ) + } + + fun apiGetList( + tag: String, + endpoint: String, + filterType: HebeFilterType? = null, + dateFrom: Date? = null, + dateTo: Date? = null, + lastSync: Long? = null, + folder: Int? = null, + params: Map = mapOf(), + includeFilterType: Boolean = true, + onSuccess: (data: List, response: Response?) -> Unit + ) { + val url = if (includeFilterType && filterType != null) + "$endpoint/${filterType.endpoint}" + else endpoint + + val query = params.toMutableMap() + + when (filterType) { + HebeFilterType.BY_PUPIL -> { + query["unitId"] = data.studentUnitId.toString() + query["pupilId"] = data.studentId.toString() + query["periodId"] = data.studentSemesterId.toString() + } + HebeFilterType.BY_PERSON -> { + query["loginId"] = data.studentLoginId.toString() + } + HebeFilterType.BY_PERIOD -> { + query["periodId"] = data.studentSemesterId.toString() + query["pupilId"] = data.studentId.toString() + } + } + + if (dateFrom != null) + query["dateFrom"] = dateFrom.stringY_m_d + if (dateTo != null) + query["dateTo"] = dateTo.stringY_m_d + + if (folder != null) + query["folder"] = folder.toString() + + query["lastId"] = "-2147483648" // don't ask, it's just Vulcan + query["pageSize"] = "500" + query["lastSyncDate"] = LocalDateTime + .ofInstant(Instant.ofEpochMilli(lastSync ?: 0), ZoneId.systemDefault()) + .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + + apiGet(tag, url, query) { json: JsonArray, response -> + onSuccess(json.map { it.asJsonObject }, response) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/HebeFilterType.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/HebeFilterType.kt new file mode 100644 index 00000000..6734ab71 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/HebeFilterType.kt @@ -0,0 +1,7 @@ +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe + +enum class HebeFilterType(val endpoint: String) { + BY_PUPIL("byPupil"), + BY_PERSON("byPerson"), + BY_PERIOD("byPeriod") +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeAddressbook.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeAddressbook.kt new file mode 100644 index 00000000..c96142aa --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeAddressbook.kt @@ -0,0 +1,118 @@ +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe + +import androidx.core.util.set +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_ADDRESSBOOK +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_ADDRESSBOOK +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe +import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_EDUCATOR +import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_OTHER +import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_PARENT +import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_PARENTS_COUNCIL +import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_STUDENT +import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_TEACHER +import kotlin.text.replace + +class VulcanHebeAddressbook( + override val data: DataVulcan, + override val lastSync: Long?, + val onSuccess: (endpointId: Int) -> Unit +) : VulcanHebe(data, lastSync) { + companion object { + const val TAG = "VulcanHebeAddressbook" + } + + private fun String.removeUnitName(unitName: String?): String { + return (unitName ?: data.schoolShort)?.let { + this.replace("($it)", "").trim() + } ?: this + } + + init { + apiGetList( + TAG, + VULCAN_HEBE_ENDPOINT_ADDRESSBOOK, + HebeFilterType.BY_PERSON, + lastSync = lastSync, + includeFilterType = false + ) { list, _ -> + list.forEach { person -> + val id = person.getString("Id") ?: return@forEach + val loginId = person.getString("LoginId") ?: return@forEach + + val idType = id.split("-") + .getOrNull(0) + val idLong = id.split("-") + .getOrNull(1) + ?.toLongOrNull() + ?: return@forEach + + val typeBase = when (idType) { + "e" -> TYPE_TEACHER + "c" -> TYPE_PARENT + "p" -> TYPE_STUDENT + else -> TYPE_OTHER + } + + val name = person.getString("Name") ?: "" + val surname = person.getString("Surname") ?: "" + val namePrefix = "$surname $name - " + + val teacher = data.teacherList[idLong] ?: Teacher( + data.profileId, + idLong, + name, + surname, + loginId + ).also { + data.teacherList[idLong] = it + } + + person.getJsonArray("Roles")?.asJsonObjectList()?.onEach { role -> + var roleText: String? = null + val unitName = role.getString("ConstituentUnitSymbol") + + val personType = when (role.getInt("RoleOrder")) { + 0 -> { /* Wychowawca */ + roleText = role.getString("ClassSymbol") + ?.removeUnitName(unitName) + TYPE_EDUCATOR + } + 1 -> TYPE_TEACHER /* Nauczyciel */ + 2 -> return@onEach /* Pracownik */ + 3 -> { /* Rada rodziców */ + roleText = role.getString("Address") + ?.removeUnitName(unitName) + ?.removePrefix(namePrefix) + ?.trim() + TYPE_PARENTS_COUNCIL + } + 5 -> { + roleText = role.getString("RoleName") + ?.plus(" - ") + ?.plus( + role.getString("Address") + ?.removeUnitName(unitName) + ?.removePrefix(namePrefix) + ?.trim() + ) + TYPE_STUDENT + } + else -> TYPE_OTHER + } + + teacher.setTeacherType(personType) + teacher.typeDescription = roleText + } + + if (teacher.type == 0) + teacher.setTeacherType(typeBase) + } + + data.setSyncNext(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK, 2 * DAY) + onSuccess(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeAttendance.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeAttendance.kt new file mode 100644 index 00000000..3d207388 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeAttendance.kt @@ -0,0 +1,143 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2021-2-21 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe + +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_ATTENDANCE +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_ATTENDANCE +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe +import pl.szczodrzynski.edziennik.data.db.entity.Attendance +import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS + +class VulcanHebeAttendance( + override val data: DataVulcan, + override val lastSync: Long?, + val onSuccess: (endpointId: Int) -> Unit +) : VulcanHebe(data, lastSync) { + + companion object { + const val TAG = "VulcanHebeAttendance" + } + + init { + val semesterNumber = data.studentSemesterNumber + val startDate = profile?.getSemesterStart(semesterNumber) + val endDate = profile?.getSemesterEnd(semesterNumber) + + apiGetList( + TAG, + VULCAN_HEBE_ENDPOINT_ATTENDANCE, + HebeFilterType.BY_PUPIL, + dateFrom = startDate, + dateTo = endDate, + lastSync = lastSync + ) { list, _ -> + list.forEach { attendance -> + val id = attendance.getLong("AuxPresenceId") ?: return@forEach + val type = attendance.getJsonObject("PresenceType") ?: return@forEach + val baseType = getBaseType(type) + val typeName = type.getString("Name") ?: return@forEach + val typeCategoryId = type.getLong("CategoryId") ?: return@forEach + val typeSymbol = type.getString("Symbol") ?: return@forEach + val typeShort = when (typeCategoryId.toInt()) { + 6, 8 -> typeSymbol + else -> data.app.attendanceManager.getTypeShort(baseType) + } + val typeColor = when (typeCategoryId.toInt()) { + 1 -> 0xffffffff // obecność + 2 -> 0xffffa687 // nieobecność + 3 -> 0xfffcc150 // nieobecność usprawiedliwiona + 4 -> 0xffede049 // spóźnienie + 5 -> 0xffbbdd5f // spóźnienie usprawiedliwione + 6 -> 0xffa9c9fd // nieobecny z przyczyn szkolnych + 7 -> 0xffddbbe5 // zwolniony + 8 -> 0xffffffff // usunięty wpis + else -> null + }?.toInt() + val date = getDate(attendance, "Day") ?: return@forEach + val lessonRange = getLessonRange(attendance, "TimeSlot") + val startTime = lessonRange?.startTime + val semester = profile?.dateToSemester(date) ?: return@forEach + val teacherId = attendance.getJsonObject("TeacherPrimary")?.getLong("Id") ?: -1 + val subjectId = attendance.getJsonObject("Subject")?.getLong("Id") ?: -1 + val addedDate = getDateTime(attendance, "DateModify") + val lessonNumber = lessonRange?.lessonNumber + val isCounted = attendance.getBoolean("CalculatePresence") + ?: (baseType != Attendance.TYPE_RELEASED) + + val attendanceObject = Attendance( + profileId = profileId, + id = id, + baseType = baseType, + typeName = typeName, + typeShort = typeShort, + typeSymbol = typeSymbol, + typeColor = typeColor, + date = date, + startTime = startTime, + semester = semester, + teacherId = teacherId, + subjectId = subjectId, + addedDate = addedDate + ).also { + it.lessonNumber = lessonNumber + it.isCounted = isCounted + } + + data.attendanceList.add(attendanceObject) + if (baseType != Attendance.TYPE_PRESENT) { + data.metadataList.add( + Metadata( + profileId, + Metadata.TYPE_ATTENDANCE, + attendanceObject.id, + profile?.empty ?: true + || baseType == Attendance.TYPE_PRESENT_CUSTOM + || baseType == Attendance.TYPE_UNKNOWN, + profile?.empty ?: true + || baseType == Attendance.TYPE_PRESENT_CUSTOM + || baseType == Attendance.TYPE_UNKNOWN + ) + ) + } + } + + data.setSyncNext(ENDPOINT_VULCAN_HEBE_ATTENDANCE, SYNC_ALWAYS) + onSuccess(ENDPOINT_VULCAN_HEBE_ATTENDANCE) + } + } + + fun getBaseType(attendanceType: JsonObject): Int { + val absent = attendanceType.getBoolean("Absence") ?: false + val excused = attendanceType.getBoolean("AbsenceJustified") ?: false + return if (absent) { + if (excused) + Attendance.TYPE_ABSENT_EXCUSED + else + Attendance.TYPE_ABSENT + } else { + val belated = attendanceType.getBoolean("Late") ?: false + val released = attendanceType.getBoolean("LegalAbsence") ?: false + val present = attendanceType.getBoolean("Presence") ?: true + if (belated) + if (excused) + Attendance.TYPE_BELATED_EXCUSED + else + Attendance.TYPE_BELATED + else if (released) + Attendance.TYPE_RELEASED + else if (present) + if (attendanceType.getInt("CategoryId") != 1) + Attendance.TYPE_PRESENT_CUSTOM + else + Attendance.TYPE_PRESENT + else + Attendance.TYPE_UNKNOWN + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeExams.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeExams.kt new file mode 100644 index 00000000..54388a86 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeExams.kt @@ -0,0 +1,78 @@ +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe + +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_EXAMS +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_EXAMS +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe +import pl.szczodrzynski.edziennik.data.db.entity.Event +import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS + +class VulcanHebeExams( + override val data: DataVulcan, + override val lastSync: Long?, + val onSuccess: (endpointId: Int) -> Unit +) : VulcanHebe(data, lastSync) { + companion object { + const val TAG = "VulcanHebeExams" + } + + init { + apiGetList( + TAG, + VULCAN_HEBE_ENDPOINT_EXAMS, + HebeFilterType.BY_PUPIL, + lastSync = lastSync + ) { list, _ -> + list.forEach { exam -> + val id = exam.getLong("Id") ?: return@forEach + val eventDate = getDate(exam, "Deadline") ?: return@forEach + val subjectId = getSubjectId(exam, "Subject") ?: -1 + val teacherId = getTeacherId(exam, "Creator") ?: -1 + val teamId = getTeamId(exam, "Distribution") + ?: getClassId(exam, "Class") + ?: data.teamClass?.id + ?: -1 + val topic = exam.getString("Content")?.trim() ?: "" + + val lessonList = data.db.timetableDao().getAllForDateNow(profileId, eventDate) + val startTime = lessonList.firstOrNull { it.subjectId == subjectId }?.startTime + + val type = when (exam.getString("Type")) { + "Praca klasowa", + "Sprawdzian" -> Event.TYPE_EXAM + "Kartkówka" -> Event.TYPE_SHORT_QUIZ + else -> Event.TYPE_DEFAULT + } + + val eventObject = Event( + profileId = profileId, + id = id, + date = eventDate, + time = startTime, + topic = topic, + color = null, + type = type, + teacherId = teacherId, + subjectId = subjectId, + teamId = teamId + ) + + data.eventList.add(eventObject) + data.metadataList.add( + Metadata( + profileId, + Metadata.TYPE_EVENT, + id, + profile?.empty ?: true, + profile?.empty ?: true + ) + ) + } + + data.setSyncNext(ENDPOINT_VULCAN_HEBE_EXAMS, SYNC_ALWAYS) + onSuccess(ENDPOINT_VULCAN_HEBE_EXAMS) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeGrades.kt new file mode 100644 index 00000000..11a7fdbc --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeGrades.kt @@ -0,0 +1,121 @@ +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe + +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_GRADES +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_GRADES +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe +import pl.szczodrzynski.edziennik.data.db.entity.Grade +import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import java.text.DecimalFormat +import kotlin.math.roundToInt + +class VulcanHebeGrades( + override val data: DataVulcan, + override val lastSync: Long?, + val onSuccess: (endpointId: Int) -> Unit +) : VulcanHebe(data, lastSync) { + companion object { + const val TAG = "VulcanHebeGrades" + } + + init { + apiGetList( + TAG, + VULCAN_HEBE_ENDPOINT_GRADES, + HebeFilterType.BY_PUPIL, + lastSync = lastSync + ) { list, _ -> + list.forEach { grade -> + val id = grade.getLong("Id") ?: return@forEach + + val column = grade.getJsonObject("Column") + val category = column.getJsonObject("Category") + val categoryText = category.getString("Name") + + val teacherId = getTeacherId(grade, "Creator") ?: -1 + val subjectId = getSubjectId(column, "Subject") ?: -1 + + val description = column.getString("Name") + val comment = grade.getString("Comment") + var value = grade.getFloat("Value") + var weight = column.getFloat("Weight") ?: 0.0f + val numerator = grade.getFloat("Numerator ") + val denominator = grade.getFloat("Denominator") + val addedDate = getDateTime(grade, "DateModify") + + var finalDescription = "" + + var name = when (numerator != null && denominator != null) { + true -> { + value = numerator / denominator + finalDescription += DecimalFormat("#.##").format(numerator) + + "/" + DecimalFormat("#.##").format(denominator) + weight = 0.0f + (value * 100).roundToInt().toString() + "%" + } + else -> { + if (value == null) weight = 0.0f + + grade.getString("Content") ?: "" + } + } + + comment?.also { + if (name == "") name = it + else finalDescription = (if (finalDescription == "") "" else " ") + it + } + + description?.also { + finalDescription = (if (finalDescription == "") "" else " - ") + it + } + + val columnColor = column.getInt("Color") ?: 0 + val color = if (columnColor == 0) + when (name) { + "1-", "1", "1+" -> 0xffd65757 + "2-", "2", "2+" -> 0xff9071b3 + "3-", "3", "3+" -> 0xffd2ab24 + "4-", "4", "4+" -> 0xff50b6d6 + "5-", "5", "5+" -> 0xff2cbd92 + "6-", "6", "6+" -> 0xff91b43c + else -> 0xff3D5F9C + }.toInt() + else + columnColor + + val gradeObject = Grade( + profileId = profileId, + id = id, + name = name, + type = Grade.TYPE_NORMAL, + value = value ?: 0.0f, + weight = weight, + color = color, + category = categoryText, + description = finalDescription, + comment = null, + semester = getSemester(column), + teacherId = teacherId, + subjectId = subjectId, + addedDate = addedDate + ) + + data.gradeList.add(gradeObject) + data.metadataList.add( + Metadata( + profileId, + Metadata.TYPE_GRADE, + id, + profile?.empty ?: true, + profile?.empty ?: true + ) + ) + } + + data.setSyncNext(ENDPOINT_VULCAN_HEBE_GRADES, SYNC_ALWAYS) + onSuccess(ENDPOINT_VULCAN_HEBE_GRADES) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeHomework.kt new file mode 100644 index 00000000..9ab8ede0 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeHomework.kt @@ -0,0 +1,69 @@ +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe + +import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_HOMEWORK +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_HOMEWORK +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe +import pl.szczodrzynski.edziennik.data.db.entity.Event +import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.getLong +import pl.szczodrzynski.edziennik.getString + +class VulcanHebeHomework( + override val data: DataVulcan, + override val lastSync: Long?, + val onSuccess: (endpointId: Int) -> Unit +) : VulcanHebe(data, lastSync) { + companion object { + const val TAG = "VulcanHebeHomework" + } + + init { + apiGetList( + TAG, + VULCAN_HEBE_ENDPOINT_HOMEWORK, + HebeFilterType.BY_PUPIL, + lastSync = lastSync + ) { list, _ -> + list.forEach { exam -> + val id = exam.getLong("IdHomework") ?: return@forEach + val eventDate = getDate(exam, "Deadline") ?: return@forEach + val subjectId = getSubjectId(exam, "Subject") ?: -1 + val teacherId = getTeacherId(exam, "Creator") ?: -1 + val teamId = data.teamClass?.id ?: -1 + val topic = exam.getString("Content")?.trim() ?: "" + + val lessonList = data.db.timetableDao().getAllForDateNow(profileId, eventDate) + val startTime = lessonList.firstOrNull { it.subjectId == subjectId }?.startTime + + val eventObject = Event( + profileId = profileId, + id = id, + date = eventDate, + time = startTime, + topic = topic, + color = null, + type = Event.TYPE_HOMEWORK, + teacherId = teacherId, + subjectId = subjectId, + teamId = teamId + ) + + data.eventList.add(eventObject) + data.metadataList.add( + Metadata( + profileId, + Metadata.TYPE_HOMEWORK, + id, + profile?.empty ?: true, + profile?.empty ?: true + ) + ) + } + + data.setSyncNext(ENDPOINT_VULCAN_HEBE_HOMEWORK, SYNC_ALWAYS) + onSuccess(ENDPOINT_VULCAN_HEBE_HOMEWORK) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMain.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMain.kt new file mode 100644 index 00000000..b3d88d04 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMain.kt @@ -0,0 +1,160 @@ +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe + +import com.google.gson.JsonArray +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_VULCAN +import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MAIN +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_MAIN +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe +import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.utils.models.Date + +class VulcanHebeMain( + override val data: DataVulcan, + override val lastSync: Long? = null +) : VulcanHebe(data, lastSync) { + companion object { + const val TAG = "VulcanHebeMain" + } + + fun getStudents( + profile: Profile?, + profileList: MutableList?, + loginStoreId: Int? = null, + firstProfileId: Int? = null, + onEmpty: (() -> Unit)? = null, + onSuccess: () -> Unit + ) { + if (profile == null && (profileList == null || loginStoreId == null || firstProfileId == null)) + throw IllegalArgumentException() + + apiGet( + TAG, + VULCAN_HEBE_ENDPOINT_MAIN, + query = mapOf("lastSyncDate" to "null"), + baseUrl = profile == null + ) { students: JsonArray, _ -> + if (students.isEmpty()) { + if (onEmpty != null) + onEmpty() + else + onSuccess() + return@apiGet + } + + // safe to assume this will be non-null when creating a profile + var profileId = firstProfileId ?: loginStoreId ?: 1 + + students.forEach { studentEl -> + val student = studentEl.asJsonObject + + val pupil = student.getJsonObject("Pupil") + val studentId = pupil.getInt("Id") ?: return@forEach + + // check the student ID in case of not first login + if (profile != null && data.studentId != studentId) + return@forEach + + val unit = student.getJsonObject("Unit") + val constituentUnit = student.getJsonObject("ConstituentUnit") + val login = student.getJsonObject("Login") + val periods = student.getJsonArray("Periods")?.map { + it.asJsonObject + } ?: listOf() + + val period = periods.firstOrNull { + it.getBoolean("Current", false) + } ?: return@forEach + + val periodLevel = period.getInt("Level") ?: return@forEach + val semester1 = periods.firstOrNull { + it.getInt("Level") == periodLevel && it.getInt("Number") == 1 + } + val semester2 = periods.firstOrNull { + it.getInt("Level") == periodLevel && it.getInt("Number") == 2 + } + + val schoolSymbol = unit.getString("Symbol") ?: return@forEach + val schoolShort = constituentUnit.getString("Short") ?: return@forEach + val schoolCode = "${data.symbol}_$schoolSymbol" + + val studentUnitId = unit.getInt("Id") ?: return@forEach + val studentConstituentId = constituentUnit.getInt("Id") ?: return@forEach + val studentLoginId = login.getInt("Id") ?: return@forEach + //val studentClassId = student.getInt("IdOddzial") ?: return@forEach + val studentClassName = student.getString("ClassDisplay") ?: return@forEach + val studentFirstName = pupil.getString("FirstName") ?: "" + val studentLastName = pupil.getString("Surname") ?: "" + val studentNameLong = "$studentFirstName $studentLastName".fixName() + val studentNameShort = "$studentFirstName ${studentLastName[0]}.".fixName() + val userLogin = login.getString("Value") ?: "" + + val studentSemesterId = period.getInt("Id") ?: return@forEach + val studentSemesterNumber = period.getInt("Number") ?: return@forEach + + val hebeContext = student.getString("Context") + + val isParent = login.getString("LoginRole").equals("opiekun", ignoreCase = true) + val accountName = if (isParent) + login.getString("DisplayName")?.fixName() + else null + + val dateSemester1Start = semester1 + ?.getJsonObject("Start") + ?.getString("Date") + ?.let { Date.fromY_m_d(it) } + val dateSemester2Start = semester2 + ?.getJsonObject("Start") + ?.getString("Date") + ?.let { Date.fromY_m_d(it) } + val dateYearEnd = semester2 + ?.getJsonObject("End") + ?.getString("Date") + ?.let { Date.fromY_m_d(it) } + + val newProfile = profile ?: Profile( + profileId++, + loginStoreId!!, + LOGIN_TYPE_VULCAN, + studentNameLong, + userLogin, + studentNameLong, + studentNameShort, + accountName + ) + + newProfile.apply { + this.studentClassName = studentClassName + studentData["symbol"] = data.symbol + + studentData["studentId"] = studentId + studentData["studentUnitId"] = studentUnitId + studentData["studentConstituentId"] = studentConstituentId + studentData["studentLoginId"] = studentLoginId + studentData["studentSemesterId"] = studentSemesterId + studentData["studentSemesterNumber"] = studentSemesterNumber + studentData["semester1Id"] = semester1?.getInt("Id") ?: 0 + studentData["semester2Id"] = semester2?.getInt("Id") ?: 0 + studentData["schoolSymbol"] = schoolSymbol + studentData["schoolShort"] = schoolShort + studentData["schoolName"] = schoolCode + studentData["hebeContext"] = hebeContext + } + dateSemester1Start?.let { + newProfile.dateSemester1Start = it + newProfile.studentSchoolYearStart = it.year + } + dateSemester2Start?.let { newProfile.dateSemester2Start = it } + dateYearEnd?.let { newProfile.dateYearEnd = it } + + if (profile != null) + data.setSyncNext(ENDPOINT_VULCAN_HEBE_MAIN, 1 * DAY) + + profileList?.add(newProfile) + } + + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMessages.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMessages.kt new file mode 100644 index 00000000..22fb39b7 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMessages.kt @@ -0,0 +1,127 @@ +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe + +import androidx.core.util.set +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_MESSAGES +import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGES +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_MESSAGES_SENT +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe +import pl.szczodrzynski.edziennik.data.db.entity.* +import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_DELETED +import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED +import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_SENT +import pl.szczodrzynski.navlib.crc16 +import kotlin.text.replace + +class VulcanHebeMessages( + override val data: DataVulcan, + override val lastSync: Long?, + val onSuccess: (endpointId: Int) -> Unit +) : VulcanHebe(data, lastSync) { + companion object { + const val TAG = "VulcanHebeMessagesInbox" + } + + private fun getPersonId(json: JsonObject): Long { + val senderLoginId = json.getInt("LoginId") ?: return -1 + /*if (senderLoginId == data.studentLoginId) + return -1*/ + + val senderName = json.getString("Address") ?: return -1 + val senderNameSplit = senderName.splitName() + val senderLoginIdStr = senderLoginId.toString() + val teacher = data.teacherList.singleOrNull { it.loginId == senderLoginIdStr } + ?: Teacher( + profileId, + -1 * crc16(senderName).toLong(), + senderNameSplit?.second ?: "", + senderNameSplit?.first ?: "", + senderLoginIdStr + ).also { + it.setTeacherType(Teacher.TYPE_OTHER) + data.teacherList[it.id] = it + } + return teacher.id + } + + fun getMessages(messageType: Int) { + val folder = when (messageType) { + TYPE_RECEIVED -> 1 + TYPE_SENT -> 2 + TYPE_DELETED -> 3 + else -> 1 + } + val endpointId = when (messageType) { + TYPE_RECEIVED -> ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX + TYPE_SENT -> ENDPOINT_VULCAN_HEBE_MESSAGES_SENT + else -> ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX + } + apiGetList( + TAG, + VULCAN_HEBE_ENDPOINT_MESSAGES, + HebeFilterType.BY_PERSON, + folder = folder, + lastSync = lastSync + ) { list, _ -> + list.forEach { message -> + val id = message.getLong("Id") ?: return@forEach + val subject = message.getString("Subject") ?: return@forEach + val body = message.getString("Content") ?: return@forEach + + val sender = message.getJsonObject("Sender") ?: return@forEach + + val sentDate = getDateTime(message, "DateSent") + val readDate = getDateTime(message, "DateRead", default = 0) + + val messageObject = Message( + profileId = profileId, + id = id, + type = messageType, + subject = subject, + body = body.replace("\n", "
"), + senderId = if (messageType == TYPE_RECEIVED) getPersonId(sender) else null, + addedDate = sentDate + ) + + val receivers = message.getJsonArray("Receiver") + ?.asJsonObjectList() + ?: return@forEach + val receiverReadDate = + if (receivers.size == 1) readDate + else -1 + + for (receiver in receivers) { + val messageRecipientObject = MessageRecipient( + profileId, + if (messageType == TYPE_SENT) getPersonId(receiver) else -1, + -1, + receiverReadDate, + id + ) + data.messageRecipientList.add(messageRecipientObject) + } + + data.messageList.add(messageObject) + data.setSeenMetadataList.add( + Metadata( + profileId, + Metadata.TYPE_MESSAGE, + id, + readDate > 0 || messageType == TYPE_SENT, + readDate > 0 || messageType == TYPE_SENT + ) + ) + } + + data.setSyncNext( + endpointId, + if (messageType == TYPE_RECEIVED) SYNC_ALWAYS else 1 * DAY, + if (messageType == TYPE_RECEIVED) null else DRAWER_ITEM_MESSAGES + ) + onSuccess(endpointId) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMessagesChangeStatus.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMessagesChangeStatus.kt new file mode 100644 index 00000000..74d3f759 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMessagesChangeStatus.kt @@ -0,0 +1,62 @@ +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe + +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.JsonObject +import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGES_STATUS +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe +import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent +import pl.szczodrzynski.edziennik.data.db.entity.Message +import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient +import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.full.MessageFull + +class VulcanHebeMessagesChangeStatus( + override val data: DataVulcan, + private val messageObject: MessageFull, + val onSuccess: () -> Unit +) : VulcanHebe(data, null) { + companion object { + const val TAG = "VulcanHebeMessagesChangeStatus" + } + + init { + apiPost( + TAG, + VULCAN_HEBE_ENDPOINT_MESSAGES_STATUS, + payload = JsonObject( + "MessageId" to messageObject.id, + "LoginId" to data.studentLoginId, + "Status" to 1 + ) + ) { _: Boolean, _ -> + + if (!messageObject.seen) { + data.setSeenMetadataList.add( + Metadata( + profileId, + Metadata.TYPE_MESSAGE, + messageObject.id, + true, + true + ) + ) + messageObject.seen = true + } + + if (messageObject.type != Message.TYPE_SENT) { + val messageRecipientObject = MessageRecipient( + profileId, + -1, + -1, + System.currentTimeMillis(), + messageObject.id + ) + data.messageRecipientList.add(messageRecipientObject) + } + + EventBus.getDefault().postSticky(MessageGetEvent(messageObject)) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeTimetable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeTimetable.kt new file mode 100644 index 00000000..48d06018 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeTimetable.kt @@ -0,0 +1,247 @@ +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe + +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_TIMETABLE +import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_TIMETABLE_CHANGES +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_TIMETABLE +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe +import pl.szczodrzynski.edziennik.data.db.entity.Lesson +import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_CANCELLED +import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_CHANGE +import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_NORMAL +import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_SHIFTED_SOURCE +import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_SHIFTED_TARGET +import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.utils.Utils.d +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Week + +class VulcanHebeTimetable( + override val data: DataVulcan, + override val lastSync: Long?, + val onSuccess: (endpointId: Int) -> Unit +) : VulcanHebe(data, lastSync) { + companion object { + const val TAG = "VulcanHebeTimetable" + } + + private val lessonList = mutableListOf() + private val lessonDates = mutableSetOf() + + init { + val previousWeekStart = Week.getWeekStart().stepForward(0, 0, -7) + if (Date.getToday().weekDay > 4) { + previousWeekStart.stepForward(0, 0, 7) + } + + val dateFrom = data.arguments + ?.getString("weekStart") + ?.let { Date.fromY_m_d(it) } + ?: previousWeekStart + val dateTo = dateFrom.clone().stepForward(0, 0, 13) + + val lastSync = null + + apiGetList( + TAG, + VULCAN_HEBE_ENDPOINT_TIMETABLE, + HebeFilterType.BY_PUPIL, + dateFrom = dateFrom, + dateTo = dateTo, + lastSync = lastSync + ) { lessons, _ -> + apiGetList( + TAG, + VULCAN_HEBE_ENDPOINT_TIMETABLE_CHANGES, + HebeFilterType.BY_PUPIL, + dateFrom = dateFrom, + dateTo = dateTo, + lastSync = lastSync + ) { changes, _ -> + processData(lessons, changes) + + // cancel lesson changes when caused by a shift + for (lesson in lessonList) { + if (lesson.type != TYPE_SHIFTED_TARGET) + continue + lessonList.firstOrNull { + it.oldDate == lesson.date + && it.oldLessonNumber == lesson.lessonNumber + && it.type == TYPE_CHANGE + }?.let { + it.type = TYPE_CANCELLED + it.date = null + it.lessonNumber = null + it.startTime = null + it.endTime = null + it.subjectId = null + it.teacherId = null + it.teamId = null + it.classroom = null + } + } + + // add TYPE_NO_LESSONS to empty dates + val date: Date = dateFrom.clone() + while (date <= dateTo) { + if (!lessonDates.contains(date.value)) { + lessonList.add(Lesson(profileId, date.value.toLong()).apply { + this.type = Lesson.TYPE_NO_LESSONS + this.date = date.clone() + }) + } + + date.stepForward(0, 0, 1) + } + + d( + TAG, + "Clearing lessons between ${dateFrom.stringY_m_d} and ${dateTo.stringY_m_d}" + ) + + data.lessonList.addAll(lessonList) + + data.setSyncNext(ENDPOINT_VULCAN_HEBE_TIMETABLE, SYNC_ALWAYS) + onSuccess(ENDPOINT_VULCAN_HEBE_TIMETABLE) + } + } + } + + private fun buildLesson(changes: List, json: JsonObject): Pair? { + val lesson = Lesson(profileId, -1) + var lessonShift: Lesson? = null + + val lessonDate = getDate(json, "Date") ?: return null + val lessonRange = getLessonRange(json, "TimeSlot") + val startTime = lessonRange?.startTime + val endTime = lessonRange?.endTime + val teacherId = getTeacherId(json, "TeacherPrimary") + val classroom = json.getJsonObject("Room").getString("Code") + val subjectId = getSubjectId(json, "Subject") + + val teamId = getTeamId(json, "Distribution") + ?: getClassId(json, "Clazz") + ?: data.teamClass?.id + ?: -1 + + val change = json.getJsonObject("Change") + val changeId = change.getInt("Id") + val type = when (change.getInt("Type")) { + 1 -> TYPE_CANCELLED + 2 -> TYPE_CHANGE + 3 -> TYPE_SHIFTED_SOURCE + 4 -> TYPE_CANCELLED // TODO: 2021-02-21 add showing cancellation reason + else -> TYPE_NORMAL + } + + lesson.type = type + if (type == TYPE_NORMAL) { + lesson.date = lessonDate + lesson.lessonNumber = lessonRange?.lessonNumber + lesson.startTime = startTime + lesson.endTime = endTime + lesson.subjectId = subjectId + lesson.teacherId = teacherId + lesson.teamId = teamId + lesson.classroom = classroom + } else { + lesson.oldDate = lessonDate + lesson.oldLessonNumber = lessonRange?.lessonNumber + lesson.oldStartTime = startTime + lesson.oldEndTime = endTime + lesson.oldSubjectId = subjectId + lesson.oldTeacherId = teacherId + lesson.oldTeamId = teamId + lesson.oldClassroom = classroom + } + + if (type == TYPE_CHANGE || type == TYPE_SHIFTED_SOURCE) { + val changeJson = changes.firstOrNull { + it.getInt("Id") == changeId + } ?: return lesson to null + + val changeLessonDate = getDate(changeJson, "LessonDate") ?: return lesson to null + val changeLessonRange = getLessonRange(changeJson, "TimeSlot") ?: lessonRange + val changeStartTime = changeLessonRange?.startTime + val changeEndTime = changeLessonRange?.endTime + val changeTeacherId = getTeacherId(changeJson, "TeacherPrimary") ?: teacherId + val changeClassroom = changeJson.getJsonObject("Room").getString("Code") ?: classroom + val changeSubjectId = getSubjectId(changeJson, "Subject") ?: subjectId + + val changeTeamId = getTeamId(json, "Distribution") + ?: getClassId(json, "Clazz") + ?: teamId + + if (type != TYPE_CHANGE) { + /* lesson shifted */ + lessonShift = Lesson(profileId, -1) + lessonShift.type = TYPE_SHIFTED_TARGET + + // update source lesson with the target lesson date + lesson.date = changeLessonDate + lesson.lessonNumber = changeLessonRange?.lessonNumber + lesson.startTime = changeStartTime + lesson.endTime = changeEndTime + // update target lesson with the source lesson date + lessonShift.oldDate = lessonDate + lessonShift.oldLessonNumber = lessonRange?.lessonNumber + lessonShift.oldStartTime = startTime + lessonShift.oldEndTime = endTime + } + + (if (type == TYPE_CHANGE) lesson else lessonShift) + ?.apply { + this.date = changeLessonDate + this.lessonNumber = changeLessonRange?.lessonNumber + this.startTime = changeStartTime + this.endTime = changeEndTime + this.subjectId = changeSubjectId + this.teacherId = changeTeacherId + this.teamId = changeTeamId + this.classroom = changeClassroom + } + } + + return lesson to lessonShift + } + + private fun processData(lessons: List, changes: List) { + lessons.forEach { lessonJson -> + if (lessonJson.getBoolean("Visible") != true) + return@forEach + + val lessonPair = buildLesson(changes, lessonJson) ?: return@forEach + val (lessonObject, lessonShift) = lessonPair + + when { + lessonShift != null -> lessonShift + lessonObject.type != TYPE_NORMAL -> lessonObject + else -> null + }?.let { lesson -> + val lessonDate = lesson.displayDate ?: return@let + val seen = profile?.empty ?: true || lessonDate < Date.getToday() + data.metadataList.add( + Metadata( + profileId, + Metadata.TYPE_LESSON_CHANGE, + lesson.id, + seen, + seen + ) + ) + } + + lessonObject.id = lessonObject.buildId() + lessonShift?.id = lessonShift?.buildId() ?: -1 + + lessonList.add(lessonObject) + lessonShift?.let { lessonList.add(it) } + + lessonObject.displayDate?.let { lessonDates.add(it.value) } + lessonShift?.displayDate?.let { lessonDates.add(it.value) } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/firstlogin/VulcanFirstLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/firstlogin/VulcanFirstLogin.kt index ed541c69..02d8b9bd 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/firstlogin/VulcanFirstLogin.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/firstlogin/VulcanFirstLogin.kt @@ -9,9 +9,12 @@ import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanWebMain +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe.VulcanHebeMain import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.CufsCertificate import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginApi +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginHebe import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginWebMain import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent import pl.szczodrzynski.edziennik.data.api.models.ApiError @@ -25,6 +28,7 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) { private val api = VulcanApi(data, null) private val web = VulcanWebMain(data, null) + private val hebe = VulcanHebe(data, null) private val profileList = mutableListOf() private val loginStoreId = data.loginStore.id private var firstProfileId = loginStoreId @@ -50,12 +54,18 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) { checkSymbol(certificate) } } - else { + else if (data.loginStore.mode == LOGIN_MODE_VULCAN_API) { registerDevice { EventBus.getDefault().postSticky(FirstLoginFinishedEvent(profileList, data.loginStore)) onSuccess() } } + else { + registerDeviceHebe { + EventBus.getDefault().postSticky(FirstLoginFinishedEvent(profileList, data.loginStore)) + onSuccess() + } + } } private fun checkSymbol(certificate: CufsCertificate) { @@ -103,7 +113,7 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) { data.apiPin = data.apiPin.toMutableMap().also { it[symbol] = json.getString("PIN") } - registerDevice(onSuccess) + registerDeviceHebe(onSuccess) } } } @@ -197,4 +207,21 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) { } } } + + private fun registerDeviceHebe(onSuccess: () -> Unit) { + VulcanLoginHebe(data) { + VulcanHebeMain(data).getStudents( + profile = null, + profileList, + loginStoreId, + firstProfileId, + onEmpty = { + EventBus.getDefault() + .postSticky(FirstLoginFinishedEvent(listOf(), data.loginStore)) + onSuccess() + }, + onSuccess = onSuccess + ) + } + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLogin.kt index 45c0153d..b92dea88 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLogin.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLogin.kt @@ -6,6 +6,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_API +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_HEBE import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_WEB_MAIN import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.utils.Utils @@ -54,6 +55,10 @@ class VulcanLogin(val data: DataVulcan, val onSuccess: () -> Unit) { data.startProgress(R.string.edziennik_progress_login_vulcan_api) VulcanLoginApi(data) { onSuccess(loginMethodId) } } + LOGIN_METHOD_VULCAN_HEBE -> { + data.startProgress(R.string.edziennik_progress_login_vulcan_api) + VulcanLoginHebe(data) { onSuccess(loginMethodId) } + } } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginApi.kt index ec84cd05..def1d7e3 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginApi.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginApi.kt @@ -191,18 +191,6 @@ class VulcanLoginApi(val data: DataVulcan, val onSuccess: () -> Unit) { } } - val deviceId = data.app.deviceId.padStart(16, '0') - val loginStoreId = data.loginStore.id.toString(16).padStart(4, '0') - val symbol = data.symbol?.crc16()?.toString(16)?.take(2) ?: "00" - val uuid = - deviceId.substring(0..7) + - "-" + deviceId.substring(8..11) + - "-" + deviceId.substring(12..15) + - "-" + loginStoreId + - "-" + symbol + "6f72616e7a" - - val deviceNameSuffix = " - nie usuwać" - val szkolnyApi = SzkolnyApi(data.app) val firebaseToken = szkolnyApi.runCatching({ getFirebaseToken("vulcan") @@ -216,8 +204,8 @@ class VulcanLoginApi(val data: DataVulcan, val onSuccess: () -> Unit) { .addHeader("RequestMobileType", "RegisterDevice") .addParameter("PIN", data.apiPin[data.symbol]) .addParameter("TokenKey", data.apiToken[data.symbol]) - .addParameter("DeviceId", uuid) - .addParameter("DeviceName", VULCAN_API_DEVICE_NAME.take(50 - deviceNameSuffix.length) + deviceNameSuffix) + .addParameter("DeviceId", data.buildDeviceId()) + .addParameter("DeviceName", VULCAN_API_DEVICE_NAME) .addParameter("DeviceNameUser", "") .addParameter("DeviceDescription", "") .addParameter("DeviceSystemType", "Android") diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginHebe.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginHebe.kt new file mode 100644 index 00000000..1308f449 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginHebe.kt @@ -0,0 +1,106 @@ +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login + +import com.google.gson.JsonObject +import io.github.wulkanowy.signer.hebe.generateKeyPair +import pl.szczodrzynski.edziennik.JsonObject +import pl.szczodrzynski.edziennik.data.api.ERROR_LOGIN_DATA_MISSING +import pl.szczodrzynski.edziennik.data.api.VULCAN_API_DEVICE_NAME +import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_REGISTER_NEW +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi +import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.isNotNullNorEmpty + +class VulcanLoginHebe(val data: DataVulcan, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "VulcanLoginHebe" + } + + init { run { + // i'm sure this does something useful + // not quite sure what, though + if (data.studentSemesterNumber == 1 && data.semester1Id == 0) + data.semester1Id = data.studentSemesterNumber + if (data.studentSemesterNumber == 2 && data.semester2Id == 0) + data.semester2Id = data.studentSemesterNumber + + copyFromLoginStore() + + if (data.profile != null && data.isApiLoginValid()) { + onSuccess() + } + else { + if (data.symbol.isNotNullNorEmpty() && data.apiToken[data.symbol].isNotNullNorEmpty() && data.apiPin[data.symbol].isNotNullNorEmpty()) { + loginWithToken() + } + else { + data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING)) + } + } + }} + + private fun copyFromLoginStore() { + data.loginStore.data.apply { + // map form inputs to the symbol + if (has("symbol")) { + data.symbol = getString("symbol") + remove("symbol") + } + if (has("deviceToken")) { + data.apiToken = data.apiToken.toMutableMap().also { + it[data.symbol] = getString("deviceToken") + } + remove("deviceToken") + } + if (has("devicePin")) { + data.apiPin = data.apiPin.toMutableMap().also { + it[data.symbol] = getString("devicePin") + } + remove("devicePin") + } + } + } + + private fun loginWithToken() { + val szkolnyApi = SzkolnyApi(data.app) + val hebe = VulcanHebe(data, null) + + if (data.hebePublicKey == null || data.hebePrivateKey == null || data.hebePublicHash == null) { + val (publicPem, privatePem, publicHash) = generateKeyPair() + data.hebePublicKey = publicPem + data.hebePrivateKey = privatePem + data.hebePublicHash = publicHash + } + + val firebaseToken = szkolnyApi.runCatching({ + getFirebaseToken("vulcan") + }, onError = { + // screw errors + }) ?: data.app.config.sync.tokenVulcan + + hebe.apiPost( + TAG, + VULCAN_HEBE_ENDPOINT_REGISTER_NEW, + payload = JsonObject( + "OS" to "Android", + "PIN" to data.apiPin[data.symbol], + "Certificate" to data.hebePublicKey, + "CertificateType" to "RSA_PEM", + "DeviceModel" to VULCAN_API_DEVICE_NAME, + "SecurityToken" to data.apiToken[data.symbol], + "SelfIdentifier" to data.buildDeviceId(), + "CertificateThumbprint" to data.hebePublicHash + ), + baseUrl = true, + firebaseToken = firebaseToken + ) { _: JsonObject, _ -> + data.apiToken = data.apiToken.toMutableMap().also { + it[data.symbol] = it[data.symbol]?.substring(0, 3) + } + data.loginStore.removeLoginData("apiPin") + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt index f8ca5090..b15b6408 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt @@ -46,6 +46,6 @@ object Signing { /*fun provideKey(param1: String, param2: Long): ByteArray {*/ fun pleaseStopRightNow(param1: String, param2: Long): ByteArray { - return "$param1.MTIzNDU2Nzg5MDzyYb9Lof===.$param2".sha256() + return "$param1.MTIzNDU2Nzg5MDL9U0lwJn===.$param2".sha256() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginInfo.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginInfo.kt index 2b3acd68..13fa6123 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginInfo.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginInfo.kt @@ -131,9 +131,9 @@ object LoginInfo { registerLogo = R.drawable.login_logo_vulcan, loginModes = listOf( Mode( - loginMode = LOGIN_MODE_VULCAN_API, + loginMode = LOGIN_MODE_VULCAN_HEBE, name = R.string.login_mode_vulcan_api, - icon = R.drawable.login_mode_vulcan_api, + icon = R.drawable.login_mode_vulcan_hebe, hintText = R.string.login_mode_vulcan_api_hint, guideText = R.string.login_mode_vulcan_api_guide, isRecommended = true, diff --git a/app/src/main/res/drawable/login_mode_vulcan_hebe.png b/app/src/main/res/drawable/login_mode_vulcan_hebe.png new file mode 100644 index 00000000..bea96dc7 Binary files /dev/null and b/app/src/main/res/drawable/login_mode_vulcan_hebe.png differ diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 461bdbd3..6e37bf58 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -854,7 +854,7 @@ Open-Source-Lizenzen Datenschutzrichtlinie E-Klassenbuch - © Kuba Szczodrzyński && Kacper Ziubryniewicz\nSeptember 2018 - 2020 + © Kuba Szczodrzyński && Kacper Ziubryniewicz\nSeptember 2018 - Februar 2021 Klicken Sie hier, um nach Aktualisierungen zu suchen Aktualisierung Version diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index f4a89910..ab80b93c 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -856,7 +856,7 @@ Open-source licenses Privacy policy E-register - © Kuba Szczodrzyński && Kacper Ziubryniewicz\nSeptember 2018 - 2020 + © Kuba Szczodrzyński && Kacper Ziubryniewicz\nSeptember 2018 - February 2021 Click to check for updates Update Version diff --git a/app/src/main/res/values/errors.xml b/app/src/main/res/values/errors.xml index 3e5f48d6..91fae735 100644 --- a/app/src/main/res/values/errors.xml +++ b/app/src/main/res/values/errors.xml @@ -130,6 +130,7 @@ ERROR_VULCAN_API_BAD_REQUEST ERROR_VULCAN_API_OTHER ERROR_VULCAN_ATTACHMENT_DOWNLOAD + ERROR_VULCAN_API_DEPRECATED ERROR_LOGIN_IDZIENNIK_WEB_INVALID_LOGIN ERROR_LOGIN_IDZIENNIK_WEB_INVALID_SCHOOL_NAME @@ -318,6 +319,7 @@ VULCAN®: błąd żądania, zgłoś błąd VULCAN®: inny błąd, wyślij zgłoszenie VULCAN®: nie znaleziono adresu załącznika + W związku z wygaszeniem aplikacji Dzienniczek+ przez firmę Vulcan, należy zalogować się ponownie.\n\nAby móc dalej korzystać z aplikacji Szkolny.eu, otwórz Ustawienia i wybierz opcję Dodaj nowego ucznia.\nNastępnie zaloguj się do dziennika Vulcan zgodnie z instrukcją.\n\nPrzepraszamy za niedogodności. Nieprawidłowe dane logowania Nieprawidłowa nazwa szkoły diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 40f79aa9..a913914d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -919,7 +919,7 @@ Licencje open-source Polityka prywatności E-dziennik - © Kuba Szczodrzyński && Kacper Ziubryniewicz\nwrzesień 2018 - 2020 + © Kuba Szczodrzyński && Kacper Ziubryniewicz\nwrzesień 2018 - luty 2021 Kliknij, aby sprawdzić aktualizacje Aktualizacja Wersja diff --git a/build.gradle b/build.gradle index 3b24a9a9..54a6a31b 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { kotlin_version = '1.4.30' release = [ - versionName: "4.4.3", - versionCode: 4040399 + versionName: "4.5-beta.2", + versionCode: 4050002 ] setup = [