From 4b6427794855937e131a690397d16afc28602563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Tue, 11 Oct 2022 23:23:11 +0200 Subject: [PATCH 01/15] [API/Usos] Add basic USOS API structure. --- .idea/dictionaries/Kuba.xml | 1 + .../edziennik/data/api/Errors.kt | 2 + .../edziennik/data/api/LoginMethods.kt | 10 ++ .../data/api/edziennik/usos/DataUsos.kt | 39 +++++++ .../edziennik/data/api/edziennik/usos/Usos.kt | 109 ++++++++++++++++++ .../data/api/edziennik/usos/UsosFeatures.kt | 18 +++ .../api/edziennik/usos/login/UsosLogin.kt | 54 +++++++++ .../api/edziennik/usos/login/UsosLoginApi.kt | 29 +++++ .../edziennik/ui/login/LoginInfo.kt | 19 ++- app/src/main/res/drawable/login_logo_usos.png | Bin 0 -> 15635 bytes app/src/main/res/values/errors.xml | 4 + app/src/main/res/values/strings.xml | 4 + 12 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/Usos.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/UsosFeatures.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLogin.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt create mode 100644 app/src/main/res/drawable/login_logo_usos.png diff --git a/.idea/dictionaries/Kuba.xml b/.idea/dictionaries/Kuba.xml index 592a5d5e..aba38775 100644 --- a/.idea/dictionaries/Kuba.xml +++ b/.idea/dictionaries/Kuba.xml @@ -13,6 +13,7 @@ synergia szczodrzyński szkolny + usos \ No newline at end of file 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 227561e6..617ef5ed 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 @@ -204,6 +204,8 @@ const val ERROR_PODLASIE_API_NO_TOKEN = 630 const val ERROR_PODLASIE_API_OTHER = 631 const val ERROR_PODLASIE_API_DATA_MISSING = 632 +const val ERROR_USOS_OAUTH_LOGIN_REQUEST = 701 + const val ERROR_TEMPLATE_WEB_OTHER = 801 const val EXCEPTION_API_TASK = 900 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 0f0c07ae..9825ea3f 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 @@ -13,6 +13,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login.Mobidzie import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.login.PodlasieLoginApi 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.usos.login.UsosLoginApi 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 @@ -127,6 +128,15 @@ val podlasieLoginMethods = listOf( .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED } ) +const val LOGIN_TYPE_USOS = 7 +const val LOGIN_MODE_USOS_OAUTH = 0 +const val LOGIN_METHOD_USOS_API = 100 +val usosLoginMethods = listOf( + LoginMethod(LOGIN_TYPE_USOS, LOGIN_METHOD_USOS_API, UsosLoginApi::class.java) + .withIsPossible { _, _ -> true } + .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED } +) + val templateLoginMethods = listOf( LoginMethod(LOGIN_TYPE_TEMPLATE, LOGIN_METHOD_TEMPLATE_WEB, TemplateLoginWeb::class.java) .withIsPossible { _, _ -> true } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt new file mode 100644 index 00000000..b4a925bf --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-11. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos + +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_USOS_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 + +class DataUsos( + app: App, + profile: Profile?, + loginStore: LoginStore, +) : Data(app, profile, loginStore) { + + fun isApiLoginValid() = oauthTokenKey != null && oauthTokenSecret != null + + override fun satisfyLoginMethods() { + loginMethods.clear() + if (isApiLoginValid()) { + loginMethods += LOGIN_METHOD_USOS_API + } + } + + override fun generateUserCode() = "USOS:TEST" + + var oauthTokenKey: String? + get() { mOauthTokenKey = mOauthTokenKey ?: loginStore.getLoginData("oauthTokenKey", null); return mOauthTokenKey } + set(value) { loginStore.putLoginData("oauthTokenKey", value); mOauthTokenKey = value } + private var mOauthTokenKey: String? = null + + var oauthTokenSecret: String? + get() { mOauthTokenSecret = mOauthTokenSecret ?: loginStore.getLoginData("oauthTokenSecret", null); return mOauthTokenSecret } + set(value) { loginStore.putLoginData("oauthTokenSecret", value); mOauthTokenSecret = value } + private var mOauthTokenSecret: String? = null +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/Usos.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/Usos.kt new file mode 100644 index 00000000..c97e3695 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/Usos.kt @@ -0,0 +1,109 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-11. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos + +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.login.UsosLogin +import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback +import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.api.prepare +import pl.szczodrzynski.edziennik.data.api.usosLoginMethods +import pl.szczodrzynski.edziennik.data.db.entity.LoginStore +import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull +import pl.szczodrzynski.edziennik.data.db.full.EventFull +import pl.szczodrzynski.edziennik.data.db.full.MessageFull +import pl.szczodrzynski.edziennik.utils.Utils.d + +class Usos( + val app: App, + val profile: Profile?, + val loginStore: LoginStore, + val callback: EdziennikCallback, +) : EdziennikInterface { + companion object { + private const val TAG = "Usos" + } + + val internalErrorList = mutableListOf() + val data: DataUsos + + init { + data = DataUsos(app, profile, loginStore).apply { + callback = wrapCallback(this@Usos.callback) + satisfyLoginMethods() + } + } + + private fun completed() { + data.saveData() + callback.onCompleted() + } + + override fun sync( + featureIds: List, + viewId: Int?, + onlyEndpoints: List?, + arguments: JsonObject?, + ) { + data.arguments = arguments + data.prepare(usosLoginMethods, UsosFeatures, featureIds, viewId, onlyEndpoints) + d(TAG, "LoginMethod IDs: ${data.targetLoginMethodIds}") + d(TAG, "Endpoint IDs: ${data.targetEndpointIds}") + UsosLogin(data) { + /*UsosData(data) { + completed() + }*/ + } + } + + override fun getMessage(message: MessageFull) {} + override fun sendMessage(recipients: List, subject: String, text: String) {} + override fun markAllAnnouncementsAsRead() {} + override fun getAnnouncement(announcement: AnnouncementFull) {} + override fun getAttachment(owner: Any, attachmentId: Long, attachmentName: String) {} + override fun getRecipientList() {} + override fun getEvent(eventFull: EventFull) {} + + override fun firstLogin() { + /*UsosFirstLogin(data) { + completed() + }*/ + } + + override fun cancel() { + d(TAG, "Cancelled") + data.cancel() + } + + private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback { + return object : EdziennikCallback { + override fun onCompleted() { + callback.onCompleted() + } + + override fun onProgress(step: Float) { + callback.onProgress(step) + } + + override fun onStartProgress(stringRes: Int) { + callback.onStartProgress(stringRes) + } + + override fun onError(apiError: ApiError) { + when (apiError.errorCode) { + in internalErrorList -> { + // finish immediately if the same error occurs twice during the same sync + callback.onError(apiError) + } + else -> callback.onError(apiError) + } + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/UsosFeatures.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/UsosFeatures.kt new file mode 100644 index 00000000..a4ee1389 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/UsosFeatures.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-11. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos + +import pl.szczodrzynski.edziennik.data.api.FEATURE_STUDENT_INFO +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_USOS_API +import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_USOS +import pl.szczodrzynski.edziennik.data.api.models.Feature + +const val ENDPOINT_USOS_API_USER = 7000 + +val UsosFeatures = listOf( + Feature(LOGIN_TYPE_USOS, FEATURE_STUDENT_INFO, listOf( + ENDPOINT_USOS_API_USER to LOGIN_METHOD_USOS_API, + ), listOf(LOGIN_METHOD_USOS_API)), +) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLogin.kt new file mode 100644 index 00000000..599b8f16 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLogin.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-11. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos.login + +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_USOS_API +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos +import pl.szczodrzynski.edziennik.utils.Utils.d + +class UsosLogin(val data: DataUsos, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "UsosLogin" + } + + private var cancelled = false + + init { + nextLoginMethod(onSuccess) + } + + private fun nextLoginMethod(onSuccess: () -> Unit) { + if (data.targetLoginMethodIds.isEmpty()) { + onSuccess() + return + } + if (cancelled) { + onSuccess() + return + } + useLoginMethod(data.targetLoginMethodIds.removeAt(0)) { usedMethodId -> + data.progress(data.progressStep) + if (usedMethodId != -1) + data.loginMethods.add(usedMethodId) + nextLoginMethod(onSuccess) + } + } + + private fun useLoginMethod(loginMethodId: Int, onSuccess: (usedMethodId: Int) -> Unit) { + // this should never be true + if (data.loginMethods.contains(loginMethodId)) { + onSuccess(-1) + return + } + d(TAG, "Using login method $loginMethodId") + when (loginMethodId) { + LOGIN_METHOD_USOS_API -> { + data.startProgress(R.string.edziennik_progress_login_usos_api) + UsosLoginApi(data) { onSuccess(loginMethodId) } + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt new file mode 100644 index 00000000..bdf771c3 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-11. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos.login + +import pl.szczodrzynski.edziennik.data.api.ERROR_PROFILE_MISSING +import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_OAUTH_LOGIN_REQUEST +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos +import pl.szczodrzynski.edziennik.data.api.models.ApiError + +class UsosLoginApi(val data: DataUsos, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "UsosLoginApi" + } + + init { run { + if (data.profile == null) { + data.error(ApiError(TAG, ERROR_PROFILE_MISSING)) + return@run + } + + if (data.isApiLoginValid()) { + onSuccess() + } else { + data.error(ApiError(TAG, ERROR_USOS_OAUTH_LOGIN_REQUEST)) + } + }} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginInfo.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginInfo.kt index 0448b514..be6d62d8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginInfo.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginInfo.kt @@ -313,7 +313,24 @@ object LoginInfo { errorCodes = mapOf() ) ) - ) + ), + Register( + loginType = LOGIN_TYPE_USOS, + internalName = "usos", + registerName = R.string.login_type_usos, + registerLogo = R.drawable.login_logo_usos, + loginModes = listOf( + Mode( + loginMode = LOGIN_MODE_USOS_OAUTH, + name = R.string.login_mode_usos_oauth, + icon = R.drawable.login_logo_usos, + guideText = R.string.login_mode_usos_oauth_guide, + isPlatformSelection = true, + credentials = listOf(), + errorCodes = mapOf(), + ), + ), + ), ) } diff --git a/app/src/main/res/drawable/login_logo_usos.png b/app/src/main/res/drawable/login_logo_usos.png new file mode 100644 index 0000000000000000000000000000000000000000..7c376bddaf28d4462635115fed43a00a484a8a3c GIT binary patch literal 15635 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/errors.xml b/app/src/main/res/values/errors.xml index f0916a8b..4e978f7b 100644 --- a/app/src/main/res/values/errors.xml +++ b/app/src/main/res/values/errors.xml @@ -174,6 +174,8 @@ ERROR_PODLASIE_API_OTHER ERROR_PODLASIE_API_DATA_MISSING + ERROR_USOS_OAUTH_LOGIN_REQUEST + ERROR_TEMPLATE_WEB_OTHER EXCEPTION_API_TASK @@ -364,6 +366,8 @@ ERROR_PODLASIE_API_OTHER Brak danych. Zgłoś błąd programiście. + Wymagane logowanie w przeglądarce + ERROR_TEMPLATE_WEB_OTHER Błąd synchronizacji. Upewnij się, że masz połączenie z internetem, a następnie zgłoś błąd. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a8f81073..66ac0299 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1537,4 +1537,8 @@ (rodzic) - nieznany przedmiot - {cmd-information-outline} Oceny, których przedmiot nie został podany w dzienniku. Może to być na przykład taki, który nie jest prowadzony w tym roku szkolnym. + Logowanie do USOS API... + USOS + Logowanie z użyciem przeglądarki + TODO From 7935d0f097527614be4cdf42b641fc047e3b821d Mon Sep 17 00:00:00 2001 From: kuba2k2 Date: Thu, 13 Oct 2022 11:45:17 +0200 Subject: [PATCH 02/15] [API] Pass parameters to user action required errors. --- .../szczodrzynski/edziennik/MainActivity.kt | 5 +- .../api/events/UserActionRequiredEvent.kt | 5 +- .../edziennik/data/api/models/ApiError.kt | 7 +++ .../ui/login/LoginProgressFragment.kt | 5 +- .../utils/managers/UserActionManager.kt | 50 ++++++++++++++----- app/src/main/res/values/strings.xml | 1 + 6 files changed, 55 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt index 7197ea10..30c3a3e0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt @@ -853,7 +853,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope { @Subscribe(threadMode = ThreadMode.MAIN) fun onUserActionRequiredEvent(event: UserActionRequiredEvent) { - app.userActionManager.execute(this, event.profileId, event.type) + app.userActionManager.execute(this, event.profileId, event.type, event.params) } private fun fragmentToSyncName(currentFragment: Int): Int { @@ -914,7 +914,8 @@ class MainActivity : AppCompatActivity(), CoroutineScope { app.userActionManager.execute( this, extras.getInt("profileId"), - extras.getInt("type") + extras.getInt("type"), + extras.getBundle("params"), ) true } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/UserActionRequiredEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/UserActionRequiredEvent.kt index 49b510c2..da68f3e8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/UserActionRequiredEvent.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/UserActionRequiredEvent.kt @@ -4,11 +4,14 @@ package pl.szczodrzynski.edziennik.data.api.events -data class UserActionRequiredEvent(val profileId: Int, val type: Int) { +import android.os.Bundle + +data class UserActionRequiredEvent(val profileId: Int, val type: Int, val params: Bundle?) { companion object { const val LOGIN_DATA_MOBIDZIENNIK = 101 const val LOGIN_DATA_LIBRUS = 102 const val LOGIN_DATA_VULCAN = 104 const val CAPTCHA_LIBRUS = 202 + const val OAUTH_USOS = 701 } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/ApiError.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/ApiError.kt index bb5d20cc..b631ed7d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/ApiError.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/ApiError.kt @@ -5,6 +5,7 @@ package pl.szczodrzynski.edziennik.data.api.models import android.content.Context +import android.os.Bundle import com.google.gson.JsonObject import im.wangchao.mhttp.Request import im.wangchao.mhttp.Response @@ -30,6 +31,7 @@ class ApiError(val tag: String, var errorCode: Int) { var request: Request? = null var response: Response? = null var isCritical = true + var params: Bundle? = null fun withThrowable(throwable: Throwable?): ApiError { this.throwable = throwable @@ -58,6 +60,11 @@ class ApiError(val tag: String, var errorCode: Int) { return this } + fun withParams(bundle: Bundle): ApiError { + this.params = bundle + return this + } + fun getStringText(context: Context): String { return context.resources.getIdentifier("error_${errorCode}", "string", context.packageName).let { if (it != 0) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginProgressFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginProgressFragment.kt index 03abbc2b..ee29575e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginProgressFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginProgressFragment.kt @@ -137,9 +137,8 @@ class LoginProgressFragment : Fragment(), CoroutineScope { return } - app.userActionManager.execute(activity, event.profileId, event.type, onSuccess = { code -> - args.putString("recaptchaCode", code) - args.putLong("recaptchaTime", System.currentTimeMillis()) + app.userActionManager.execute(activity, event.profileId, event.type, event.params, onSuccess = { params -> + args.putAll(params) doFirstLogin(args) }, onFailure = { activity.error(ApiError(TAG, ERROR_CAPTCHA_NEEDED)) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt index dbe05325..76d5733d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt @@ -7,6 +7,7 @@ package pl.szczodrzynski.edziennik.utils.managers import android.app.NotificationManager import android.app.PendingIntent import android.content.Context +import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.core.app.NotificationCompat import org.greenrobot.eventbus.EventBus @@ -14,9 +15,11 @@ import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.ERROR_CAPTCHA_LIBRUS_PORTAL +import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_OAUTH_LOGIN_REQUEST import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.ext.Bundle import pl.szczodrzynski.edziennik.ext.Intent import pl.szczodrzynski.edziennik.ext.JsonObject import pl.szczodrzynski.edziennik.ext.pendingIntentFlag @@ -27,18 +30,21 @@ class UserActionManager(val app: App) { private const val TAG = "UserActionManager" } - fun requiresUserAction(apiError: ApiError): Boolean { - return apiError.errorCode == ERROR_CAPTCHA_LIBRUS_PORTAL + fun requiresUserAction(apiError: ApiError) = when (apiError.errorCode) { + ERROR_CAPTCHA_LIBRUS_PORTAL -> true + ERROR_USOS_OAUTH_LOGIN_REQUEST -> true + else -> false } fun sendToUser(apiError: ApiError) { val type = when (apiError.errorCode) { ERROR_CAPTCHA_LIBRUS_PORTAL -> UserActionRequiredEvent.CAPTCHA_LIBRUS + ERROR_USOS_OAUTH_LOGIN_REQUEST -> UserActionRequiredEvent.OAUTH_USOS else -> 0 } if (EventBus.getDefault().hasSubscriberForEvent(UserActionRequiredEvent::class.java)) { - EventBus.getDefault().post(UserActionRequiredEvent(apiError.profileId ?: -1, type)) + EventBus.getDefault().post(UserActionRequiredEvent(apiError.profileId ?: -1, type, apiError.params)) return } @@ -46,6 +52,7 @@ class UserActionManager(val app: App) { val text = app.getString(when (type) { UserActionRequiredEvent.CAPTCHA_LIBRUS -> R.string.notification_user_action_required_captcha_librus + UserActionRequiredEvent.OAUTH_USOS -> R.string.notification_user_action_required_oauth_usos else -> R.string.notification_user_action_required_text }, apiError.profileId) @@ -54,7 +61,8 @@ class UserActionManager(val app: App) { MainActivity::class.java, "action" to "userActionRequired", "profileId" to (apiError.profileId ?: -1), - "type" to type + "type" to type, + "params" to apiError.params, ) val pendingIntent = PendingIntent.getActivity(app, System.currentTimeMillis().toInt(), intent, PendingIntent.FLAG_ONE_SHOT or pendingIntentFlag()) @@ -78,23 +86,41 @@ class UserActionManager(val app: App) { activity: AppCompatActivity, profileId: Int?, type: Int, - onSuccess: ((code: String) -> Unit)? = null, + params: Bundle? = null, + onSuccess: ((params: Bundle) -> Unit)? = null, onFailure: (() -> Unit)? = null ) { - if (type != UserActionRequiredEvent.CAPTCHA_LIBRUS) - return + when (type) { + UserActionRequiredEvent.CAPTCHA_LIBRUS -> executeLibrus(activity, profileId, onSuccess, onFailure) + UserActionRequiredEvent.OAUTH_USOS -> executeOauth(activity, profileId, params, onSuccess, onFailure) + } + } + private fun executeLibrus( + activity: AppCompatActivity, + profileId: Int?, + onSuccess: ((params: Bundle) -> Unit)? = null, + onFailure: (() -> Unit)? = null, + ) { if (profileId == null) return // show captcha dialog // use passed onSuccess listener, else sync profile LibrusCaptchaDialog( activity = activity, - onSuccess = onSuccess ?: { code -> - EdziennikTask.syncProfile(profileId, arguments = JsonObject( - "recaptchaCode" to code, - "recaptchaTime" to System.currentTimeMillis() - )).enqueue(activity) + onSuccess = { code -> + if (onSuccess != null) { + val params = Bundle( + "recaptchaCode" to code, + "recaptchaTime" to System.currentTimeMillis(), + ) + onSuccess(params) + } else { + EdziennikTask.syncProfile(profileId, arguments = JsonObject( + "recaptchaCode" to code, + "recaptchaTime" to System.currentTimeMillis(), + )).enqueue(activity) + } }, onFailure = onFailure ).show() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 66ac0299..393248c9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1541,4 +1541,5 @@ USOS Logowanie z użyciem przeglądarki TODO + USOS - wymagane logowanie z użyciem przeglądarki From c7362bce12e5615d36eef04df93ed384491a339f Mon Sep 17 00:00:00 2001 From: kuba2k2 Date: Thu, 13 Oct 2022 21:27:55 +0200 Subject: [PATCH 03/15] [API/Usos] Add base rest API class. --- .../data/api/edziennik/usos/DataUsos.kt | 25 +++ .../data/api/edziennik/usos/data/UsosApi.kt | 170 ++++++++++++++++++ .../data/api/edziennik/usos/data/UsosData.kt | 49 +++++ .../edziennik/ext/TextExtensions.kt | 8 + .../utils/managers/UserActionManager.kt | 16 +- 5 files changed, 266 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt index b4a925bf..1dee14ba 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt @@ -27,6 +27,21 @@ class DataUsos( override fun generateUserCode() = "USOS:TEST" + var instanceUrl: String? + get() { mInstanceUrl = mInstanceUrl ?: loginStore.getLoginData("instanceUrl", null); return mInstanceUrl } + set(value) { loginStore.putLoginData("instanceUrl", value); mInstanceUrl = value } + private var mInstanceUrl: String? = null + + var oauthConsumerKey: String? + get() { mOauthConsumerKey = mOauthConsumerKey ?: loginStore.getLoginData("oauthConsumerKey", null); return mOauthConsumerKey } + set(value) { loginStore.putLoginData("oauthConsumerKey", value); mOauthConsumerKey = value } + private var mOauthConsumerKey: String? = null + + var oauthConsumerSecret: String? + get() { mOauthConsumerSecret = mOauthConsumerSecret ?: loginStore.getLoginData("oauthConsumerSecret", null); return mOauthConsumerSecret } + set(value) { loginStore.putLoginData("oauthConsumerSecret", value); mOauthConsumerSecret = value } + private var mOauthConsumerSecret: String? = null + var oauthTokenKey: String? get() { mOauthTokenKey = mOauthTokenKey ?: loginStore.getLoginData("oauthTokenKey", null); return mOauthTokenKey } set(value) { loginStore.putLoginData("oauthTokenKey", value); mOauthTokenKey = value } @@ -36,4 +51,14 @@ class DataUsos( get() { mOauthTokenSecret = mOauthTokenSecret ?: loginStore.getLoginData("oauthTokenSecret", null); return mOauthTokenSecret } set(value) { loginStore.putLoginData("oauthTokenSecret", value); mOauthTokenSecret = value } private var mOauthTokenSecret: String? = null + + var studentId: String? + get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", null); return mStudentId } + set(value) { profile?.putStudentData("studentId", value) ?: return; mStudentId = value } + private var mStudentId: String? = null + + var studentNumber: String? + get() { mStudentNumber = mStudentNumber ?: profile?.getStudentData("studentNumber", null); return mStudentNumber } + set(value) { profile?.putStudentData("studentNumber", value) ?: return; mStudentNumber = value } + private var mStudentNumber: String? = null } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt new file mode 100644 index 00000000..6ffcd46a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt @@ -0,0 +1,170 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-13. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data + +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import im.wangchao.mhttp.AbsCallbackHandler +import im.wangchao.mhttp.Request +import im.wangchao.mhttp.Response +import im.wangchao.mhttp.body.MediaTypeUtils +import im.wangchao.mhttp.callback.JsonArrayCallbackHandler +import im.wangchao.mhttp.callback.JsonCallbackHandler +import im.wangchao.mhttp.callback.TextCallbackHandler +import pl.szczodrzynski.edziennik.data.api.ERROR_REQUEST_FAILURE +import pl.szczodrzynski.edziennik.data.api.SERVER_USER_AGENT +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.ext.currentTimeUnix +import pl.szczodrzynski.edziennik.ext.hmacSHA1 +import pl.szczodrzynski.edziennik.ext.toQueryString +import pl.szczodrzynski.edziennik.ext.urlEncode +import pl.szczodrzynski.edziennik.utils.Utils.d +import java.net.HttpURLConnection.* +import java.util.UUID + +open class UsosApi(open val data: DataUsos, open val lastSync: Long?) { + companion object { + private const val TAG = "UsosApi" + } + + enum class ResponseType { + OBJECT, + ARRAY, + PLAIN, + } + + val profileId + get() = data.profile?.id ?: -1 + + val profile + get() = data.profile + + private fun valueToString(value: Any) = when (value) { + is String -> value + is Number -> value.toString() + is List<*> -> listToString(value) + else -> value.toString() + } + + private fun listToString(list: List<*>): String { + return list.map { + if (it is Pair<*, *> && it.first is String && it.second is List<*>) + return@map "${it.first}[${listToString(it.second as List<*>)}]" + return@map valueToString(it ?: "") + }.joinToString("|") + } + + private fun buildSignature(method: String, url: String, params: Map): String { + val query = params.toQueryString() + val signatureString = listOf( + method.uppercase(), + url.urlEncode(), + query.urlEncode(), + ).joinToString("&") + val signingKey = listOf( + data.oauthConsumerSecret ?: "", + data.oauthTokenSecret ?: "", + ).joinToString("&") { it.urlEncode() } + return signatureString.hmacSHA1(signingKey) + } + + fun apiRequest( + tag: String, + service: String, + params: Map, + responseType: ResponseType, + onSuccess: (data: T) -> Unit, + ) { + val url = "${data.instanceUrl}/services/$service" + d(tag, "Request: Usos/Api - $url") + val formData = params.mapValues { + valueToString(it.value) + } + val auth = mutableMapOf( + "realm" to url, + "oauth_consumer_key" to (data.oauthConsumerKey ?: ""), + "oauth_nonce" to UUID.randomUUID().toString(), + "oauth_signature_method" to "HMAC-SHA1", + "oauth_timestamp" to currentTimeUnix().toString(), + "oauth_token" to (data.oauthTokenKey ?: ""), + "oauth_version" to "1.0", + ) + val signature = buildSignature("POST", url, formData + auth) + auth["oauth_signature"] = signature + + val authString = auth.map { + """${it.key}="${it.value.urlEncode()}"""" + }.joinToString(", ") + + Request.builder() + .url(url) + .userAgent(SERVER_USER_AGENT) + .addHeader("Authorization", "OAuth $authString") + .post() + .setTextBody(formData.toQueryString(), MediaTypeUtils.APPLICATION_FORM) + .allowErrorCode(HTTP_BAD_REQUEST) + .allowErrorCode(HTTP_UNAUTHORIZED) + .allowErrorCode(HTTP_FORBIDDEN) + .allowErrorCode(HTTP_NOT_FOUND) + .allowErrorCode(HTTP_UNAVAILABLE) + .callback(getCallback(tag, responseType, onSuccess)) + .build() + .enqueue() + } + + @Suppress("UNCHECKED_CAST") + private fun getCallback( + tag: String, + responseType: ResponseType, + onSuccess: (data: T) -> Unit, + ) = when (responseType) { + ResponseType.OBJECT -> object : JsonCallbackHandler() { + override fun onSuccess(data: JsonObject?, response: Response?) { + processResponse(response, data as T, onSuccess) + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + processError(tag, response, throwable) + } + } + ResponseType.ARRAY -> object : JsonArrayCallbackHandler() { + override fun onSuccess(data: JsonArray?, response: Response?) { + processResponse(response, data as T, onSuccess) + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + processError(tag, response, throwable) + } + } + ResponseType.PLAIN -> object : TextCallbackHandler() { + override fun onSuccess(data: String?, response: Response?) { + processResponse(response, data as T, onSuccess) + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + processError(tag, response, throwable) + } + } + } + + private fun processResponse( + response: Response?, + data: T?, + onSuccess: (data: T) -> Unit, + ) { + + } + + private fun processError( + tag: String, + response: Response?, + throwable: Throwable?, + ) { + data.error(ApiError(tag, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt new file mode 100644 index 00000000..bea52ea3 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-13. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data + +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.web.TemplateWebSample +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_USER +import pl.szczodrzynski.edziennik.utils.Utils.d + +class UsosData(val data: DataUsos, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "UsosData" + } + + init { + nextEndpoint(onSuccess) + } + + private fun nextEndpoint(onSuccess: () -> Unit) { + if (data.targetEndpointIds.isEmpty()) { + onSuccess() + return + } + if (data.cancelled) { + onSuccess() + return + } + val id = data.targetEndpointIds.firstKey() + val lastSync = data.targetEndpointIds.remove(id) + useEndpoint(id, lastSync) { endpointId -> + data.progress(data.progressStep) + nextEndpoint(onSuccess) + } + } + + private fun useEndpoint(endpointId: Int, lastSync: Long?, onSuccess: (endpointId: Int) -> Unit) { + d(TAG, "Using endpoint $endpointId. Last sync time = $lastSync") + when (endpointId) { + ENDPOINT_USOS_API_USER -> { + data.startProgress(R.string.edziennik_progress_endpoint_student_info) + TemplateWebSample(data, lastSync, onSuccess) + } + else -> onSuccess(endpointId) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt index ebfb1410..84fe6631 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt @@ -18,6 +18,7 @@ import android.text.style.StyleSpan import androidx.annotation.PluralsRes import androidx.annotation.StringRes import com.mikepenz.materialdrawer.holder.StringHolder +import java.net.URLEncoder fun CharSequence?.isNotNullNorEmpty(): Boolean { return this != null && this.isNotEmpty() @@ -343,3 +344,10 @@ fun Int.toStringHolder() = StringHolder(this) fun CharSequence.toStringHolder() = StringHolder(this) fun @receiver:StringRes Int.resolveString(context: Context) = context.getString(this) + +fun String.urlEncode(): String = URLEncoder.encode(this, "UTF-8").replace("+", "%20") + +fun Map.toQueryString() = this + .map { it.key.urlEncode() to it.value.urlEncode() } + .sortedBy { it.first } + .joinToString("&") { "${it.first}=${it.second}" } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt index 76d5733d..537915ac 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt @@ -99,8 +99,8 @@ class UserActionManager(val app: App) { private fun executeLibrus( activity: AppCompatActivity, profileId: Int?, - onSuccess: ((params: Bundle) -> Unit)? = null, - onFailure: (() -> Unit)? = null, + onSuccess: ((params: Bundle) -> Unit)?, + onFailure: (() -> Unit)?, ) { if (profileId == null) return @@ -125,4 +125,16 @@ class UserActionManager(val app: App) { onFailure = onFailure ).show() } + + private fun executeOauth( + activity: AppCompatActivity, + profileId: Int?, + params: Bundle?, + onSuccess: ((params: Bundle) -> Unit)?, + onFailure: (() -> Unit)?, + ) { + if (profileId == null || params == null) + return + + } } From 9f3aaf6e8685f40731fa1a4a6200ea6ca648ffb4 Mon Sep 17 00:00:00 2001 From: kuba2k2 Date: Fri, 14 Oct 2022 14:43:40 +0200 Subject: [PATCH 04/15] [API] Move register platforms to new endpoint. --- .../edziennik/data/api/szkolny/SzkolnyApi.kt | 4 ++-- .../edziennik/data/api/szkolny/SzkolnyService.kt | 4 ++-- .../edziennik/ui/login/LoginFormFragment.kt | 12 ++++++++---- .../pl/szczodrzynski/edziennik/ui/login/LoginInfo.kt | 4 +++- .../edziennik/ui/login/LoginPlatformListFragment.kt | 3 ++- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt index c86ae235..a9f6a69f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt @@ -451,9 +451,9 @@ class SzkolnyApi(val app: App) : CoroutineScope { @Throws(Exception::class) fun getRealms(registerName: String): List { - val response = api.fsLoginRealms(registerName).execute() + val response = api.platforms(registerName).execute() if (response.isSuccessful && response.body() != null) { - return response.body()!! + return parseResponse(response) } throw SzkolnyApiException(null) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyService.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyService.kt index 5c60c3ff..cca9475a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyService.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyService.kt @@ -45,6 +45,6 @@ interface SzkolnyService { @GET("registerAvailability") fun registerAvailability(): Call>> - @GET("https://szkolny-eu.github.io/FSLogin/realms/{registerName}.json") - fun fsLoginRealms(@Path("registerName") registerName: String): Call> + @GET("platforms/{registerName}") + fun platforms(@Path("registerName") registerName: String): Call>> } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginFormFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginFormFragment.kt index 3c5a26ca..5586dd77 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginFormFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginFormFragment.kt @@ -33,7 +33,6 @@ import pl.szczodrzynski.edziennik.ui.login.LoginInfo.BaseCredential import pl.szczodrzynski.edziennik.ui.login.LoginInfo.FormCheckbox import pl.szczodrzynski.edziennik.ui.login.LoginInfo.FormField import pl.szczodrzynski.navlib.colorAttr -import java.util.* import kotlin.coroutines.CoroutineContext class LoginFormFragment : Fragment(), CoroutineScope { @@ -63,8 +62,10 @@ class LoginFormFragment : Fragment(), CoroutineScope { get() = arguments?.getString("platformDescription") private val platformFormFields get() = arguments?.getString("platformFormFields")?.split(";") - private val platformRealmData - get() = arguments?.getString("platformRealmData")?.toJsonObject() + private val platformData + get() = arguments?.getString("platformData")?.toJsonObject() + private val platformStoreKey + get() = arguments?.getString("platformStoreKey") override fun onCreateView( inflater: LayoutInflater, @@ -250,7 +251,10 @@ class LoginFormFragment : Fragment(), CoroutineScope { payload.putBoolean("fakeLogin", true) } - payload.putBundle("webRealmData", platformRealmData?.toBundle()) + if (platformStoreKey == null) + payload.putAll(platformData?.toBundle()) + else + payload.putBundle(platformStoreKey, platformData?.toBundle()) var hasErrors = false credentials.forEach { (credential, b) -> diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginInfo.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginInfo.kt index be6d62d8..15a7021d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginInfo.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginInfo.kt @@ -6,6 +6,7 @@ package pl.szczodrzynski.edziennik.ui.login import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import com.google.gson.JsonObject import com.mikepenz.iconics.typeface.IIcon import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import pl.szczodrzynski.edziennik.R @@ -374,7 +375,8 @@ object LoginInfo { val icon: String, val screenshot: String?, val formFields: List, - val realmData: RealmData + val data: JsonObject, + val storeKey: String?, ) open class BaseCredential( diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginPlatformListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginPlatformListFragment.kt index 8a2bb25d..cff801a4 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginPlatformListFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginPlatformListFragment.kt @@ -68,7 +68,8 @@ class LoginPlatformListFragment : Fragment(), CoroutineScope { "platformName" to platform.name, "platformDescription" to platform.description, "platformFormFields" to platform.formFields.joinToString(";"), - "platformRealmData" to app.gson.toJson(platform.realmData) + "platformData" to platform.data, + "platformStoreKey" to platform.storeKey, ), activity.navOptions) } From 6c96875c8332b8902d7ecf19038a58ce1cd465c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Fri, 14 Oct 2022 19:44:22 +0200 Subject: [PATCH 05/15] [API/Login] Allow passing LoginStore params in user action requests. --- .../edziennik/ui/login/LoginFormFragment.kt | 7 ++++- .../ui/login/LoginPlatformListFragment.kt | 2 +- .../utils/managers/UserActionManager.kt | 30 ++++++++++--------- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginFormFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginFormFragment.kt index 5586dd77..b403568b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginFormFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginFormFragment.kt @@ -91,6 +91,11 @@ class LoginFormFragment : Fragment(), CoroutineScope { val loginMode = arguments?.getInt("loginMode") ?: return val mode = register.loginModes.firstOrNull { it.loginMode == loginMode } ?: return + if (mode.credentials.isEmpty()) { + login(loginType, loginMode) + return + } + b.title.setText(R.string.login_form_title_format, app.getString(register.registerName)) b.subTitle.text = platformName ?: app.getString(mode.name) b.text.text = platformGuideText ?: app.getString(mode.guideText) @@ -252,7 +257,7 @@ class LoginFormFragment : Fragment(), CoroutineScope { } if (platformStoreKey == null) - payload.putAll(platformData?.toBundle()) + payload.putAll(platformData?.toBundle() ?: Bundle()) else payload.putBundle(platformStoreKey, platformData?.toBundle()) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginPlatformListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginPlatformListFragment.kt index cff801a4..5629487f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginPlatformListFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginPlatformListFragment.kt @@ -68,7 +68,7 @@ class LoginPlatformListFragment : Fragment(), CoroutineScope { "platformName" to platform.name, "platformDescription" to platform.description, "platformFormFields" to platform.formFields.joinToString(";"), - "platformData" to platform.data, + "platformData" to platform.data.toString(), "platformStoreKey" to platform.storeKey, ), activity.navOptions) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt index 537915ac..bbdb8602 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt @@ -19,11 +19,9 @@ import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_OAUTH_LOGIN_REQUEST import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent import pl.szczodrzynski.edziennik.data.api.models.ApiError -import pl.szczodrzynski.edziennik.ext.Bundle -import pl.szczodrzynski.edziennik.ext.Intent -import pl.szczodrzynski.edziennik.ext.JsonObject -import pl.szczodrzynski.edziennik.ext.pendingIntentFlag +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.ui.captcha.LibrusCaptchaDialog +import pl.szczodrzynski.edziennik.utils.Utils.d class UserActionManager(val app: App) { companion object { @@ -90,8 +88,9 @@ class UserActionManager(val app: App) { onSuccess: ((params: Bundle) -> Unit)? = null, onFailure: (() -> Unit)? = null ) { + d(TAG, "Running user action ($type) with params: ${params?.toJsonObject()}") when (type) { - UserActionRequiredEvent.CAPTCHA_LIBRUS -> executeLibrus(activity, profileId, onSuccess, onFailure) + UserActionRequiredEvent.CAPTCHA_LIBRUS -> executeLibrus(activity, profileId, params, onSuccess, onFailure) UserActionRequiredEvent.OAUTH_USOS -> executeOauth(activity, profileId, params, onSuccess, onFailure) } } @@ -99,27 +98,29 @@ class UserActionManager(val app: App) { private fun executeLibrus( activity: AppCompatActivity, profileId: Int?, + params: Bundle?, onSuccess: ((params: Bundle) -> Unit)?, onFailure: (() -> Unit)?, ) { if (profileId == null) return + val extras = params?.getBundle("extras") // show captcha dialog // use passed onSuccess listener, else sync profile LibrusCaptchaDialog( activity = activity, onSuccess = { code -> + val args = Bundle( + "recaptchaCode" to code, + "recaptchaTime" to System.currentTimeMillis(), + ) + if (extras != null) + args.putAll(extras) + if (onSuccess != null) { - val params = Bundle( - "recaptchaCode" to code, - "recaptchaTime" to System.currentTimeMillis(), - ) - onSuccess(params) + onSuccess(args) } else { - EdziennikTask.syncProfile(profileId, arguments = JsonObject( - "recaptchaCode" to code, - "recaptchaTime" to System.currentTimeMillis(), - )).enqueue(activity) + EdziennikTask.syncProfile(profileId, arguments = args.toJsonObject()).enqueue(activity) } }, onFailure = onFailure @@ -135,6 +136,7 @@ class UserActionManager(val app: App) { ) { if (profileId == null || params == null) return + val extras = params.getBundle("extras") } } From 2ff784066efc716c82b2843293eb6ecbe9ac86b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Fri, 14 Oct 2022 21:44:58 +0200 Subject: [PATCH 06/15] [API/Usos] Implement OAuth authorization flow. --- .../edziennik/data/api/Constants.kt | 9 +++ .../data/api/edziennik/EdziennikTask.kt | 2 + .../data/api/edziennik/usos/DataUsos.kt | 10 +++ .../edziennik/data/api/edziennik/usos/Usos.kt | 5 +- .../data/api/edziennik/usos/data/UsosApi.kt | 12 ++-- .../data/api/edziennik/usos/data/UsosData.kt | 2 +- .../usos/firstlogin/UsosFirstLogin.kt | 23 +++++++ .../api/edziennik/usos/login/UsosLoginApi.kt | 53 +++++++++++++-- .../edziennik/ext/TextExtensions.kt | 7 ++ .../edziennik/ui/dialogs/OAuthLoginDialog.kt | 67 +++++++++++++++++++ .../edziennik/ui/login/LoginActivity.kt | 5 +- .../edziennik/ui/login/LoginFormFragment.kt | 12 +++- .../utils/managers/UserActionManager.kt | 43 +++++++++--- app/src/main/res/values/strings.xml | 1 + 14 files changed, 225 insertions(+), 26 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/firstlogin/UsosFirstLogin.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/OAuthLoginDialog.kt 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 3df74621..c0318251 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 @@ -99,3 +99,12 @@ const val PODLASIE_API_VERSION = "1.0.62" const val PODLASIE_API_URL = "https://cpdklaser.zeto.bialystok.pl/api" const val PODLASIE_API_USER_ENDPOINT = "/pobierzDaneUcznia" const val PODLASIE_API_LOGOUT_DEVICES_ENDPOINT = "/wyczyscUrzadzenia" + +const val USOS_API_OAUTH_REDIRECT_URL = "szkolny://redirect/usos" + +val USOS_API_SCOPES by lazy { listOf( + "offline_access", + "studies", + "grades", + "events", +) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt index 0b49e38b..097ed29e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt @@ -12,6 +12,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.Librus import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.Mobidziennik import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.Podlasie import pl.szczodrzynski.edziennik.data.api.edziennik.template.Template +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.Usos import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.Vulcan import pl.szczodrzynski.edziennik.data.api.events.RegisterAvailabilityEvent import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback @@ -113,6 +114,7 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa LOGIN_TYPE_VULCAN -> Vulcan(app, profile, loginStore, taskCallback) LOGIN_TYPE_PODLASIE -> Podlasie(app, profile, loginStore, taskCallback) LOGIN_TYPE_TEMPLATE -> Template(app, profile, loginStore, taskCallback) + LOGIN_TYPE_USOS -> Usos(app, profile, loginStore, taskCallback) else -> null } if (edziennikInterface == null) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt index 1dee14ba..f374db94 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt @@ -27,11 +27,21 @@ class DataUsos( override fun generateUserCode() = "USOS:TEST" + var schoolId: String? + get() { mSchoolId = mSchoolId ?: loginStore.getLoginData("schoolId", null); return mSchoolId } + set(value) { loginStore.putLoginData("schoolId", value); mSchoolId = value } + private var mSchoolId: String? = null + var instanceUrl: String? get() { mInstanceUrl = mInstanceUrl ?: loginStore.getLoginData("instanceUrl", null); return mInstanceUrl } set(value) { loginStore.putLoginData("instanceUrl", value); mInstanceUrl = value } private var mInstanceUrl: String? = null + var oauthLoginResponse: String? + get() { mOauthLoginResponse = mOauthLoginResponse ?: loginStore.getLoginData("oauthLoginResponse", null); return mOauthLoginResponse } + set(value) { loginStore.putLoginData("oauthLoginResponse", value); mOauthLoginResponse = value } + private var mOauthLoginResponse: String? = null + var oauthConsumerKey: String? get() { mOauthConsumerKey = mOauthConsumerKey ?: loginStore.getLoginData("oauthConsumerKey", null); return mOauthConsumerKey } set(value) { loginStore.putLoginData("oauthConsumerKey", value); mOauthConsumerKey = value } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/Usos.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/Usos.kt index c97e3695..4ff9d04c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/Usos.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/Usos.kt @@ -6,6 +6,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.usos import com.google.gson.JsonObject import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.firstlogin.UsosFirstLogin import pl.szczodrzynski.edziennik.data.api.edziennik.usos.login.UsosLogin import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface @@ -71,9 +72,9 @@ class Usos( override fun getEvent(eventFull: EventFull) {} override fun firstLogin() { - /*UsosFirstLogin(data) { + UsosFirstLogin(data) { completed() - }*/ + } } override fun cancel() { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt index 6ffcd46a..ac73e29a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt @@ -78,7 +78,7 @@ open class UsosApi(open val data: DataUsos, open val lastSync: Long?) { responseType: ResponseType, onSuccess: (data: T) -> Unit, ) { - val url = "${data.instanceUrl}/services/$service" + val url = "${data.instanceUrl}services/$service" d(tag, "Request: Usos/Api - $url") val formData = params.mapValues { valueToString(it.value) @@ -92,7 +92,11 @@ open class UsosApi(open val data: DataUsos, open val lastSync: Long?) { "oauth_token" to (data.oauthTokenKey ?: ""), "oauth_version" to "1.0", ) - val signature = buildSignature("POST", url, formData + auth) + val signature = buildSignature( + method = "POST", + url = url, + params = formData + auth.filterKeys { it.startsWith("oauth_") }, + ) auth["oauth_signature"] = signature val authString = auth.map { @@ -152,10 +156,10 @@ open class UsosApi(open val data: DataUsos, open val lastSync: Long?) { private fun processResponse( response: Response?, - data: T?, + data: T, onSuccess: (data: T) -> Unit, ) { - + onSuccess(data) } private fun processError( diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt index bea52ea3..5ad199ff 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt @@ -41,7 +41,7 @@ class UsosData(val data: DataUsos, val onSuccess: () -> Unit) { when (endpointId) { ENDPOINT_USOS_API_USER -> { data.startProgress(R.string.edziennik_progress_endpoint_student_info) - TemplateWebSample(data, lastSync, onSuccess) +// TemplateWebSample(data, lastSync, onSuccess) } else -> onSuccess(endpointId) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/firstlogin/UsosFirstLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/firstlogin/UsosFirstLogin.kt new file mode 100644 index 00000000..3094980c --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/firstlogin/UsosFirstLogin.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-14. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos.firstlogin + +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.login.UsosLoginApi + +class UsosFirstLogin(val data: DataUsos, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "UsosFirstLogin" + } + + private val api = UsosApi(data, null) + + init { + UsosLoginApi(data) { + + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt index bdf771c3..3027c334 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt @@ -4,10 +4,17 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.usos.login -import pl.szczodrzynski.edziennik.data.api.ERROR_PROFILE_MISSING import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_OAUTH_LOGIN_REQUEST +import pl.szczodrzynski.edziennik.data.api.USOS_API_OAUTH_REDIRECT_URL +import pl.szczodrzynski.edziennik.data.api.USOS_API_SCOPES import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.ext.Bundle +import pl.szczodrzynski.edziennik.ext.fromQueryString +import pl.szczodrzynski.edziennik.ext.toBundle +import pl.szczodrzynski.edziennik.ext.toQueryString +import pl.szczodrzynski.edziennik.utils.Utils.d class UsosLoginApi(val data: DataUsos, val onSuccess: () -> Unit) { companion object { @@ -15,15 +22,47 @@ class UsosLoginApi(val data: DataUsos, val onSuccess: () -> Unit) { } init { run { - if (data.profile == null) { - data.error(ApiError(TAG, ERROR_PROFILE_MISSING)) - return@run - } - if (data.isApiLoginValid()) { onSuccess() + } else if (data.oauthLoginResponse != null) { + login() } else { - data.error(ApiError(TAG, ERROR_USOS_OAUTH_LOGIN_REQUEST)) + authorize() } }} + + private fun authorize() { + val api = UsosApi(data, null) + + api.apiRequest( + tag = TAG, + service = "oauth/request_token", + params = mapOf( + "oauth_callback" to USOS_API_OAUTH_REDIRECT_URL, + "scopes" to USOS_API_SCOPES, + ), + responseType = UsosApi.ResponseType.PLAIN, + ) { + val response = it.fromQueryString() + data.oauthTokenKey = response["oauth_token"] + data.oauthTokenSecret = response["oauth_token_secret"] + + val authUrl = "${data.instanceUrl}services/oauth/authorize" + val authParams = mapOf( + "interactivity" to "confirm_user", + "oauth_token" to (data.oauthTokenKey ?: ""), + ) + val params = Bundle( + "authorizeUrl" to "$authUrl?${authParams.toQueryString()}", + "redirectUrl" to USOS_API_OAUTH_REDIRECT_URL, + "responseStoreKey" to "oauthLoginResponse", + "extras" to data.loginStore.data.toBundle(), + ) + data.error(ApiError(TAG, ERROR_USOS_OAUTH_LOGIN_REQUEST).withParams(params)) + } + } + + private fun login() { + d(TAG, "Login to ${data.schoolId} with ${data.oauthLoginResponse} (${data.oauthTokenSecret})") + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt index 84fe6631..d7717c7b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt @@ -18,6 +18,7 @@ import android.text.style.StyleSpan import androidx.annotation.PluralsRes import androidx.annotation.StringRes import com.mikepenz.materialdrawer.holder.StringHolder +import java.net.URLDecoder import java.net.URLEncoder fun CharSequence?.isNotNullNorEmpty(): Boolean { @@ -346,8 +347,14 @@ fun CharSequence.toStringHolder() = StringHolder(this) fun @receiver:StringRes Int.resolveString(context: Context) = context.getString(this) fun String.urlEncode(): String = URLEncoder.encode(this, "UTF-8").replace("+", "%20") +fun String.urlDecode(): String = URLDecoder.decode(this, "UTF-8") fun Map.toQueryString() = this .map { it.key.urlEncode() to it.value.urlEncode() } .sortedBy { it.first } .joinToString("&") { "${it.first}=${it.second}" } + +fun String.fromQueryString() = this + .split("&") + .map { it.split("=") } + .associate { it[0].urlDecode() to it[1].urlDecode() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/OAuthLoginDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/OAuthLoginDialog.kt new file mode 100644 index 00000000..b777377e --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/OAuthLoginDialog.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-14. + */ + +package pl.szczodrzynski.edziennik.ui.dialogs + +import android.annotation.SuppressLint +import android.graphics.Bitmap +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.FrameLayout +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.updateLayoutParams +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.ext.dp +import pl.szczodrzynski.edziennik.ui.dialogs.base.ViewDialog +import pl.szczodrzynski.edziennik.utils.Utils.d + +class OAuthLoginDialog( + activity: AppCompatActivity, + private val authorizeUrl: String, + private val redirectUrl: String, + private val onSuccess: (responseUrl: String) -> Unit, + private val onFailure: (() -> Unit)?, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : ViewDialog(activity, onShowListener, onDismissListener) { + + override val TAG = "OAuthLoginDialog" + + override fun getTitleRes() = R.string.oauth_dialog_title + override fun getPositiveButtonText() = R.string.close + + private var isSuccessful = false + + @SuppressLint("SetJavaScriptEnabled") + override fun getRootView(): WebView { + val webView = WebView(activity) + webView.webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { + d(TAG, "Navigating to $url") + if (url.startsWith(redirectUrl)) { + isSuccessful = true + onSuccess(url) + dismiss() + } + } + } + webView.settings.javaScriptEnabled = true + return webView + } + + override suspend fun onShow() { + dialog.window?.setLayout(MATCH_PARENT, MATCH_PARENT) + root.minimumHeight = activity.windowManager.defaultDisplay?.height?.div(2) ?: 300.dp + root.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + root.loadUrl(authorizeUrl) + } + + override fun onDismiss() { + root.stopLoading() + if (!isSuccessful) + onFailure?.invoke() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginActivity.kt index 61aa4520..08d4917d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginActivity.kt @@ -31,6 +31,7 @@ class LoginActivity : AppCompatActivity(), CoroutineScope { private val app: App by lazy { applicationContext as App } private lateinit var b: LoginActivityBinding lateinit var navOptions: NavOptions + lateinit var navOptionsBuilder: NavOptions.Builder val nav by lazy { Navigation.findNavController(this, R.id.nav_host_fragment) } val errorSnackbar: ErrorSnackbar by lazy { ErrorSnackbar(this) } val swipeRefreshLayout: SwipeRefreshLayoutNoTouch by lazy { b.swipeRefreshLayout } @@ -87,12 +88,12 @@ class LoginActivity : AppCompatActivity(), CoroutineScope { super.onCreate(savedInstanceState) setTheme(R.style.AppTheme_Light) - navOptions = NavOptions.Builder() + navOptionsBuilder = NavOptions.Builder() .setEnterAnim(R.anim.slide_in_right) .setExitAnim(R.anim.slide_out_left) .setPopEnterAnim(R.anim.slide_in_left) .setPopExitAnim(R.anim.slide_out_right) - .build() + navOptions = navOptionsBuilder.build() b = LoginActivityBinding.inflate(layoutInflater) setContentView(b.root) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginFormFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginFormFragment.kt index b403568b..12daadcd 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginFormFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginFormFragment.kt @@ -14,6 +14,8 @@ import android.widget.Toast import androidx.core.view.isVisible import androidx.core.widget.addTextChangedListener import androidx.fragment.app.Fragment +import androidx.navigation.NavOptions +import androidx.navigation.navOptions import androidx.viewbinding.ViewBinding import com.google.android.material.textfield.TextInputLayout import com.mikepenz.iconics.IconicsDrawable @@ -304,6 +306,14 @@ class LoginFormFragment : Fragment(), CoroutineScope { if (hasErrors) return - nav.navigate(R.id.loginProgressFragment, payload, activity.navOptions) + val navOptions = + if (credentials.isEmpty()) + activity.navOptionsBuilder + .setPopUpTo(R.id.loginPlatformListFragment, inclusive = false) + .build() + else + activity.navOptions + + nav.navigate(R.id.loginProgressFragment, payload, navOptions) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt index bbdb8602..c848c47a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt @@ -21,6 +21,7 @@ import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.ui.captcha.LibrusCaptchaDialog +import pl.szczodrzynski.edziennik.ui.dialogs.OAuthLoginDialog import pl.szczodrzynski.edziennik.utils.Utils.d class UserActionManager(val app: App) { @@ -89,9 +90,13 @@ class UserActionManager(val app: App) { onFailure: (() -> Unit)? = null ) { d(TAG, "Running user action ($type) with params: ${params?.toJsonObject()}") - when (type) { + val isSuccessful = when (type) { UserActionRequiredEvent.CAPTCHA_LIBRUS -> executeLibrus(activity, profileId, params, onSuccess, onFailure) UserActionRequiredEvent.OAUTH_USOS -> executeOauth(activity, profileId, params, onSuccess, onFailure) + else -> false + } + if (!isSuccessful) { + onFailure?.invoke() } } @@ -101,9 +106,9 @@ class UserActionManager(val app: App) { params: Bundle?, onSuccess: ((params: Bundle) -> Unit)?, onFailure: (() -> Unit)?, - ) { + ): Boolean { if (profileId == null) - return + return false val extras = params?.getBundle("extras") // show captcha dialog // use passed onSuccess listener, else sync profile @@ -117,14 +122,14 @@ class UserActionManager(val app: App) { if (extras != null) args.putAll(extras) - if (onSuccess != null) { + if (onSuccess != null) onSuccess(args) - } else { + else EdziennikTask.syncProfile(profileId, arguments = args.toJsonObject()).enqueue(activity) - } }, - onFailure = onFailure + onFailure = onFailure, ).show() + return true } private fun executeOauth( @@ -133,10 +138,30 @@ class UserActionManager(val app: App) { params: Bundle?, onSuccess: ((params: Bundle) -> Unit)?, onFailure: (() -> Unit)?, - ) { + ): Boolean { if (profileId == null || params == null) - return + return false val extras = params.getBundle("extras") + val storeKey = params.getString("responseStoreKey") ?: return false + OAuthLoginDialog( + activity = activity, + authorizeUrl = params.getString("authorizeUrl") ?: return false, + redirectUrl = params.getString("redirectUrl") ?: return false, + onSuccess = { responseUrl -> + val args = Bundle( + storeKey to responseUrl, + ) + if (extras != null) + args.putAll(extras) + + if (onSuccess != null) + onSuccess(args) + else + EdziennikTask.syncProfile(profileId, arguments = args.toJsonObject()).enqueue(activity) + }, + onFailure = onFailure, + ).show() + return true } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 393248c9..7aae860c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1542,4 +1542,5 @@ Logowanie z użyciem przeglądarki TODO USOS - wymagane logowanie z użyciem przeglądarki + Zaloguj się From 7ded400a30e6aa8a39fc417648cd119649f5838c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sat, 15 Oct 2022 19:07:12 +0200 Subject: [PATCH 07/15] [API/Usos] Implement first login. --- app/src/main/AndroidManifest.xml | 4 + .../edziennik/data/api/Errors.kt | 3 + .../data/api/edziennik/usos/DataUsos.kt | 9 ++- .../data/api/edziennik/usos/data/UsosApi.kt | 16 ++-- .../usos/firstlogin/UsosFirstLogin.kt | 63 ++++++++++++++- .../api/edziennik/usos/login/UsosLoginApi.kt | 73 +++++++++++++----- .../edziennik/ext/JsonExtensions.kt | 10 ++- .../edziennik/ext/TextExtensions.kt | 1 + .../edziennik/ui/dialogs/OAuthLoginDialog.kt | 67 ---------------- .../edziennik/ui/login/LoginInfo.kt | 2 +- .../ui/login/oauth/OAuthLoginActivity.kt | 64 +++++++++++++++ .../ui/login/oauth/OAuthLoginResult.kt | 10 +++ .../utils/managers/UserActionManager.kt | 49 +++++++----- app/src/main/res/drawable/login_logo_usos.png | Bin 15635 -> 16760 bytes .../main/res/drawable/login_mode_usos_api.png | Bin 0 -> 5392 bytes 15 files changed, 255 insertions(+), 116 deletions(-) delete mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/OAuthLoginDialog.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/login/oauth/OAuthLoginActivity.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/login/oauth/OAuthLoginResult.kt create mode 100644 app/src/main/res/drawable/login_mode_usos_api.png diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c5c273b4..fb824573 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -157,6 +157,10 @@ android:configChanges="orientation|keyboardHidden" android:exported="false" android:theme="@style/Base.Theme.AppCompat" /> + 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 617ef5ed..10405e5f 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 @@ -205,6 +205,9 @@ const val ERROR_PODLASIE_API_OTHER = 631 const val ERROR_PODLASIE_API_DATA_MISSING = 632 const val ERROR_USOS_OAUTH_LOGIN_REQUEST = 701 +const val ERROR_USOS_OAUTH_GOT_DIFFERENT_TOKEN = 702 +const val ERROR_USOS_OAUTH_INCOMPLETE_RESPONSE = 703 +const val ERROR_USOS_NO_STUDENT_PROGRAMMES = 704 const val ERROR_TEMPLATE_WEB_OTHER = 801 diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt index f374db94..0fd1b2ba 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt @@ -16,7 +16,7 @@ class DataUsos( loginStore: LoginStore, ) : Data(app, profile, loginStore) { - fun isApiLoginValid() = oauthTokenKey != null && oauthTokenSecret != null + fun isApiLoginValid() = oauthTokenKey != null && oauthTokenSecret != null && oauthTokenIsUser override fun satisfyLoginMethods() { loginMethods.clear() @@ -25,7 +25,7 @@ class DataUsos( } } - override fun generateUserCode() = "USOS:TEST" + override fun generateUserCode() = "$schoolId:${studentNumber ?: studentId}" var schoolId: String? get() { mSchoolId = mSchoolId ?: loginStore.getLoginData("schoolId", null); return mSchoolId } @@ -62,6 +62,11 @@ class DataUsos( set(value) { loginStore.putLoginData("oauthTokenSecret", value); mOauthTokenSecret = value } private var mOauthTokenSecret: String? = null + var oauthTokenIsUser: Boolean + get() { mOauthTokenIsUser = mOauthTokenIsUser ?: loginStore.getLoginData("oauthTokenIsUser", false); return mOauthTokenIsUser ?: false } + set(value) { loginStore.putLoginData("oauthTokenIsUser", value); mOauthTokenIsUser = value } + private var mOauthTokenIsUser: Boolean? = null + var studentId: String? get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", null); return mStudentId } set(value) { profile?.putStudentData("studentId", value) ?: return; mStudentId = value } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt index ac73e29a..1d3ab69c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt @@ -76,7 +76,7 @@ open class UsosApi(open val data: DataUsos, open val lastSync: Long?) { service: String, params: Map, responseType: ResponseType, - onSuccess: (data: T) -> Unit, + onSuccess: (data: T, response: Response?) -> Unit, ) { val url = "${data.instanceUrl}services/$service" d(tag, "Request: Usos/Api - $url") @@ -123,10 +123,10 @@ open class UsosApi(open val data: DataUsos, open val lastSync: Long?) { private fun getCallback( tag: String, responseType: ResponseType, - onSuccess: (data: T) -> Unit, + onSuccess: (data: T, response: Response?) -> Unit, ) = when (responseType) { ResponseType.OBJECT -> object : JsonCallbackHandler() { - override fun onSuccess(data: JsonObject?, response: Response?) { + override fun onSuccess(data: JsonObject?, response: Response) { processResponse(response, data as T, onSuccess) } @@ -135,7 +135,7 @@ open class UsosApi(open val data: DataUsos, open val lastSync: Long?) { } } ResponseType.ARRAY -> object : JsonArrayCallbackHandler() { - override fun onSuccess(data: JsonArray?, response: Response?) { + override fun onSuccess(data: JsonArray?, response: Response) { processResponse(response, data as T, onSuccess) } @@ -144,7 +144,7 @@ open class UsosApi(open val data: DataUsos, open val lastSync: Long?) { } } ResponseType.PLAIN -> object : TextCallbackHandler() { - override fun onSuccess(data: String?, response: Response?) { + override fun onSuccess(data: String?, response: Response) { processResponse(response, data as T, onSuccess) } @@ -155,11 +155,11 @@ open class UsosApi(open val data: DataUsos, open val lastSync: Long?) { } private fun processResponse( - response: Response?, + response: Response, data: T, - onSuccess: (data: T) -> Unit, + onSuccess: (data: T, response: Response?) -> Unit, ) { - onSuccess(data) + onSuccess(data, response) } private fun processError( diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/firstlogin/UsosFirstLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/firstlogin/UsosFirstLogin.kt index 3094980c..ad51cf92 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/firstlogin/UsosFirstLogin.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/firstlogin/UsosFirstLogin.kt @@ -4,9 +4,18 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.usos.firstlogin +import com.google.gson.JsonObject +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_NO_STUDENT_PROGRAMMES +import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_USOS +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.firstlogin.LibrusFirstLogin import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi import pl.szczodrzynski.edziennik.data.api.edziennik.usos.login.UsosLoginApi +import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.ext.* class UsosFirstLogin(val data: DataUsos, val onSuccess: () -> Unit) { companion object { @@ -16,8 +25,60 @@ class UsosFirstLogin(val data: DataUsos, val onSuccess: () -> Unit) { private val api = UsosApi(data, null) init { - UsosLoginApi(data) { + val loginStoreId = data.loginStore.id + val loginStoreType = LOGIN_TYPE_USOS + var firstProfileId = loginStoreId + UsosLoginApi(data) { + api.apiRequest( + tag = TAG, + service = "users/user", + params = mapOf( + "fields" to listOf( + "id", + "first_name", + "last_name", + "student_number", + "student_programmes" to listOf( + "programme" to listOf("id"), + ), + ), + ), + responseType = UsosApi.ResponseType.OBJECT, + ) { json, response -> + val programmes = json.getJsonArray("student_programmes") + if (programmes.isNullOrEmpty()) { + data.error(ApiError(TAG, ERROR_USOS_NO_STUDENT_PROGRAMMES) + .withApiResponse(json) + .withResponse(response)) + return@apiRequest + } + + val firstName = json.getString("first_name") + val lastName = json.getString("last_name") + val studentName = buildFullName(firstName, lastName) + + val profile = Profile( + id = firstProfileId++, + loginStoreId = loginStoreId, loginStoreType = loginStoreType, + name = studentName, + subname = data.schoolId, + studentNameLong = studentName, + studentNameShort = studentName.getShortName(), + accountName = null, // student account + studentData = JsonObject( + "studentId" to json.getInt("id"), + "studentNumber" to json.getInt("student_number"), + ), + ).also { + it.studentClassName = programmes.getJsonObject(0).getJsonObject("programme").getString("id") + } + + EventBus.getDefault().postSticky( + FirstLoginFinishedEvent(listOf(profile), data.loginStore), + ) + onSuccess() + } } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt index 3027c334..f49c0cea 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt @@ -4,9 +4,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.usos.login -import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_OAUTH_LOGIN_REQUEST -import pl.szczodrzynski.edziennik.data.api.USOS_API_OAUTH_REDIRECT_URL -import pl.szczodrzynski.edziennik.data.api.USOS_API_SCOPES +import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi import pl.szczodrzynski.edziennik.data.api.models.ApiError @@ -21,19 +19,21 @@ class UsosLoginApi(val data: DataUsos, val onSuccess: () -> Unit) { private const val TAG = "UsosLoginApi" } - init { run { - if (data.isApiLoginValid()) { - onSuccess() - } else if (data.oauthLoginResponse != null) { - login() - } else { - authorize() + private val api = UsosApi(data, null) + + init { + run { + if (data.isApiLoginValid()) { + onSuccess() + } else if (data.oauthLoginResponse != null) { + login() + } else { + authorize() + } } - }} + } private fun authorize() { - val api = UsosApi(data, null) - api.apiRequest( tag = TAG, service = "oauth/request_token", @@ -42,10 +42,11 @@ class UsosLoginApi(val data: DataUsos, val onSuccess: () -> Unit) { "scopes" to USOS_API_SCOPES, ), responseType = UsosApi.ResponseType.PLAIN, - ) { - val response = it.fromQueryString() - data.oauthTokenKey = response["oauth_token"] - data.oauthTokenSecret = response["oauth_token_secret"] + ) { text, _ -> + val authorizeData = text.fromQueryString() + data.oauthTokenKey = authorizeData["oauth_token"] + data.oauthTokenSecret = authorizeData["oauth_token_secret"] + data.oauthTokenIsUser = false val authUrl = "${data.instanceUrl}services/oauth/authorize" val authParams = mapOf( @@ -63,6 +64,42 @@ class UsosLoginApi(val data: DataUsos, val onSuccess: () -> Unit) { } private fun login() { - d(TAG, "Login to ${data.schoolId} with ${data.oauthLoginResponse} (${data.oauthTokenSecret})") + d(TAG, "Login to ${data.schoolId} with ${data.oauthLoginResponse}") + + val authorizeResponse = data.oauthLoginResponse?.fromQueryString() + ?: return // checked in init {} + if (authorizeResponse["oauth_token"] != data.oauthTokenKey) { + // got different token + data.error(ApiError(TAG, ERROR_USOS_OAUTH_GOT_DIFFERENT_TOKEN) + .withApiResponse(data.oauthLoginResponse)) + return + } + val verifier = authorizeResponse["oauth_verifier"] + if (verifier.isNullOrBlank()) { + data.error(ApiError(TAG, ERROR_USOS_OAUTH_INCOMPLETE_RESPONSE) + .withApiResponse(data.oauthLoginResponse)) + return + } + + api.apiRequest( + tag = TAG, + service = "oauth/access_token", + params = mapOf( + "oauth_verifier" to verifier, + ), + responseType = UsosApi.ResponseType.PLAIN, + ) { text, response -> + val accessData = text.fromQueryString() + data.oauthTokenKey = accessData["oauth_token"] + data.oauthTokenSecret = accessData["oauth_token_secret"] + data.oauthTokenIsUser = data.oauthTokenKey != null && data.oauthTokenSecret != null + + if (!data.oauthTokenIsUser) + data.error(ApiError(TAG, ERROR_USOS_OAUTH_INCOMPLETE_RESPONSE) + .withApiResponse(text) + .withResponse(response)) + else + onSuccess() + } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/JsonExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/JsonExtensions.kt index 9f0db8ad..14d25b48 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ext/JsonExtensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/JsonExtensions.kt @@ -10,6 +10,8 @@ import com.google.gson.JsonElement import com.google.gson.JsonObject import com.google.gson.JsonParser import com.google.gson.JsonPrimitive +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract fun JsonObject?.get(key: String): JsonElement? = this?.get(key) @@ -93,7 +95,13 @@ fun JsonArray(vararg properties: Any?): JsonArray { } } -fun JsonArray?.isNullOrEmpty(): Boolean = (this?.size() ?: 0) == 0 +@OptIn(ExperimentalContracts::class) +fun JsonArray?.isNullOrEmpty(): Boolean { + contract { + returns(false) implies (this@isNullOrEmpty != null) + } + return this == null || this.isEmpty +} operator fun JsonArray.plusAssign(o: JsonElement) = this.add(o) operator fun JsonArray.plusAssign(o: String) = this.add(o) operator fun JsonArray.plusAssign(o: Char) = this.add(o) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt index d7717c7b..adb6fe55 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt @@ -355,6 +355,7 @@ fun Map.toQueryString() = this .joinToString("&") { "${it.first}=${it.second}" } fun String.fromQueryString() = this + .substringAfter('?') .split("&") .map { it.split("=") } .associate { it[0].urlDecode() to it[1].urlDecode() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/OAuthLoginDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/OAuthLoginDialog.kt deleted file mode 100644 index b777377e..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/OAuthLoginDialog.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) Kuba Szczodrzyński 2022-10-14. - */ - -package pl.szczodrzynski.edziennik.ui.dialogs - -import android.annotation.SuppressLint -import android.graphics.Bitmap -import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams.MATCH_PARENT -import android.webkit.WebView -import android.webkit.WebViewClient -import android.widget.FrameLayout -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.updateLayoutParams -import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.ext.dp -import pl.szczodrzynski.edziennik.ui.dialogs.base.ViewDialog -import pl.szczodrzynski.edziennik.utils.Utils.d - -class OAuthLoginDialog( - activity: AppCompatActivity, - private val authorizeUrl: String, - private val redirectUrl: String, - private val onSuccess: (responseUrl: String) -> Unit, - private val onFailure: (() -> Unit)?, - onShowListener: ((tag: String) -> Unit)? = null, - onDismissListener: ((tag: String) -> Unit)? = null, -) : ViewDialog(activity, onShowListener, onDismissListener) { - - override val TAG = "OAuthLoginDialog" - - override fun getTitleRes() = R.string.oauth_dialog_title - override fun getPositiveButtonText() = R.string.close - - private var isSuccessful = false - - @SuppressLint("SetJavaScriptEnabled") - override fun getRootView(): WebView { - val webView = WebView(activity) - webView.webViewClient = object : WebViewClient() { - override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { - d(TAG, "Navigating to $url") - if (url.startsWith(redirectUrl)) { - isSuccessful = true - onSuccess(url) - dismiss() - } - } - } - webView.settings.javaScriptEnabled = true - return webView - } - - override suspend fun onShow() { - dialog.window?.setLayout(MATCH_PARENT, MATCH_PARENT) - root.minimumHeight = activity.windowManager.defaultDisplay?.height?.div(2) ?: 300.dp - root.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) - root.loadUrl(authorizeUrl) - } - - override fun onDismiss() { - root.stopLoading() - if (!isSuccessful) - onFailure?.invoke() - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginInfo.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginInfo.kt index 15a7021d..8b605734 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginInfo.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginInfo.kt @@ -324,7 +324,7 @@ object LoginInfo { Mode( loginMode = LOGIN_MODE_USOS_OAUTH, name = R.string.login_mode_usos_oauth, - icon = R.drawable.login_logo_usos, + icon = R.drawable.login_mode_usos_api, guideText = R.string.login_mode_usos_oauth_guide, isPlatformSelection = true, credentials = listOf(), diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/oauth/OAuthLoginActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/oauth/OAuthLoginActivity.kt new file mode 100644 index 00000000..c80c3d82 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/oauth/OAuthLoginActivity.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-15. + */ + +package pl.szczodrzynski.edziennik.ui.login.oauth + +import android.annotation.SuppressLint +import android.graphics.Bitmap +import android.os.Bundle +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.FrameLayout +import androidx.appcompat.app.AppCompatActivity +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.utils.Utils.d + +class OAuthLoginActivity : AppCompatActivity() { + companion object { + private const val TAG = "OAuthLoginActivity" + } + + private var isSuccessful = false + + @SuppressLint("SetJavaScriptEnabled") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setTitle(R.string.oauth_dialog_title) + + val authorizeUrl = intent.getStringExtra("authorizeUrl") ?: return + val redirectUrl = intent.getStringExtra("redirectUrl") ?: return + + val webView = WebView(this) + webView.webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { + d(TAG, "Navigating to $url") + if (url.startsWith(redirectUrl)) { + isSuccessful = true + EventBus.getDefault().post(OAuthLoginResult( + isError = false, + responseUrl = url, + )) + finish() + } + } + } + webView.settings.javaScriptEnabled = true + webView.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + setContentView(webView) + + webView.loadUrl(authorizeUrl) + } + + override fun onDestroy() { + super.onDestroy() + if (!isSuccessful) + EventBus.getDefault().post(OAuthLoginResult( + isError = false, + responseUrl = null, + )) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/oauth/OAuthLoginResult.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/oauth/OAuthLoginResult.kt new file mode 100644 index 00000000..108c0c8e --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/oauth/OAuthLoginResult.kt @@ -0,0 +1,10 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-15. + */ + +package pl.szczodrzynski.edziennik.ui.login.oauth + +data class OAuthLoginResult( + val isError: Boolean, + val responseUrl: String?, +) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt index c848c47a..3117c650 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt @@ -11,6 +11,8 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.core.app.NotificationCompat import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.R @@ -21,7 +23,8 @@ import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.ui.captcha.LibrusCaptchaDialog -import pl.szczodrzynski.edziennik.ui.dialogs.OAuthLoginDialog +import pl.szczodrzynski.edziennik.ui.login.oauth.OAuthLoginActivity +import pl.szczodrzynski.edziennik.ui.login.oauth.OAuthLoginResult import pl.szczodrzynski.edziennik.utils.Utils.d class UserActionManager(val app: App) { @@ -143,25 +146,35 @@ class UserActionManager(val app: App) { return false val extras = params.getBundle("extras") val storeKey = params.getString("responseStoreKey") ?: return false + params.getString("authorizeUrl") ?: return false + params.getString("redirectUrl") ?: return false - OAuthLoginDialog( - activity = activity, - authorizeUrl = params.getString("authorizeUrl") ?: return false, - redirectUrl = params.getString("redirectUrl") ?: return false, - onSuccess = { responseUrl -> - val args = Bundle( - storeKey to responseUrl, - ) - if (extras != null) - args.putAll(extras) + var listener: Any? = null + listener = object { + @Subscribe(threadMode = ThreadMode.MAIN) + fun onOAuthLoginResult(result: OAuthLoginResult) { + EventBus.getDefault().unregister(listener) + when { + result.isError -> onFailure?.invoke() + result.responseUrl != null -> { + val args = Bundle( + storeKey to result.responseUrl, + ) + if (extras != null) + args.putAll(extras) - if (onSuccess != null) - onSuccess(args) - else - EdziennikTask.syncProfile(profileId, arguments = args.toJsonObject()).enqueue(activity) - }, - onFailure = onFailure, - ).show() + if (onSuccess != null) + onSuccess(args) + else + EdziennikTask.syncProfile(profileId, arguments = args.toJsonObject()).enqueue(activity) + } + } + } + } + EventBus.getDefault().register(listener) + + val intent = Intent(activity, OAuthLoginActivity::class.java).putExtras(params) + activity.startActivity(intent) return true } } diff --git a/app/src/main/res/drawable/login_logo_usos.png b/app/src/main/res/drawable/login_logo_usos.png index 7c376bddaf28d4462635115fed43a00a484a8a3c..98d95b3496facdc49f2236148d5fa501ce1d2d0c 100644 GIT binary patch literal 16760 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 diff --git a/app/src/main/res/drawable/login_mode_usos_api.png b/app/src/main/res/drawable/login_mode_usos_api.png new file mode 100644 index 0000000000000000000000000000000000000000..6ec32631ce9eb9390d4c71a0b1f308d20e903948 GIT binary patch literal 5392 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 u5nlQ5*SGm#+$-Utt>PQQ|0ni;8~zU Date: Sat, 15 Oct 2022 21:24:30 +0200 Subject: [PATCH 08/15] [API/Login] Make user action handling more universal. --- .../szczodrzynski/edziennik/MainActivity.kt | 16 +- .../edziennik/data/api/ApiService.kt | 18 +- .../edziennik/data/api/Constants.kt | 3 + .../edziennik/data/api/Errors.kt | 7 +- .../data/api/edziennik/librus/Librus.kt | 2 + .../librus/login/LibrusLoginPortal.kt | 22 ++- .../edziennik/mobidziennik/Mobidziennik.kt | 2 + .../data/api/edziennik/podlasie/Podlasie.kt | 5 + .../data/api/edziennik/template/Template.kt | 5 + .../edziennik/data/api/edziennik/usos/Usos.kt | 5 + .../api/edziennik/usos/login/UsosLoginApi.kt | 18 +- .../data/api/edziennik/vulcan/Vulcan.kt | 2 + .../api/events/UserActionRequiredEvent.kt | 16 +- .../data/api/interfaces/EdziennikCallback.kt | 2 + .../edziennik/data/api/models/Data.kt | 15 +- .../edziennik/ext/BundleExtensions.kt | 10 ++ ...tchaDialog.kt => RecaptchaPromptDialog.kt} | 14 +- .../edziennik/ui/home/cards/HomeDebugCard.kt | 7 +- .../edziennik/ui/login/LoginInfo.kt | 1 - .../ui/login/LoginProgressFragment.kt | 25 ++- .../utils/managers/UserActionManager.kt | 164 ++++++++---------- app/src/main/res/layout/card_home_debug.xml | 7 - app/src/main/res/values/errors.xml | 14 -- 23 files changed, 206 insertions(+), 174 deletions(-) rename app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/{LibrusCaptchaDialog.kt => RecaptchaPromptDialog.kt} (88%) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt index 30c3a3e0..000f9be6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt @@ -34,6 +34,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_REQUIRES_USER_ACTION 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.* @@ -69,6 +70,7 @@ import pl.szczodrzynski.edziennik.ui.grades.editor.GradesEditorFragment import pl.szczodrzynski.edziennik.ui.home.HomeFragment import pl.szczodrzynski.edziennik.ui.homework.HomeworkFragment import pl.szczodrzynski.edziennik.ui.login.LoginActivity +import pl.szczodrzynski.edziennik.ui.login.LoginProgressFragment import pl.szczodrzynski.edziennik.ui.messages.compose.MessagesComposeFragment import pl.szczodrzynski.edziennik.ui.messages.list.MessagesFragment import pl.szczodrzynski.edziennik.ui.messages.single.MessageFragment @@ -83,6 +85,7 @@ import pl.szczodrzynski.edziennik.utils.* import pl.szczodrzynski.edziennik.utils.Utils.d import pl.szczodrzynski.edziennik.utils.Utils.dpToPx import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager.Error.Type +import pl.szczodrzynski.edziennik.utils.managers.UserActionManager import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.NavTarget import pl.szczodrzynski.navlib.* @@ -853,7 +856,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope { @Subscribe(threadMode = ThreadMode.MAIN) fun onUserActionRequiredEvent(event: UserActionRequiredEvent) { - app.userActionManager.execute(this, event.profileId, event.type, event.params) + app.userActionManager.execute(this, event, UserActionManager.UserActionCallback()) } private fun fragmentToSyncName(currentFragment: Int): Int { @@ -911,12 +914,13 @@ class MainActivity : AppCompatActivity(), CoroutineScope { false } "userActionRequired" -> { - app.userActionManager.execute( - this, - extras.getInt("profileId"), - extras.getInt("type"), - extras.getBundle("params"), + val event = UserActionRequiredEvent( + profileId = extras.getInt("profileId"), + type = extras.getEnum("type") ?: return, + params = extras.getBundle("params") ?: return, + errorText = 0, ) + app.userActionManager.execute(this, event, UserActionManager.UserActionCallback()) true } "createManualEvent" -> { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/ApiService.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/ApiService.kt index f68e8840..0d9fe92c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/ApiService.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/ApiService.kt @@ -84,19 +84,21 @@ class ApiService : Service() { runTask() } + override fun onRequiresUserAction(event: UserActionRequiredEvent) { + app.userActionManager.sendToUser(event) + taskRunning?.cancel() + clearTask() + runTask() + } + override fun onError(apiError: ApiError) { lastEventTime = System.currentTimeMillis() d(TAG, "Task $taskRunningId threw an error - $apiError") apiError.profileId = taskProfileId - if (app.userActionManager.requiresUserAction(apiError)) { - app.userActionManager.sendToUser(apiError) - } - else { - EventBus.getDefault().postSticky(ApiTaskErrorEvent(apiError)) - errorList.add(apiError) - apiError.throwable?.printStackTrace() - } + EventBus.getDefault().postSticky(ApiTaskErrorEvent(apiError)) + errorList.add(apiError) + apiError.throwable?.printStackTrace() if (apiError.isCritical) { taskRunning?.cancel() 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 c0318251..c29b8698 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 @@ -59,6 +59,9 @@ const val LIBRUS_SANDBOX_URL = "https://sandbox.librus.pl/index.php?action=" const val LIBRUS_SYNERGIA_HOMEWORK_ATTACHMENT_URL = "https://synergia.librus.pl/homework/downloadFile" const val LIBRUS_SYNERGIA_MESSAGES_ATTACHMENT_URL = "https://synergia.librus.pl/wiadomosci/pobierz_zalacznik" +const val LIBRUS_PORTAL_RECAPTCHA_KEY = "6Lf48moUAAAAAB9ClhdvHr46gRWR" +const val LIBRUS_PORTAL_RECAPTCHA_REFERER = "https://portal.librus.pl/rodzina/login" + val MOBIDZIENNIK_USER_AGENT = SYSTEM_USER_AGENT 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 10405e5f..b4e892e6 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 @@ -58,11 +58,7 @@ const val ERROR_INVALID_LOGIN_MODE = 110 const val ERROR_LOGIN_METHOD_NOT_SATISFIED = 111 const val ERROR_NOT_IMPLEMENTED = 112 const val ERROR_FILE_DOWNLOAD = 113 - -const val ERROR_NO_STUDENTS_IN_ACCOUNT = 115 - -const val ERROR_CAPTCHA_NEEDED = 3000 -const val ERROR_CAPTCHA_LIBRUS_PORTAL = 3001 +const val ERROR_REQUIRES_USER_ACTION = 114 const val ERROR_API_PDO_ERROR = 5000 const val ERROR_API_INVALID_CLIENT = 5001 @@ -204,7 +200,6 @@ const val ERROR_PODLASIE_API_NO_TOKEN = 630 const val ERROR_PODLASIE_API_OTHER = 631 const val ERROR_PODLASIE_API_DATA_MISSING = 632 -const val ERROR_USOS_OAUTH_LOGIN_REQUEST = 701 const val ERROR_USOS_OAUTH_GOT_DIFFERENT_TOKEN = 702 const val ERROR_USOS_OAUTH_INCOMPLETE_RESPONSE = 703 const val ERROR_USOS_NO_STUDENT_PROGRAMMES = 704 diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/Librus.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/Librus.kt index abf3ac5f..0830752f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/Librus.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/Librus.kt @@ -16,6 +16,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages.Librus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia.* import pl.szczodrzynski.edziennik.data.api.edziennik.librus.firstlogin.LibrusFirstLogin import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLogin +import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface import pl.szczodrzynski.edziennik.data.api.models.ApiError @@ -162,6 +163,7 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback { return object : EdziennikCallback { override fun onCompleted() { callback.onCompleted() } + override fun onRequiresUserAction(event: UserActionRequiredEvent) { callback.onRequiresUserAction(event) } override fun onProgress(step: Float) { callback.onProgress(step) } override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) } override fun onError(apiError: ApiError) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginPortal.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginPortal.kt index ed01d01f..ab21c692 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginPortal.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginPortal.kt @@ -10,6 +10,7 @@ import im.wangchao.mhttp.callback.TextCallbackHandler import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.Utils.d @@ -148,12 +149,23 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) { val error = if (response.code() == 200) null else json.getJsonArray("errors")?.getString(0) ?: json.getJsonObject("errors")?.entrySet()?.firstOrNull()?.value?.asString + + if (error?.contains("robotem") == true || json.getBoolean("captchaRequired") == true) { + data.requireUserAction( + type = UserActionRequiredEvent.Type.RECAPTCHA, + params = Bundle( + "siteKey" to LIBRUS_PORTAL_RECAPTCHA_KEY, + "referer" to LIBRUS_PORTAL_RECAPTCHA_REFERER, + ), + errorText = R.string.notification_user_action_required_captcha_librus, + ) + return + } + error?.let { code -> when { code.contains("Sesja logowania wygasła") -> ERROR_LOGIN_LIBRUS_PORTAL_CSRF_EXPIRED code.contains("Upewnij się, że nie") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN - // this doesn't work anyway: `errors` is an object with `g-recaptcha-response` set - code.contains("robotem") -> ERROR_CAPTCHA_LIBRUS_PORTAL code.contains("Podany adres e-mail jest nieprawidłowy.") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN else -> ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR }.let { errorCode -> @@ -163,12 +175,6 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) { return } } - if (json.getBoolean("captchaRequired") == true) { - data.error(ApiError(TAG, ERROR_CAPTCHA_LIBRUS_PORTAL) - .withResponse(response) - .withApiResponse(json)) - return - } authorize(json.getString("redirect", LIBRUS_AUTHORIZE_URL)) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/Mobidziennik.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/Mobidziennik.kt index e19fabca..0144ad5d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/Mobidziennik.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/Mobidziennik.kt @@ -11,6 +11,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.Mobidzien import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web.* import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.firstlogin.MobidziennikFirstLogin import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login.MobidziennikLogin +import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface import pl.szczodrzynski.edziennik.data.api.models.ApiError @@ -142,6 +143,7 @@ class Mobidziennik(val app: App, val profile: Profile?, val loginStore: LoginSto private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback { return object : EdziennikCallback { override fun onCompleted() { callback.onCompleted() } + override fun onRequiresUserAction(event: UserActionRequiredEvent) { callback.onRequiresUserAction(event) } override fun onProgress(step: Float) { callback.onProgress(step) } override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) } override fun onError(apiError: ApiError) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/Podlasie.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/Podlasie.kt index d7ec441f..0eaaebae 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/Podlasie.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/Podlasie.kt @@ -12,6 +12,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data.PodlasieData import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.firstlogin.PodlasieFirstLogin import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.login.PodlasieLogin import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent +import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface import pl.szczodrzynski.edziennik.data.api.models.ApiError @@ -142,6 +143,10 @@ class Podlasie(val app: App, val profile: Profile?, val loginStore: LoginStore, callback.onCompleted() } + override fun onRequiresUserAction(event: UserActionRequiredEvent) { + callback.onRequiresUserAction(event) + } + override fun onProgress(step: Float) { callback.onProgress(step) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/Template.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/Template.kt index 843d36e9..8abf079f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/Template.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/Template.kt @@ -10,6 +10,7 @@ import pl.szczodrzynski.edziennik.data.api.CODE_INTERNAL_LIBRUS_ACCOUNT_410 import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.TemplateData import pl.szczodrzynski.edziennik.data.api.edziennik.template.firstlogin.TemplateFirstLogin import pl.szczodrzynski.edziennik.data.api.edziennik.template.login.TemplateLogin +import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface import pl.szczodrzynski.edziennik.data.api.models.ApiError @@ -108,6 +109,10 @@ class Template(val app: App, val profile: Profile?, val loginStore: LoginStore, callback.onCompleted() } + override fun onRequiresUserAction(event: UserActionRequiredEvent) { + callback.onRequiresUserAction(event) + } + override fun onProgress(step: Float) { callback.onProgress(step) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/Usos.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/Usos.kt index 4ff9d04c..d676d006 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/Usos.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/Usos.kt @@ -8,6 +8,7 @@ import com.google.gson.JsonObject import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.data.api.edziennik.usos.firstlogin.UsosFirstLogin import pl.szczodrzynski.edziennik.data.api.edziennik.usos.login.UsosLogin +import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface import pl.szczodrzynski.edziennik.data.api.models.ApiError @@ -88,6 +89,10 @@ class Usos( callback.onCompleted() } + override fun onRequiresUserAction(event: UserActionRequiredEvent) { + callback.onRequiresUserAction(event) + } + override fun onProgress(step: Float) { callback.onProgress(step) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt index f49c0cea..11172d7d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt @@ -4,9 +4,11 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.usos.login +import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi +import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.ext.Bundle import pl.szczodrzynski.edziennik.ext.fromQueryString @@ -53,13 +55,16 @@ class UsosLoginApi(val data: DataUsos, val onSuccess: () -> Unit) { "interactivity" to "confirm_user", "oauth_token" to (data.oauthTokenKey ?: ""), ) - val params = Bundle( - "authorizeUrl" to "$authUrl?${authParams.toQueryString()}", - "redirectUrl" to USOS_API_OAUTH_REDIRECT_URL, - "responseStoreKey" to "oauthLoginResponse", - "extras" to data.loginStore.data.toBundle(), + data.requireUserAction( + type = UserActionRequiredEvent.Type.OAUTH, + params = Bundle( + "authorizeUrl" to "$authUrl?${authParams.toQueryString()}", + "redirectUrl" to USOS_API_OAUTH_REDIRECT_URL, + "responseStoreKey" to "oauthLoginResponse", + "extras" to data.loginStore.data.toBundle(), + ), + errorText = R.string.notification_user_action_required_oauth_usos, ) - data.error(ApiError(TAG, ERROR_USOS_OAUTH_LOGIN_REQUEST).withParams(params)) } } @@ -93,6 +98,7 @@ class UsosLoginApi(val data: DataUsos, val onSuccess: () -> Unit) { data.oauthTokenKey = accessData["oauth_token"] data.oauthTokenSecret = accessData["oauth_token_secret"] data.oauthTokenIsUser = data.oauthTokenKey != null && data.oauthTokenSecret != null + data.loginStore.removeLoginData("oauthLoginResponse") if (!data.oauthTokenIsUser) data.error(ApiError(TAG, ERROR_USOS_OAUTH_INCOMPLETE_RESPONSE) 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 64c87bf6..37af4b69 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 @@ -17,6 +17,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLogin import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent import pl.szczodrzynski.edziennik.data.api.events.EventGetEvent import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent +import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface import pl.szczodrzynski.edziennik.data.api.models.ApiError @@ -179,6 +180,7 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback { return object : EdziennikCallback { override fun onCompleted() { callback.onCompleted() } + override fun onRequiresUserAction(event: UserActionRequiredEvent) { callback.onRequiresUserAction(event) } override fun onProgress(step: Float) { callback.onProgress(step) } override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) } override fun onError(apiError: ApiError) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/UserActionRequiredEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/UserActionRequiredEvent.kt index da68f3e8..3995842a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/UserActionRequiredEvent.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/UserActionRequiredEvent.kt @@ -6,12 +6,14 @@ package pl.szczodrzynski.edziennik.data.api.events import android.os.Bundle -data class UserActionRequiredEvent(val profileId: Int, val type: Int, val params: Bundle?) { - companion object { - const val LOGIN_DATA_MOBIDZIENNIK = 101 - const val LOGIN_DATA_LIBRUS = 102 - const val LOGIN_DATA_VULCAN = 104 - const val CAPTCHA_LIBRUS = 202 - const val OAUTH_USOS = 701 +data class UserActionRequiredEvent( + val profileId: Int?, + val type: Type, + val params: Bundle, + val errorText: Int, +) { + enum class Type { + RECAPTCHA, + OAUTH, } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikCallback.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikCallback.kt index c1c878b4..4943ff1e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikCallback.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikCallback.kt @@ -4,6 +4,7 @@ package pl.szczodrzynski.edziennik.data.api.interfaces +import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent import pl.szczodrzynski.edziennik.data.api.models.Feature import pl.szczodrzynski.edziennik.data.api.models.LoginMethod @@ -14,4 +15,5 @@ import pl.szczodrzynski.edziennik.data.api.models.LoginMethod */ interface EdziennikCallback : EndpointCallback { fun onCompleted() + fun onRequiresUserAction(event: UserActionRequiredEvent) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt index ea53112a..c02c666a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt @@ -1,5 +1,6 @@ package pl.szczodrzynski.edziennik.data.api.models +import android.os.Bundle import android.util.LongSparseArray import android.util.SparseArray import androidx.core.util.set @@ -12,7 +13,8 @@ import pl.szczodrzynski.edziennik.BuildConfig import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.ERROR_REQUEST_FAILURE import pl.szczodrzynski.edziennik.data.api.Regexes.MESSAGE_META -import pl.szczodrzynski.edziennik.data.api.interfaces.EndpointCallback +import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent +import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback import pl.szczodrzynski.edziennik.data.db.AppDb import pl.szczodrzynski.edziennik.data.db.entity.* import pl.szczodrzynski.edziennik.ext.* @@ -37,7 +39,7 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt /** * A callback passed to all [Feature]s and [LoginMethod]s */ - lateinit var callback: EndpointCallback + lateinit var callback: EdziennikCallback /** * A list of [LoginMethod]s *already fulfilled* during this sync. @@ -374,6 +376,15 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt callback.onError(apiError) } + fun requireUserAction(type: UserActionRequiredEvent.Type, params: Bundle, errorText: Int) { + callback.onRequiresUserAction(UserActionRequiredEvent( + profileId = profile?.id, + type = type, + params = params, + errorText = errorText, + )) + } + fun progress(step: Float) { callback.onProgress(step) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/BundleExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/BundleExtensions.kt index cc6488bd..037d78f5 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ext/BundleExtensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/BundleExtensions.kt @@ -22,6 +22,15 @@ fun Bundle?.getFloat(key: String, defaultValue: Float): Float { fun Bundle?.getString(key: String, defaultValue: String): String { return this?.getString(key, defaultValue) ?: defaultValue } +inline fun > Bundle?.getEnum(key: String): E? { + return this?.getString(key)?.let { + try { + enumValueOf(it) + } catch (e: Exception) { + null + } + } +} fun Bundle?.getIntOrNull(key: String): Int? { return this?.get(key) as? Int @@ -48,6 +57,7 @@ fun Bundle(vararg properties: Pair): Bundle { is Bundle -> putBundle(property.first, property.second as Bundle) is Parcelable -> putParcelable(property.first, property.second as Parcelable) is Array<*> -> putParcelableArray(property.first, property.second as Array) + is Enum<*> -> putString(property.first, (property.second as Enum<*>).name) } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/LibrusCaptchaDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaPromptDialog.kt similarity index 88% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/LibrusCaptchaDialog.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaPromptDialog.kt index 12ad0e37..a927347d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/LibrusCaptchaDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaPromptDialog.kt @@ -13,14 +13,16 @@ import pl.szczodrzynski.edziennik.databinding.RecaptchaViewBinding import pl.szczodrzynski.edziennik.ext.onClick import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog -class LibrusCaptchaDialog( +class RecaptchaPromptDialog( activity: AppCompatActivity, + private val siteKey: String, + private val referer: String, private val onSuccess: (recaptchaCode: String) -> Unit, - private val onFailure: (() -> Unit)?, + private val onCancel: (() -> Unit)?, onShowListener: ((tag: String) -> Unit)? = null, onDismissListener: ((tag: String) -> Unit)? = null, ) : BindingDialog(activity, onShowListener, onDismissListener) { - override val TAG = "LibrusCaptchaDialog" + override val TAG = "RecaptchaPromptDialog" override fun getTitleRes(): Int? = null override fun inflate(layoutInflater: LayoutInflater) = @@ -46,8 +48,8 @@ class LibrusCaptchaDialog( b.progress.visibility = View.VISIBLE RecaptchaDialog( activity, - siteKey = "6Lf48moUAAAAAB9ClhdvHr46gRWR-CN31CXQPG2U", - referer = "https://portal.librus.pl/rodzina/login", + siteKey = siteKey, + referer = referer, onSuccess = { recaptchaCode -> b.checkbox.background = checkboxBackground b.checkbox.foreground = checkboxForeground @@ -67,6 +69,6 @@ class LibrusCaptchaDialog( override fun onDismiss() { if (!success) - onFailure?.invoke() + onCancel?.invoke() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeDebugCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeDebugCard.kt index 329a677a..6947b74b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeDebugCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeDebugCard.kt @@ -27,7 +27,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.databinding.CardHomeDebugBinding import pl.szczodrzynski.edziennik.ext.dp import pl.szczodrzynski.edziennik.ext.onClick -import pl.szczodrzynski.edziennik.ui.captcha.LibrusCaptchaDialog +import pl.szczodrzynski.edziennik.ui.captcha.RecaptchaPromptDialog import pl.szczodrzynski.edziennik.ui.home.HomeCard import pl.szczodrzynski.edziennik.ui.home.HomeCardAdapter import pl.szczodrzynski.edziennik.ui.home.HomeFragment @@ -85,11 +85,6 @@ class HomeDebugCard( app.startActivity(Chucker.getLaunchIntent(activity, 1)); } - b.librusCaptchaButton.onClick { - //app.startActivity(Intent(activity, LoginLibrusCaptchaActivity::class.java)) - LibrusCaptchaDialog(activity, onSuccess = {}, onFailure = {}).show() - } - b.getLogs.onClick { val logs = HyperLog.getDeviceLogsInFile(activity, true) val intent = Intent(Intent.ACTION_SEND) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginInfo.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginInfo.kt index 8b605734..774530f1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginInfo.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginInfo.kt @@ -65,7 +65,6 @@ object LoginInfo { errorCodes = mapOf( ERROR_LOGIN_LIBRUS_PORTAL_NOT_ACTIVATED to R.string.login_error_account_not_activated, ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN to R.string.login_error_incorrect_login_or_password, - ERROR_CAPTCHA_LIBRUS_PORTAL to R.string.error_3001_reason ) ), /*Mode( diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginProgressFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginProgressFragment.kt index ee29575e..87a01008 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginProgressFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginProgressFragment.kt @@ -19,7 +19,7 @@ import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.data.api.ERROR_CAPTCHA_NEEDED +import pl.szczodrzynski.edziennik.data.api.ERROR_REQUIRES_USER_ACTION import pl.szczodrzynski.edziennik.data.api.LOGIN_NO_ARGUMENTS import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.api.events.ApiTaskErrorEvent @@ -29,6 +29,7 @@ import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.databinding.LoginProgressFragmentBinding import pl.szczodrzynski.edziennik.ext.joinNotNullStrings +import pl.szczodrzynski.edziennik.utils.managers.UserActionManager import kotlin.coroutines.CoroutineContext import kotlin.math.max @@ -137,13 +138,21 @@ class LoginProgressFragment : Fragment(), CoroutineScope { return } - app.userActionManager.execute(activity, event.profileId, event.type, event.params, onSuccess = { params -> - args.putAll(params) - doFirstLogin(args) - }, onFailure = { - activity.error(ApiError(TAG, ERROR_CAPTCHA_NEEDED)) - nav.navigateUp() - }) + val callback = UserActionManager.UserActionCallback( + onSuccess = { data -> + args.putAll(data) + doFirstLogin(args) + }, + onFailure = { + activity.error(ApiError(TAG, ERROR_REQUIRES_USER_ACTION)) + nav.navigateUp() + }, + onCancel = { + nav.navigateUp() + }, + ) + + app.userActionManager.execute(activity, event, callback) } override fun onStart() { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt index 3117c650..3e8761c8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt @@ -16,13 +16,10 @@ import org.greenrobot.eventbus.ThreadMode import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.data.api.ERROR_CAPTCHA_LIBRUS_PORTAL -import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_OAUTH_LOGIN_REQUEST import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent -import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.ext.* -import pl.szczodrzynski.edziennik.ui.captcha.LibrusCaptchaDialog +import pl.szczodrzynski.edziennik.ui.captcha.RecaptchaPromptDialog import pl.szczodrzynski.edziennik.ui.login.oauth.OAuthLoginActivity import pl.szczodrzynski.edziennik.ui.login.oauth.OAuthLoginResult import pl.szczodrzynski.edziennik.utils.Utils.d @@ -32,43 +29,31 @@ class UserActionManager(val app: App) { private const val TAG = "UserActionManager" } - fun requiresUserAction(apiError: ApiError) = when (apiError.errorCode) { - ERROR_CAPTCHA_LIBRUS_PORTAL -> true - ERROR_USOS_OAUTH_LOGIN_REQUEST -> true - else -> false - } - - fun sendToUser(apiError: ApiError) { - val type = when (apiError.errorCode) { - ERROR_CAPTCHA_LIBRUS_PORTAL -> UserActionRequiredEvent.CAPTCHA_LIBRUS - ERROR_USOS_OAUTH_LOGIN_REQUEST -> UserActionRequiredEvent.OAUTH_USOS - else -> 0 - } - + fun sendToUser(event: UserActionRequiredEvent) { if (EventBus.getDefault().hasSubscriberForEvent(UserActionRequiredEvent::class.java)) { - EventBus.getDefault().post(UserActionRequiredEvent(apiError.profileId ?: -1, type, apiError.params)) + EventBus.getDefault().post(event) return } val manager = app.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - - val text = app.getString(when (type) { - UserActionRequiredEvent.CAPTCHA_LIBRUS -> R.string.notification_user_action_required_captcha_librus - UserActionRequiredEvent.OAUTH_USOS -> R.string.notification_user_action_required_oauth_usos - else -> R.string.notification_user_action_required_text - }, apiError.profileId) - + val text = app.getString(event.errorText, event.profileId) val intent = Intent( - app, - MainActivity::class.java, - "action" to "userActionRequired", - "profileId" to (apiError.profileId ?: -1), - "type" to type, - "params" to apiError.params, + app, + MainActivity::class.java, + "action" to "userActionRequired", + "profileId" to event.profileId, + "type" to event.type, + "params" to event.params, + ) + val pendingIntent = PendingIntent.getActivity( + app, + System.currentTimeMillis().toInt(), + intent, + PendingIntent.FLAG_ONE_SHOT or pendingIntentFlag(), ) - val pendingIntent = PendingIntent.getActivity(app, System.currentTimeMillis().toInt(), intent, PendingIntent.FLAG_ONE_SHOT or pendingIntentFlag()) - val notification = NotificationCompat.Builder(app, app.notificationChannelsManager.userAttention.key) + val notification = + NotificationCompat.Builder(app, app.notificationChannelsManager.userAttention.key) .setContentTitle(app.getString(R.string.notification_user_action_required_title)) .setContentText(text) .setSmallIcon(R.drawable.ic_error_outline) @@ -84,70 +69,56 @@ class UserActionManager(val app: App) { manager.notify(System.currentTimeMillis().toInt(), notification) } + class UserActionCallback( + val onSuccess: ((data: Bundle) -> Unit)? = null, + val onFailure: (() -> Unit)? = null, + val onCancel: (() -> Unit)? = null, + ) + fun execute( - activity: AppCompatActivity, - profileId: Int?, - type: Int, - params: Bundle? = null, - onSuccess: ((params: Bundle) -> Unit)? = null, - onFailure: (() -> Unit)? = null + activity: AppCompatActivity, + event: UserActionRequiredEvent, + callback: UserActionCallback, ) { - d(TAG, "Running user action ($type) with params: ${params?.toJsonObject()}") - val isSuccessful = when (type) { - UserActionRequiredEvent.CAPTCHA_LIBRUS -> executeLibrus(activity, profileId, params, onSuccess, onFailure) - UserActionRequiredEvent.OAUTH_USOS -> executeOauth(activity, profileId, params, onSuccess, onFailure) - else -> false - } - if (!isSuccessful) { - onFailure?.invoke() + d(TAG, "Running user action (${event.type}) with params: ${event.params}") + val isSuccessful = when (event.type) { + UserActionRequiredEvent.Type.RECAPTCHA -> executeRecaptcha(activity, event, callback) + UserActionRequiredEvent.Type.OAUTH -> executeOauth(activity, event, callback) } + if (!isSuccessful) + callback.onFailure?.invoke() } - private fun executeLibrus( + private fun executeRecaptcha( activity: AppCompatActivity, - profileId: Int?, - params: Bundle?, - onSuccess: ((params: Bundle) -> Unit)?, - onFailure: (() -> Unit)?, + event: UserActionRequiredEvent, + callback: UserActionCallback, ): Boolean { - if (profileId == null) - return false - val extras = params?.getBundle("extras") - // show captcha dialog - // use passed onSuccess listener, else sync profile - LibrusCaptchaDialog( + val siteKey = event.params.getString("siteKey") ?: return false + val referer = event.params.getString("referer") ?: return false + RecaptchaPromptDialog( activity = activity, + siteKey = siteKey, + referer = referer, onSuccess = { code -> - val args = Bundle( + finishAction(activity, event, callback, Bundle( "recaptchaCode" to code, "recaptchaTime" to System.currentTimeMillis(), - ) - if (extras != null) - args.putAll(extras) - - if (onSuccess != null) - onSuccess(args) - else - EdziennikTask.syncProfile(profileId, arguments = args.toJsonObject()).enqueue(activity) + )) }, - onFailure = onFailure, + onCancel = callback.onCancel, ).show() return true } private fun executeOauth( activity: AppCompatActivity, - profileId: Int?, - params: Bundle?, - onSuccess: ((params: Bundle) -> Unit)?, - onFailure: (() -> Unit)?, + event: UserActionRequiredEvent, + callback: UserActionCallback, ): Boolean { - if (profileId == null || params == null) - return false - val extras = params.getBundle("extras") - val storeKey = params.getString("responseStoreKey") ?: return false - params.getString("authorizeUrl") ?: return false - params.getString("redirectUrl") ?: return false + val storeKey = event.params.getString("responseStoreKey") ?: return false + event.params.getString("authorizeUrl") ?: return false + event.params.getString("redirectUrl") ?: return false var listener: Any? = null listener = object { @@ -155,26 +126,41 @@ class UserActionManager(val app: App) { fun onOAuthLoginResult(result: OAuthLoginResult) { EventBus.getDefault().unregister(listener) when { - result.isError -> onFailure?.invoke() + result.isError -> callback.onFailure?.invoke() result.responseUrl != null -> { - val args = Bundle( + finishAction(activity, event, callback, Bundle( storeKey to result.responseUrl, - ) - if (extras != null) - args.putAll(extras) - - if (onSuccess != null) - onSuccess(args) - else - EdziennikTask.syncProfile(profileId, arguments = args.toJsonObject()).enqueue(activity) + )) } + else -> callback.onCancel?.invoke() } } } EventBus.getDefault().register(listener) - val intent = Intent(activity, OAuthLoginActivity::class.java).putExtras(params) + val intent = Intent(activity, OAuthLoginActivity::class.java).putExtras(event.params) activity.startActivity(intent) return true } + + private fun finishAction( + activity: AppCompatActivity, + event: UserActionRequiredEvent, + callback: UserActionCallback, + data: Bundle, + ) { + val extras = event.params.getBundle("extras") + if (extras != null) + data.putAll(extras) + + if (callback.onSuccess != null) + callback.onSuccess.invoke(data) + else if (event.profileId != null) + EdziennikTask.syncProfile( + profileId = event.profileId, + arguments = data.toJsonObject(), + ).enqueue(activity) + else + callback.onFailure?.invoke() + } } diff --git a/app/src/main/res/layout/card_home_debug.xml b/app/src/main/res/layout/card_home_debug.xml index 18d21c02..4bcca808 100644 --- a/app/src/main/res/layout/card_home_debug.xml +++ b/app/src/main/res/layout/card_home_debug.xml @@ -25,13 +25,6 @@ android:layout_height="wrap_content" android:text="Save Debug Logs" /> - - ERROR_NOT_IMPLEMENTED ERROR_FILE_DOWNLOAD - ERROR_NO_STUDENTS_IN_ACCOUNT - - ERROR_CAPTCHA_NEEDED - ERROR_CAPTCHA_LIBRUS_PORTAL - ERROR_API_PDO_ERROR ERROR_API_INVALID_CLIENT ERROR_API_INVALID_ARGUMENT @@ -174,8 +169,6 @@ ERROR_PODLASIE_API_OTHER ERROR_PODLASIE_API_DATA_MISSING - ERROR_USOS_OAUTH_LOGIN_REQUEST - ERROR_TEMPLATE_WEB_OTHER EXCEPTION_API_TASK @@ -223,11 +216,6 @@ Nie zaimplementowano Wystąpił błąd podczas pobierania pliku. Dziennik może być przeciążony lub mieć przerwę techniczną. - Brak uczniów przypisanych do konta - - Wymagane rozwiązanie zadania Captcha - LIBRUS®️: wymagane rozwiązanie zadania Captcha - ERROR_API_PDO_ERROR Nieprawidłowy ID klienta API API: nieprawidłowy argument @@ -366,8 +354,6 @@ ERROR_PODLASIE_API_OTHER Brak danych. Zgłoś błąd programiście. - Wymagane logowanie w przeglądarce - ERROR_TEMPLATE_WEB_OTHER Błąd synchronizacji. Upewnij się, że masz połączenie z internetem, a następnie zgłoś błąd. From 8097e8d06dc6a565e597b63a40e3998e346c51dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sun, 16 Oct 2022 00:09:51 +0200 Subject: [PATCH 09/15] [API/Usos] Add syncing Courses and Terms. --- .../edziennik/data/api/Errors.kt | 1 + .../data/api/edziennik/usos/DataUsos.kt | 13 +-- .../edziennik/data/api/edziennik/usos/Usos.kt | 5 +- .../data/api/edziennik/usos/UsosFeatures.kt | 15 +++- .../data/api/edziennik/usos/data/UsosApi.kt | 24 ++++-- .../data/api/edziennik/usos/data/UsosData.kt | 14 ++- .../edziennik/usos/data/api/UsosApiCourses.kt | 86 +++++++++++++++++++ .../edziennik/usos/data/api/UsosApiTerms.kt | 65 ++++++++++++++ .../usos/firstlogin/UsosFirstLogin.kt | 2 +- .../edziennik/data/db/entity/Profile.kt | 4 + 10 files changed, 205 insertions(+), 24 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiCourses.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTerms.kt 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 b4e892e6..8be89c6e 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 @@ -203,6 +203,7 @@ const val ERROR_PODLASIE_API_DATA_MISSING = 632 const val ERROR_USOS_OAUTH_GOT_DIFFERENT_TOKEN = 702 const val ERROR_USOS_OAUTH_INCOMPLETE_RESPONSE = 703 const val ERROR_USOS_NO_STUDENT_PROGRAMMES = 704 +const val ERROR_USOS_API_INCOMPLETE_RESPONSE = 705 const val ERROR_TEMPLATE_WEB_OTHER = 801 diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt index 0fd1b2ba..5cd054f8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt @@ -25,7 +25,7 @@ class DataUsos( } } - override fun generateUserCode() = "$schoolId:${studentNumber ?: studentId}" + override fun generateUserCode() = "$schoolId:${profile?.studentNumber ?: studentId}" var schoolId: String? get() { mSchoolId = mSchoolId ?: loginStore.getLoginData("schoolId", null); return mSchoolId } @@ -67,13 +67,8 @@ class DataUsos( set(value) { loginStore.putLoginData("oauthTokenIsUser", value); mOauthTokenIsUser = value } private var mOauthTokenIsUser: Boolean? = null - var studentId: String? - get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", null); return mStudentId } + var studentId: Int + get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", 0); return mStudentId ?: 0 } set(value) { profile?.putStudentData("studentId", value) ?: return; mStudentId = value } - private var mStudentId: String? = null - - var studentNumber: String? - get() { mStudentNumber = mStudentNumber ?: profile?.getStudentData("studentNumber", null); return mStudentNumber } - set(value) { profile?.putStudentData("studentNumber", value) ?: return; mStudentNumber = value } - private var mStudentNumber: String? = null + private var mStudentId: Int? = null } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/Usos.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/Usos.kt index d676d006..cbf92ca8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/Usos.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/Usos.kt @@ -6,6 +6,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.usos import com.google.gson.JsonObject import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosData import pl.szczodrzynski.edziennik.data.api.edziennik.usos.firstlogin.UsosFirstLogin import pl.szczodrzynski.edziennik.data.api.edziennik.usos.login.UsosLogin import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent @@ -58,9 +59,9 @@ class Usos( d(TAG, "LoginMethod IDs: ${data.targetLoginMethodIds}") d(TAG, "Endpoint IDs: ${data.targetEndpointIds}") UsosLogin(data) { - /*UsosData(data) { + UsosData(data) { completed() - }*/ + } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/UsosFeatures.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/UsosFeatures.kt index a4ee1389..bdcf9c78 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/UsosFeatures.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/UsosFeatures.kt @@ -4,15 +4,26 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.usos +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.FEATURE_ALWAYS_NEEDED import pl.szczodrzynski.edziennik.data.api.FEATURE_STUDENT_INFO -import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_USOS_API -import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_USOS +import pl.szczodrzynski.edziennik.data.api.FEATURE_TEAM_INFO import pl.szczodrzynski.edziennik.data.api.models.Feature const val ENDPOINT_USOS_API_USER = 7000 +const val ENDPOINT_USOS_API_TERMS = 7010 +const val ENDPOINT_USOS_API_COURSES = 7020 val UsosFeatures = listOf( Feature(LOGIN_TYPE_USOS, FEATURE_STUDENT_INFO, listOf( ENDPOINT_USOS_API_USER to LOGIN_METHOD_USOS_API, ), listOf(LOGIN_METHOD_USOS_API)), + + Feature(LOGIN_TYPE_USOS, FEATURE_SCHOOL_INFO, listOf( + ENDPOINT_USOS_API_TERMS to LOGIN_METHOD_USOS_API, + ), listOf(LOGIN_METHOD_USOS_API)), + + Feature(LOGIN_TYPE_USOS, FEATURE_TEAM_INFO, listOf( + ENDPOINT_USOS_API_COURSES to LOGIN_METHOD_USOS_API, + ), listOf(LOGIN_METHOD_USOS_API)), ) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt index 1d3ab69c..829fefa4 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt @@ -6,7 +6,6 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data import com.google.gson.JsonArray import com.google.gson.JsonObject -import im.wangchao.mhttp.AbsCallbackHandler import im.wangchao.mhttp.Request import im.wangchao.mhttp.Response import im.wangchao.mhttp.body.MediaTypeUtils @@ -17,10 +16,7 @@ import pl.szczodrzynski.edziennik.data.api.ERROR_REQUEST_FAILURE import pl.szczodrzynski.edziennik.data.api.SERVER_USER_AGENT import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos import pl.szczodrzynski.edziennik.data.api.models.ApiError -import pl.szczodrzynski.edziennik.ext.currentTimeUnix -import pl.szczodrzynski.edziennik.ext.hmacSHA1 -import pl.szczodrzynski.edziennik.ext.toQueryString -import pl.szczodrzynski.edziennik.ext.urlEncode +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.Utils.d import java.net.HttpURLConnection.* import java.util.UUID @@ -42,6 +38,9 @@ open class UsosApi(open val data: DataUsos, open val lastSync: Long?) { val profile get() = data.profile + protected fun JsonObject.getLangString(key: String) = + this.getJsonObject(key)?.getString("pl") + private fun valueToString(value: Any) = when (value) { is String -> value is Number -> value.toString() @@ -74,15 +73,22 @@ open class UsosApi(open val data: DataUsos, open val lastSync: Long?) { fun apiRequest( tag: String, service: String, - params: Map, + params: Map? = null, + fields: List? = null, responseType: ResponseType, onSuccess: (data: T, response: Response?) -> Unit, ) { val url = "${data.instanceUrl}services/$service" d(tag, "Request: Usos/Api - $url") - val formData = params.mapValues { - valueToString(it.value) - } + + val formData = mutableMapOf() + if (params != null) + formData.putAll(params.mapValues { + valueToString(it.value) + }) + if (fields != null) + formData["fields"] = valueToString(fields) + val auth = mutableMapOf( "realm" to url, "oauth_consumer_key" to (data.oauthConsumerKey ?: ""), diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt index 5ad199ff..5ea6ab66 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt @@ -7,7 +7,11 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.web.TemplateWebSample import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_COURSES +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_TERMS import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_USER +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiCourses +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiTerms import pl.szczodrzynski.edziennik.utils.Utils.d class UsosData(val data: DataUsos, val onSuccess: () -> Unit) { @@ -39,9 +43,17 @@ class UsosData(val data: DataUsos, val onSuccess: () -> Unit) { private fun useEndpoint(endpointId: Int, lastSync: Long?, onSuccess: (endpointId: Int) -> Unit) { d(TAG, "Using endpoint $endpointId. Last sync time = $lastSync") when (endpointId) { - ENDPOINT_USOS_API_USER -> { + /*ENDPOINT_USOS_API_USER -> { data.startProgress(R.string.edziennik_progress_endpoint_student_info) // TemplateWebSample(data, lastSync, onSuccess) + }*/ + ENDPOINT_USOS_API_TERMS -> { + data.startProgress(R.string.edziennik_progress_endpoint_school_info) + UsosApiTerms(data, lastSync, onSuccess) + } + ENDPOINT_USOS_API_COURSES -> { + data.startProgress(R.string.edziennik_progress_endpoint_teams) + UsosApiCourses(data, lastSync, onSuccess) } else -> onSuccess(endpointId) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiCourses.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiCourses.kt new file mode 100644 index 00000000..8f7359b5 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiCourses.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-15. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api + +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_COURSES +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi +import pl.szczodrzynski.edziennik.data.db.entity.Team +import pl.szczodrzynski.edziennik.ext.* + +class UsosApiCourses( + override val data: DataUsos, + override val lastSync: Long?, + val onSuccess: (endpointId: Int) -> Unit, +) : UsosApi(data, lastSync) { + companion object { + const val TAG = "UsosApiCourses" + } + + init { + apiRequest( + tag = TAG, + service = "courses/user", + fields = listOf( + // "terms" to listOf("id", "name", "start_date", "end_date"), + "course_editions" to listOf( + "course_id", + // "course_name", + // "term_id", + "user_groups" to listOf( + "course_unit_id", + "group_number", + "class_type", + "class_type_id", + // "lecturers", + ), + ), + ), + responseType = ResponseType.OBJECT, + ) { json, response -> + if (!processResponse(json)) { + data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response) + return@apiRequest + } + + data.setSyncNext(ENDPOINT_USOS_API_COURSES, 2 * DAY) + onSuccess(ENDPOINT_USOS_API_COURSES) + } + } + + private fun processResponse(json: JsonObject): Boolean { + // val term = json.getJsonArray("terms")?.firstOrNull() ?: return false + val courseEditions = json.getJsonObject("course_editions") + ?.entrySet() + ?.flatMap { it.value.asJsonArray } + ?.map { it.asJsonObject } ?: return false + + var hasValidTeam = false + for (courseEdition in courseEditions) { + val courseId = courseEdition.getString("course_id") ?: continue + // val courseName = courseEdition.getLangString("course_name") ?: continue + val userGroups = courseEdition.getJsonArray("user_groups")?.asJsonObjectList() ?: continue + for (userGroup in userGroups) { + val courseUnitId = userGroup.getLong("course_unit_id") ?: continue + val groupNumber = userGroup.getInt("group_number") ?: continue + val classType = userGroup.getLangString("class_type") ?: continue + val classTypeId = userGroup.getString("class_type_id") ?: continue + + data.teamList.put(courseUnitId, Team( + profileId, + courseUnitId, + "$classType $groupNumber ($courseId)", + 2, + "${data.schoolId}:${courseId} $classTypeId$groupNumber", + -1, + )) + hasValidTeam = true + } + } + return hasValidTeam + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTerms.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTerms.kt new file mode 100644 index 00000000..4a17bffa --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTerms.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-15. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api + +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_TERMS +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi +import pl.szczodrzynski.edziennik.ext.* +import pl.szczodrzynski.edziennik.utils.models.Date + +class UsosApiTerms( + override val data: DataUsos, + override val lastSync: Long?, + val onSuccess: (endpointId: Int) -> Unit, +) : UsosApi(data, lastSync) { + companion object { + const val TAG = "UsosApiTerms" + } + + init { + apiRequest( + tag = TAG, + service = "terms/search", + params = mapOf( + "query" to Date.getToday().year.toString(), + ), + responseType = ResponseType.ARRAY, + ) { json, response -> + if (!processResponse(json)) { + data.error(UsosApiCourses.TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response) + return@apiRequest + } + + data.setSyncNext(ENDPOINT_USOS_API_TERMS, 7 * DAY) + onSuccess(ENDPOINT_USOS_API_TERMS) + } + } + + private fun processResponse(json: JsonArray): Boolean { + val dates = mutableSetOf() + for (term in json.asJsonObjectList()) { + if (!term.getBoolean("is_active", false)) + continue + val startDate = term.getString("start_date")?.let { Date.fromY_m_d(it) } + val finishDate = term.getString("finish_date")?.let { Date.fromY_m_d(it) } + if (startDate != null) + dates += startDate + if (finishDate != null) + dates += finishDate + } + val datesSorted = dates.sorted() + if (datesSorted.size != 3) + return false + profile?.studentSchoolYearStart = datesSorted[0].year + profile?.dateSemester1Start = datesSorted[0] + profile?.dateSemester2Start = datesSorted[1] + profile?.dateYearEnd = datesSorted[2] + return true + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/firstlogin/UsosFirstLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/firstlogin/UsosFirstLogin.kt index ad51cf92..4cb38cc6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/firstlogin/UsosFirstLogin.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/firstlogin/UsosFirstLogin.kt @@ -68,9 +68,9 @@ class UsosFirstLogin(val data: DataUsos, val onSuccess: () -> Unit) { accountName = null, // student account studentData = JsonObject( "studentId" to json.getInt("id"), - "studentNumber" to json.getInt("student_number"), ), ).also { + it.studentNumber = json.getInt("student_number", -1) it.studentClassName = programmes.getJsonObject(0).getJsonObject("programme").getString("id") } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt index 9fc0521a..b1615fbc 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt @@ -233,6 +233,10 @@ open class Profile( MainActivity.DRAWER_ITEM_GRADES, MainActivity.DRAWER_ITEM_HOMEWORK ) + LOGIN_TYPE_USOS -> listOf( + MainActivity.DRAWER_ITEM_TIMETABLE, + MainActivity.DRAWER_ITEM_AGENDA + ) else -> listOf( MainActivity.DRAWER_ITEM_TIMETABLE, MainActivity.DRAWER_ITEM_AGENDA, From 4de066bf5facab8e303dedb747fc6cb8c391f3a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sun, 16 Oct 2022 17:21:36 +0200 Subject: [PATCH 10/15] [API/Usos] Implement Timetable. --- .../data/api/edziennik/usos/UsosFeatures.kt | 24 +++- .../data/api/edziennik/usos/data/UsosData.kt | 10 +- .../edziennik/usos/data/api/UsosApiCourses.kt | 10 +- .../edziennik/usos/data/api/UsosApiTerms.kt | 3 +- .../usos/data/api/UsosApiTimetable.kt | 132 ++++++++++++++++++ .../edziennik/ext/TimeExtensions.kt | 11 +- .../ui/timetable/TimetableDayFragment.kt | 25 ++-- 7 files changed, 188 insertions(+), 27 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTimetable.kt diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/UsosFeatures.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/UsosFeatures.kt index bdcf9c78..50c15582 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/UsosFeatures.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/UsosFeatures.kt @@ -5,25 +5,35 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.usos import pl.szczodrzynski.edziennik.data.api.* -import pl.szczodrzynski.edziennik.data.api.FEATURE_ALWAYS_NEEDED -import pl.szczodrzynski.edziennik.data.api.FEATURE_STUDENT_INFO -import pl.szczodrzynski.edziennik.data.api.FEATURE_TEAM_INFO import pl.szczodrzynski.edziennik.data.api.models.Feature -const val ENDPOINT_USOS_API_USER = 7000 -const val ENDPOINT_USOS_API_TERMS = 7010 -const val ENDPOINT_USOS_API_COURSES = 7020 +const val ENDPOINT_USOS_API_USER = 7000 +const val ENDPOINT_USOS_API_TERMS = 7010 +const val ENDPOINT_USOS_API_COURSES = 7020 +const val ENDPOINT_USOS_API_TIMETABLE = 7030 val UsosFeatures = listOf( + /* + * Student information + */ Feature(LOGIN_TYPE_USOS, FEATURE_STUDENT_INFO, listOf( ENDPOINT_USOS_API_USER to LOGIN_METHOD_USOS_API, ), listOf(LOGIN_METHOD_USOS_API)), + /* + * Terms & courses + */ Feature(LOGIN_TYPE_USOS, FEATURE_SCHOOL_INFO, listOf( ENDPOINT_USOS_API_TERMS to LOGIN_METHOD_USOS_API, ), listOf(LOGIN_METHOD_USOS_API)), - Feature(LOGIN_TYPE_USOS, FEATURE_TEAM_INFO, listOf( ENDPOINT_USOS_API_COURSES to LOGIN_METHOD_USOS_API, ), listOf(LOGIN_METHOD_USOS_API)), + + /* + * Timetable + */ + Feature(LOGIN_TYPE_USOS, FEATURE_TIMETABLE, listOf( + ENDPOINT_USOS_API_TIMETABLE to LOGIN_METHOD_USOS_API, + ), listOf(LOGIN_METHOD_USOS_API)), ) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt index 5ea6ab66..6f3b6039 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt @@ -6,12 +6,10 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.web.TemplateWebSample -import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos -import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_COURSES -import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_TERMS -import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_USER +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.* import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiCourses import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiTerms +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiTimetable import pl.szczodrzynski.edziennik.utils.Utils.d class UsosData(val data: DataUsos, val onSuccess: () -> Unit) { @@ -55,6 +53,10 @@ class UsosData(val data: DataUsos, val onSuccess: () -> Unit) { data.startProgress(R.string.edziennik_progress_endpoint_teams) UsosApiCourses(data, lastSync, onSuccess) } + ENDPOINT_USOS_API_TIMETABLE -> { + data.startProgress(R.string.edziennik_progress_endpoint_timetable) + UsosApiTimetable(data, lastSync, onSuccess) + } else -> onSuccess(endpointId) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiCourses.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiCourses.kt index 8f7359b5..613d9f08 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiCourses.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiCourses.kt @@ -29,12 +29,12 @@ class UsosApiCourses( // "terms" to listOf("id", "name", "start_date", "end_date"), "course_editions" to listOf( "course_id", - // "course_name", + "course_name", // "term_id", "user_groups" to listOf( "course_unit_id", "group_number", - "class_type", + // "class_type", "class_type_id", // "lecturers", ), @@ -62,18 +62,18 @@ class UsosApiCourses( var hasValidTeam = false for (courseEdition in courseEditions) { val courseId = courseEdition.getString("course_id") ?: continue - // val courseName = courseEdition.getLangString("course_name") ?: continue + val courseName = courseEdition.getLangString("course_name") ?: continue val userGroups = courseEdition.getJsonArray("user_groups")?.asJsonObjectList() ?: continue for (userGroup in userGroups) { val courseUnitId = userGroup.getLong("course_unit_id") ?: continue val groupNumber = userGroup.getInt("group_number") ?: continue - val classType = userGroup.getLangString("class_type") ?: continue + // val classType = userGroup.getLangString("class_type") ?: continue val classTypeId = userGroup.getString("class_type_id") ?: continue data.teamList.put(courseUnitId, Team( profileId, courseUnitId, - "$classType $groupNumber ($courseId)", + "${profile?.studentClassName} $classTypeId$groupNumber - $courseName", 2, "${data.schoolId}:${courseId} $classTypeId$groupNumber", -1, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTerms.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTerms.kt index 4a17bffa..1bf47288 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTerms.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTerms.kt @@ -5,7 +5,6 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api import com.google.gson.JsonArray -import com.google.gson.JsonObject import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_TERMS @@ -32,7 +31,7 @@ class UsosApiTerms( responseType = ResponseType.ARRAY, ) { json, response -> if (!processResponse(json)) { - data.error(UsosApiCourses.TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response) + data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response) return@apiRequest } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTimetable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTimetable.kt new file mode 100644 index 00000000..355731a8 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTimetable.kt @@ -0,0 +1,132 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-16. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api + +import com.google.gson.JsonArray +import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_TIMETABLE +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.entity.Lesson +import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.ext.* +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time +import pl.szczodrzynski.edziennik.utils.models.Week + +class UsosApiTimetable( + override val data: DataUsos, + override val lastSync: Long?, + val onSuccess: (endpointId: Int) -> Unit, +) : UsosApi(data, lastSync) { + companion object { + const val TAG = "UsosApiTimetable" + } + + init { + val currentWeekStart = Week.getWeekStart() + if (Date.getToday().weekDay > 4) + currentWeekStart.stepForward(0, 0, 7) + + val weekStart = data.arguments + ?.getString("weekStart") + ?.let { Date.fromY_m_d(it) } + ?: currentWeekStart + val weekEnd = weekStart.clone().stepForward(0, 0, 6) + + apiRequest( + tag = TAG, + service = "tt/user", + params = mapOf( + "start" to weekStart.stringY_m_d, + "days" to 7, + ), + fields = listOf( + "type", + "start_time", + "end_time", + "unit_id", + "course_id", + "course_name", + "lecturer_ids", + "building_id", + "room_number", + "classtype_id", + "group_number", + ), + responseType = ResponseType.ARRAY, + ) { json, response -> + if (!processResponse(json, weekStart..weekEnd)) { + data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response) + return@apiRequest + } + + data.toRemove.add(DataRemoveModel.Timetable.between(weekStart, weekEnd)) + data.setSyncNext(ENDPOINT_USOS_API_TIMETABLE, SYNC_ALWAYS) + onSuccess(ENDPOINT_USOS_API_TIMETABLE) + } + } + + private fun processResponse(json: JsonArray, syncRange: ClosedRange): Boolean { + val foundDates = mutableSetOf() + + for (activity in json.asJsonObjectList()) { + val type = activity.getString("type") + if (type !in listOf("classgroup", "classgroup2")) + continue + + val startTime = activity.getString("start_time") ?: continue + val endTime = activity.getString("end_time") ?: continue + val unitId = activity.getLong("unit_id", -1) + val courseName = activity.getLangString("course_name") ?: continue + val courseId = activity.getString("course_id") ?: continue + val lecturerIds = activity.getJsonArray("lecturer_ids")?.map { it.asLong } + val buildingId = activity.getString("building_id") + val roomNumber = activity.getString("room_number") + val classTypeId = activity.getString("classtype_id") + val groupNumber = activity.getString("group_number") + + val lesson = Lesson(profileId, -1).also { + it.type = Lesson.TYPE_NORMAL + it.date = Date.fromY_m_d(startTime) + it.startTime = Time.fromY_m_d_H_m_s(startTime) + it.endTime = Time.fromY_m_d_H_m_s(endTime) + it.subjectId = data.getSubject( + id = null, + name = courseName, + shortName = courseId, + ).id + it.teacherId = lecturerIds?.firstOrNull() ?: -1L + it.teamId = unitId + val groupName = classTypeId?.plus(groupNumber)?.let { s -> "($s)" } + it.classroom = "$buildingId / $roomNumber ${groupName ?: ""}" + it.id = it.buildId() + } + lesson.date?.let { foundDates += it } + + val seen = profile?.empty != false || lesson.date!! < Date.getToday() + data.lessonList.add(lesson) + if (lesson.type != Lesson.TYPE_NORMAL) + data.metadataList += Metadata( + profileId, + Metadata.TYPE_LESSON_CHANGE, + lesson.id, + seen, + seen, + ) + } + + val notFoundDates = syncRange.asSequence() - foundDates + for (date in notFoundDates) { + data.lessonList += Lesson(profileId, date.value.toLong()).also { + it.type = Lesson.TYPE_NO_LESSONS + it.date = date + } + } + return true + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/TimeExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/TimeExtensions.kt index 114a5a40..31bd952c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ext/TimeExtensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/TimeExtensions.kt @@ -7,9 +7,10 @@ package pl.szczodrzynski.edziennik.ext import android.content.Context import im.wangchao.mhttp.Response import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time import java.text.SimpleDateFormat -import java.util.* +import java.util.Locale const val MINUTE = 60L const val HOUR = 60L*MINUTE @@ -115,3 +116,11 @@ fun Context.getSyncInterval(interval: Int): String { "" return hoursText?.plus(" $minutesText") ?: minutesText } + +fun ClosedRange.asSequence(): Sequence = sequence { + val date = this@asSequence.start.clone() + while (date in this@asSequence) { + yield(date.clone()) + date.stepForward(0, 0, 1) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableDayFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableDayFragment.kt index 37e82a27..1e4d9773 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableDayFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableDayFragment.kt @@ -21,8 +21,11 @@ import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp import kotlinx.coroutines.* -import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_TIMETABLE +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_USOS import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.db.entity.Lesson import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull @@ -40,8 +43,8 @@ import pl.szczodrzynski.edziennik.utils.managers.NoteManager import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time import pl.szczodrzynski.edziennik.utils.mutableLazy -import java.util.* import kotlin.coroutines.CoroutineContext +import kotlin.math.max import kotlin.math.min class TimetableDayFragment : LazyFragment(), CoroutineScope { @@ -82,7 +85,7 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope { startHour = startHour, endHour = endHour, dividerHeight = 1.dp, - halfHourHeight = 60.dp, + halfHourHeight = if (app.profile.loginStoreType == LOGIN_TYPE_USOS) 45.dp else 30.dp, hourDividerColor = R.attr.hourDividerColor.resolveAttr(context), halfHourDividerColor = R.attr.halfHourDividerColor.resolveAttr(context), hourLabelWidth = 40.dp, @@ -184,11 +187,18 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope { val lessonsActual = lessons.filter { it.type != Lesson.TYPE_NO_LESSONS } + val minStartHour = lessonsActual.minOf { it.displayStartTime?.hour ?: DEFAULT_END_HOUR } + val maxEndHour = lessonsActual.maxOf { it.displayEndTime?.hour?.plus(1) ?: DEFAULT_START_HOUR } + if (profileConfig.timetableTrimHourRange) { dayViewDelegate.deinitialize() // end/start defaults are swapped on purpose - startHour = lessonsActual.minOf { it.displayStartTime?.hour ?: DEFAULT_END_HOUR } - endHour = lessonsActual.maxOf { it.displayEndTime?.hour?.plus(1) ?: DEFAULT_START_HOUR } + startHour = minStartHour + endHour = maxEndHour + } else if (startHour > minStartHour || endHour < maxEndHour) { + dayViewDelegate.deinitialize() + startHour = min(startHour, minStartHour) + endHour = max(endHour, maxEndHour) } b.scrollView.isVisible = true @@ -377,9 +387,8 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope { // The day view needs the event time ranges in the start minute/end minute format, // so calculate those here - val startMinute = 60 * (lesson.displayStartTime?.hour - ?: 0) + (lesson.displayStartTime?.minute ?: 0) - val endMinute = startMinute + 45 + val startMinute = 60 * startTime.hour + startTime.minute + val endMinute = 60 * endTime.hour + endTime.minute eventTimeRanges.add(DayView.EventTimeRange(startMinute, endMinute)) } From 044cedff99a617a3a39fc54cc64e27e54b084735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sun, 16 Oct 2022 18:13:22 +0200 Subject: [PATCH 11/15] [Usos] Override lesson colors by activity type. --- .../98.json | 2314 +++++++++++++++++ .../usos/data/api/UsosApiTimetable.kt | 9 + .../szczodrzynski/edziennik/data/db/AppDb.kt | 3 +- .../edziennik/data/db/entity/Lesson.kt | 2 + .../data/db/migration/Migration98.kt | 15 + .../ui/timetable/TimetableDayFragment.kt | 2 +- 6 files changed, 2343 insertions(+), 2 deletions(-) create mode 100644 app/schemas/pl.szczodrzynski.edziennik.data.db.AppDb/98.json create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration98.kt diff --git a/app/schemas/pl.szczodrzynski.edziennik.data.db.AppDb/98.json b/app/schemas/pl.szczodrzynski.edziennik.data.db.AppDb/98.json new file mode 100644 index 00000000..1a291aef --- /dev/null +++ b/app/schemas/pl.szczodrzynski.edziennik.data.db.AppDb/98.json @@ -0,0 +1,2314 @@ +{ + "formatVersion": 1, + "database": { + "version": 98, + "identityHash": "2612ebba9802eedc7ebc69724606a23c", + "entities": [ + { + "tableName": "grades", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `gradeId` INTEGER NOT NULL, `gradeName` TEXT NOT NULL, `gradeType` INTEGER NOT NULL, `gradeValue` REAL NOT NULL, `gradeWeight` REAL NOT NULL, `gradeColor` INTEGER NOT NULL, `gradeCategory` TEXT, `gradeDescription` TEXT, `gradeComment` TEXT, `gradeSemester` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `gradeValueMax` REAL, `gradeClassAverage` REAL, `gradeParentId` INTEGER, `gradeIsImprovement` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `gradeId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "gradeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "gradeName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "gradeType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "gradeValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "gradeWeight", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "gradeColor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "gradeCategory", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "gradeDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "gradeComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "semester", + "columnName": "gradeSemester", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "valueMax", + "columnName": "gradeValueMax", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "classAverage", + "columnName": "gradeClassAverage", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "gradeParentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isImprovement", + "columnName": "gradeIsImprovement", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "gradeId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_grades_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_grades_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "teachers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `teacherLoginId` TEXT, `teacherName` TEXT, `teacherSurname` TEXT, `teacherType` INTEGER NOT NULL, `teacherTypeDescription` TEXT, `teacherSubjects` TEXT NOT NULL, PRIMARY KEY(`profileId`, `teacherId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loginId", + "columnName": "teacherLoginId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "teacherName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "surname", + "columnName": "teacherSurname", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "teacherType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "typeDescription", + "columnName": "teacherTypeDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subjects", + "columnName": "teacherSubjects", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "teacherId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "teacherAbsence", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `teacherAbsenceId` INTEGER NOT NULL, `teacherAbsenceType` INTEGER NOT NULL, `teacherAbsenceName` TEXT, `teacherAbsenceDateFrom` TEXT NOT NULL, `teacherAbsenceDateTo` TEXT NOT NULL, `teacherAbsenceTimeFrom` TEXT, `teacherAbsenceTimeTo` TEXT, `teacherId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `teacherAbsenceId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "teacherAbsenceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "teacherAbsenceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "teacherAbsenceName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dateFrom", + "columnName": "teacherAbsenceDateFrom", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateTo", + "columnName": "teacherAbsenceDateTo", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timeFrom", + "columnName": "teacherAbsenceTimeFrom", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timeTo", + "columnName": "teacherAbsenceTimeTo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "teacherAbsenceId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_teacherAbsence_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_teacherAbsence_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "teacherAbsenceTypes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `teacherAbsenceTypeId` INTEGER NOT NULL, `teacherAbsenceTypeName` TEXT NOT NULL, PRIMARY KEY(`profileId`, `teacherAbsenceTypeId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "teacherAbsenceTypeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "teacherAbsenceTypeName", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "teacherAbsenceTypeId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "subjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `subjectLongName` TEXT, `subjectShortName` TEXT, `subjectColor` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `subjectId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "longName", + "columnName": "subjectLongName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shortName", + "columnName": "subjectShortName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "subjectColor", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "subjectId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "notices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `noticeId` INTEGER NOT NULL, `noticeType` INTEGER NOT NULL, `noticeSemester` INTEGER NOT NULL, `noticeText` TEXT NOT NULL, `noticeCategory` TEXT, `noticePoints` REAL, `teacherId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `noticeId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "noticeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "noticeType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semester", + "columnName": "noticeSemester", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "noticeText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "noticeCategory", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "noticePoints", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "noticeId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_notices_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_notices_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "teams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `teamId` INTEGER NOT NULL, `teamType` INTEGER NOT NULL, `teamName` TEXT, `teamCode` TEXT, `teamTeacherId` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `teamId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "teamId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "teamType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "teamName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "code", + "columnName": "teamCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teamTeacherId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "teamId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "attendances", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `attendanceId` INTEGER NOT NULL, `attendanceBaseType` INTEGER NOT NULL, `attendanceTypeName` TEXT NOT NULL, `attendanceTypeShort` TEXT NOT NULL, `attendanceTypeSymbol` TEXT NOT NULL, `attendanceTypeColor` INTEGER, `attendanceDate` TEXT NOT NULL, `attendanceTime` TEXT, `attendanceSemester` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `attendanceLessonTopic` TEXT, `attendanceLessonNumber` INTEGER, `attendanceIsCounted` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `attendanceId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "attendanceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseType", + "columnName": "attendanceBaseType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "typeName", + "columnName": "attendanceTypeName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeShort", + "columnName": "attendanceTypeShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeSymbol", + "columnName": "attendanceTypeSymbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeColor", + "columnName": "attendanceTypeColor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "date", + "columnName": "attendanceDate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "startTime", + "columnName": "attendanceTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "semester", + "columnName": "attendanceSemester", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lessonTopic", + "columnName": "attendanceLessonTopic", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lessonNumber", + "columnName": "attendanceLessonNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isCounted", + "columnName": "attendanceIsCounted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "attendanceId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_attendances_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_attendances_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "events", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `eventId` INTEGER NOT NULL, `eventDate` TEXT NOT NULL, `eventTime` TEXT, `eventTopic` TEXT NOT NULL, `eventColor` INTEGER, `eventType` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `teamId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `eventAddedManually` INTEGER NOT NULL, `eventSharedBy` TEXT, `eventSharedByName` TEXT, `eventBlacklisted` INTEGER NOT NULL, `eventIsDone` INTEGER NOT NULL, `eventIsDownloaded` INTEGER NOT NULL, `homeworkBody` TEXT, `attachmentIds` TEXT, `attachmentNames` TEXT, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `eventId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "eventId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "eventDate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "eventTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "topic", + "columnName": "eventTopic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "eventColor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "eventType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teamId", + "columnName": "teamId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedManually", + "columnName": "eventAddedManually", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sharedBy", + "columnName": "eventSharedBy", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedByName", + "columnName": "eventSharedByName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "blacklisted", + "columnName": "eventBlacklisted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "eventIsDone", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDownloaded", + "columnName": "eventIsDownloaded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "homeworkBody", + "columnName": "homeworkBody", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attachmentIds", + "columnName": "attachmentIds", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attachmentNames", + "columnName": "attachmentNames", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "eventId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_events_profileId_eventDate_eventTime", + "unique": false, + "columnNames": [ + "profileId", + "eventDate", + "eventTime" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_events_profileId_eventDate_eventTime` ON `${TABLE_NAME}` (`profileId`, `eventDate`, `eventTime`)" + }, + { + "name": "index_events_profileId_eventType", + "unique": false, + "columnNames": [ + "profileId", + "eventType" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_events_profileId_eventType` ON `${TABLE_NAME}` (`profileId`, `eventType`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "eventTypes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `eventType` INTEGER NOT NULL, `eventTypeName` TEXT NOT NULL, `eventTypeColor` INTEGER NOT NULL, `eventTypeOrder` INTEGER NOT NULL, `eventTypeSource` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `eventType`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "eventType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "eventTypeName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "eventTypeColor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "eventTypeOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "eventTypeSource", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "eventType" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "loginStores", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`loginStoreId` INTEGER NOT NULL, `loginStoreType` INTEGER NOT NULL, `loginStoreMode` INTEGER NOT NULL, `loginStoreData` TEXT NOT NULL, PRIMARY KEY(`loginStoreId`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "loginStoreId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "loginStoreType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mode", + "columnName": "loginStoreMode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "loginStoreData", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "loginStoreId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "profiles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `loginStoreId` INTEGER NOT NULL, `loginStoreType` INTEGER NOT NULL, `name` TEXT NOT NULL, `subname` TEXT, `studentNameLong` TEXT NOT NULL, `studentNameShort` TEXT NOT NULL, `accountName` TEXT, `studentData` TEXT NOT NULL, `image` TEXT, `empty` INTEGER NOT NULL, `archived` INTEGER NOT NULL, `archiveId` INTEGER, `syncEnabled` INTEGER NOT NULL, `enableSharedEvents` INTEGER NOT NULL, `registration` INTEGER NOT NULL, `userCode` TEXT NOT NULL, `studentNumber` INTEGER NOT NULL, `studentClassName` TEXT, `studentSchoolYearStart` INTEGER NOT NULL, `dateSemester1Start` TEXT NOT NULL, `dateSemester2Start` TEXT NOT NULL, `dateYearEnd` TEXT NOT NULL, `disabledNotifications` TEXT, `lastReceiversSync` INTEGER NOT NULL, PRIMARY KEY(`profileId`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loginStoreId", + "columnName": "loginStoreId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loginStoreType", + "columnName": "loginStoreType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subname", + "columnName": "subname", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "studentNameLong", + "columnName": "studentNameLong", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentNameShort", + "columnName": "studentNameShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "studentData", + "columnName": "studentData", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "empty", + "columnName": "empty", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "archived", + "columnName": "archived", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "archiveId", + "columnName": "archiveId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "syncEnabled", + "columnName": "syncEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enableSharedEvents", + "columnName": "enableSharedEvents", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registration", + "columnName": "registration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userCode", + "columnName": "userCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentNumber", + "columnName": "studentNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentClassName", + "columnName": "studentClassName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "studentSchoolYearStart", + "columnName": "studentSchoolYearStart", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateSemester1Start", + "columnName": "dateSemester1Start", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateSemester2Start", + "columnName": "dateSemester2Start", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateYearEnd", + "columnName": "dateYearEnd", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "disabledNotifications", + "columnName": "disabledNotifications", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastReceiversSync", + "columnName": "lastReceiversSync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "luckyNumbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `luckyNumberDate` INTEGER NOT NULL, `luckyNumber` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `luckyNumberDate`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "luckyNumberDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "luckyNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "luckyNumberDate" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "announcements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `announcementId` INTEGER NOT NULL, `announcementSubject` TEXT NOT NULL, `announcementText` TEXT, `announcementStartDate` TEXT, `announcementEndDate` TEXT, `teacherId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `announcementIdString` TEXT, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `announcementId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "announcementId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "announcementSubject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "announcementText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startDate", + "columnName": "announcementStartDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endDate", + "columnName": "announcementEndDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "idString", + "columnName": "announcementIdString", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "announcementId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_announcements_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_announcements_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "gradeCategories", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `categoryId` INTEGER NOT NULL, `weight` REAL NOT NULL, `color` INTEGER NOT NULL, `text` TEXT, `columns` TEXT, `valueFrom` REAL NOT NULL, `valueTo` REAL NOT NULL, `type` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `categoryId`, `type`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "categoryId", + "columnName": "categoryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "columns", + "columnName": "columns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "valueFrom", + "columnName": "valueFrom", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "valueTo", + "columnName": "valueTo", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "categoryId", + "type" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "feedbackMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `received` INTEGER NOT NULL, `text` TEXT NOT NULL, `senderName` TEXT NOT NULL, `deviceId` TEXT, `deviceName` TEXT, `devId` INTEGER, `devImage` TEXT, `sentTime` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "messageId", + "columnName": "messageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "received", + "columnName": "received", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderName", + "columnName": "senderName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deviceName", + "columnName": "deviceName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "devId", + "columnName": "devId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "devImage", + "columnName": "devImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sentTime", + "columnName": "sentTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "messageId" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `messageId` INTEGER NOT NULL, `messageType` INTEGER NOT NULL, `messageSubject` TEXT NOT NULL, `messageBody` TEXT, `senderId` INTEGER, `addedDate` INTEGER NOT NULL, `messageIsPinned` INTEGER NOT NULL, `hasAttachments` INTEGER NOT NULL, `attachmentIds` TEXT, `attachmentNames` TEXT, `attachmentSizes` TEXT, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `messageId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "messageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "messageType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "messageSubject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "body", + "columnName": "messageBody", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "senderId", + "columnName": "senderId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isStarred", + "columnName": "messageIsPinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasAttachments", + "columnName": "hasAttachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachmentIds", + "columnName": "attachmentIds", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attachmentNames", + "columnName": "attachmentNames", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attachmentSizes", + "columnName": "attachmentSizes", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "messageId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_messages_profileId_messageType", + "unique": false, + "columnNames": [ + "profileId", + "messageType" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_profileId_messageType` ON `${TABLE_NAME}` (`profileId`, `messageType`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "messageRecipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `messageRecipientId` INTEGER NOT NULL, `messageRecipientReplyId` INTEGER NOT NULL, `messageRecipientReadDate` INTEGER NOT NULL, `messageId` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `messageRecipientId`, `messageId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "messageRecipientId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyId", + "columnName": "messageRecipientReplyId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readDate", + "columnName": "messageRecipientReadDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "messageId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "messageRecipientId", + "messageId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "debugLogs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "endpointTimers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `endpointId` INTEGER NOT NULL, `endpointLastSync` INTEGER, `endpointNextSync` INTEGER NOT NULL, `endpointViewId` INTEGER, PRIMARY KEY(`profileId`, `endpointId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endpointId", + "columnName": "endpointId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastSync", + "columnName": "endpointLastSync", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nextSync", + "columnName": "endpointNextSync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viewId", + "columnName": "endpointViewId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "endpointId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "lessonRanges", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `lessonRangeNumber` INTEGER NOT NULL, `lessonRangeStart` TEXT NOT NULL, `lessonRangeEnd` TEXT NOT NULL, PRIMARY KEY(`profileId`, `lessonRangeNumber`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lessonNumber", + "columnName": "lessonRangeNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startTime", + "columnName": "lessonRangeStart", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "endTime", + "columnName": "lessonRangeEnd", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "lessonRangeNumber" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `text` TEXT NOT NULL, `textLong` TEXT, `type` INTEGER NOT NULL, `profileId` INTEGER, `profileName` TEXT, `posted` INTEGER NOT NULL, `viewId` INTEGER, `extras` TEXT, `addedDate` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "textLong", + "columnName": "textLong", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "profileName", + "columnName": "profileName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "posted", + "columnName": "posted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viewId", + "columnName": "viewId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "extras", + "columnName": "extras", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "classrooms", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `id` INTEGER NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`profileId`, `id`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "noticeTypes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `id` INTEGER NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`profileId`, `id`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "attendanceTypes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `id` INTEGER NOT NULL, `baseType` INTEGER NOT NULL, `typeName` TEXT NOT NULL, `typeShort` TEXT NOT NULL, `typeSymbol` TEXT NOT NULL, `typeColor` INTEGER, PRIMARY KEY(`profileId`, `id`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseType", + "columnName": "baseType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "typeName", + "columnName": "typeName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeShort", + "columnName": "typeShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeSymbol", + "columnName": "typeSymbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeColor", + "columnName": "typeColor", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "timetable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `date` TEXT, `lessonNumber` INTEGER, `startTime` TEXT, `endTime` TEXT, `subjectId` INTEGER, `teacherId` INTEGER, `teamId` INTEGER, `classroom` TEXT, `oldDate` TEXT, `oldLessonNumber` INTEGER, `oldStartTime` TEXT, `oldEndTime` TEXT, `oldSubjectId` INTEGER, `oldTeacherId` INTEGER, `oldTeamId` INTEGER, `oldClassroom` TEXT, `isExtra` INTEGER NOT NULL, `color` INTEGER, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `id`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lessonNumber", + "columnName": "lessonNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startTime", + "columnName": "startTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endTime", + "columnName": "endTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "teamId", + "columnName": "teamId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "classroom", + "columnName": "classroom", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oldDate", + "columnName": "oldDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oldLessonNumber", + "columnName": "oldLessonNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "oldStartTime", + "columnName": "oldStartTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oldEndTime", + "columnName": "oldEndTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oldSubjectId", + "columnName": "oldSubjectId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "oldTeacherId", + "columnName": "oldTeacherId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "oldTeamId", + "columnName": "oldTeamId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "oldClassroom", + "columnName": "oldClassroom", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isExtra", + "columnName": "isExtra", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_timetable_profileId_type_date", + "unique": false, + "columnNames": [ + "profileId", + "type", + "date" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_timetable_profileId_type_date` ON `${TABLE_NAME}` (`profileId`, `type`, `date`)" + }, + { + "name": "index_timetable_profileId_type_oldDate", + "unique": false, + "columnNames": [ + "profileId", + "type", + "oldDate" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_timetable_profileId_type_oldDate` ON `${TABLE_NAME}` (`profileId`, `type`, `oldDate`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "config", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `key` TEXT NOT NULL, `value` TEXT, PRIMARY KEY(`profileId`, `key`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "librusLessons", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `lessonId` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `teamId` INTEGER, PRIMARY KEY(`profileId`, `lessonId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lessonId", + "columnName": "lessonId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teamId", + "columnName": "teamId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "lessonId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_librusLessons_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_librusLessons_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "timetableManual", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `type` INTEGER NOT NULL, `repeatBy` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `date` INTEGER, `weekDay` INTEGER, `lessonNumber` INTEGER, `startTime` TEXT, `endTime` TEXT, `subjectId` INTEGER, `teacherId` INTEGER, `teamId` INTEGER, `classroom` TEXT)", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatBy", + "columnName": "repeatBy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "weekDay", + "columnName": "weekDay", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lessonNumber", + "columnName": "lessonNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startTime", + "columnName": "startTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endTime", + "columnName": "endTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "teamId", + "columnName": "teamId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "classroom", + "columnName": "classroom", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_timetableManual_profileId_date", + "unique": false, + "columnNames": [ + "profileId", + "date" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_timetableManual_profileId_date` ON `${TABLE_NAME}` (`profileId`, `date`)" + }, + { + "name": "index_timetableManual_profileId_weekDay", + "unique": false, + "columnNames": [ + "profileId", + "weekDay" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_timetableManual_profileId_weekDay` ON `${TABLE_NAME}` (`profileId`, `weekDay`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `noteId` INTEGER NOT NULL, `noteOwnerType` TEXT, `noteOwnerId` INTEGER, `noteReplacesOriginal` INTEGER NOT NULL, `noteTopic` TEXT, `noteBody` TEXT NOT NULL, `noteColor` INTEGER, `noteSharedBy` TEXT, `noteSharedByName` TEXT, `addedDate` INTEGER NOT NULL, PRIMARY KEY(`noteId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "noteId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ownerType", + "columnName": "noteOwnerType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ownerId", + "columnName": "noteOwnerId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "replacesOriginal", + "columnName": "noteReplacesOriginal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "noteTopic", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "body", + "columnName": "noteBody", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "noteColor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharedBy", + "columnName": "noteSharedBy", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedByName", + "columnName": "noteSharedByName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "noteId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_notes_profileId_noteOwnerType_noteOwnerId", + "unique": false, + "columnNames": [ + "profileId", + "noteOwnerType", + "noteOwnerId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_notes_profileId_noteOwnerType_noteOwnerId` ON `${TABLE_NAME}` (`profileId`, `noteOwnerType`, `noteOwnerId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "metadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `metadataId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `thingType` INTEGER NOT NULL, `thingId` INTEGER NOT NULL, `seen` INTEGER NOT NULL, `notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "metadataId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thingType", + "columnName": "thingType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thingId", + "columnName": "thingId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "seen", + "columnName": "seen", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notified", + "columnName": "notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "metadataId" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_metadata_profileId_thingType_thingId", + "unique": true, + "columnNames": [ + "profileId", + "thingType", + "thingId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_metadata_profileId_thingType_thingId` ON `${TABLE_NAME}` (`profileId`, `thingType`, `thingId`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2612ebba9802eedc7ebc69724606a23c')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTimetable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTimetable.kt index 355731a8..6c1acc54 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTimetable.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTimetable.kt @@ -105,6 +105,15 @@ class UsosApiTimetable( val groupName = classTypeId?.plus(groupNumber)?.let { s -> "($s)" } it.classroom = "$buildingId / $roomNumber ${groupName ?: ""}" it.id = it.buildId() + + it.color = when (classTypeId) { + "WYK" -> 0xff0d6091 + "CW" -> 0xff54306e + "LAB" -> 0xff772747 + "KON" -> 0xff1e5128 + "^P?SEM" -> 0xff1e5128 // TODO make it regex + else -> 0xff08534c + }.toInt() } lesson.date?.let { foundDates += it } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt index 93758366..b7f6ccf2 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt @@ -44,7 +44,7 @@ import pl.szczodrzynski.edziennik.data.db.migration.* TimetableManual::class, Note::class, Metadata::class -], version = 97) +], version = 98) @TypeConverters( ConverterTime::class, ConverterDate::class, @@ -185,6 +185,7 @@ abstract class AppDb : RoomDatabase() { Migration95(), Migration96(), Migration97(), + Migration98(), ).allowMainThreadQueries().build() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Lesson.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Lesson.kt index 5123a2f5..37b7f6f3 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Lesson.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Lesson.kt @@ -74,6 +74,8 @@ open class Lesson( @Ignore var showAsUnseen = false + var color: Int? = null + override fun toString(): String { return "Lesson(profileId=$profileId, " + "id=$id, " + diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration98.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration98.kt new file mode 100644 index 00000000..91003fd9 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration98.kt @@ -0,0 +1,15 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-16. + */ + +package pl.szczodrzynski.edziennik.data.db.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration98 : Migration(97, 98) { + override fun migrate(database: SupportSQLiteDatabase) { + // timetable colors - override color in lesson object + database.execSQL("ALTER TABLE timetable ADD COLUMN color INT DEFAULT NULL;") + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableDayFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableDayFragment.kt index 1e4d9773..55250151 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableDayFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableDayFragment.kt @@ -328,7 +328,7 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope { lesson.getNoteSubstituteText(showNotes = true) ?: lesson.displaySubjectName val (subjectTextPrimary, subjectTextSecondary) = if (profileConfig.timetableColorSubjectName) { - val subjectColor = Colors.stringToMaterialColorCRC(lessonText?.toString() ?: "") + val subjectColor = lesson.color ?: Colors.stringToMaterialColorCRC(lessonText?.toString() ?: "") if (lb.annotationVisible) { lb.subjectContainer.background = ColorDrawable(subjectColor) } else { From cf255078509be3b28a6f529e2cb68dcb8d370c1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sun, 16 Oct 2022 18:41:37 +0200 Subject: [PATCH 12/15] [API/Usos] Save lecturers as teachers. Add team class. --- .../data/api/edziennik/usos/data/UsosApi.kt | 8 +++ .../data/api/edziennik/usos/data/UsosData.kt | 7 +- .../edziennik/usos/data/api/UsosApiCourses.kt | 5 +- .../edziennik/usos/data/api/UsosApiUser.kt | 72 +++++++++++++++++++ .../edziennik/data/api/models/Data.kt | 28 +++++--- 5 files changed, 104 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiUser.kt diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt index 829fefa4..d81eff58 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt @@ -41,6 +41,14 @@ open class UsosApi(open val data: DataUsos, open val lastSync: Long?) { protected fun JsonObject.getLangString(key: String) = this.getJsonObject(key)?.getString("pl") + protected fun JsonObject.getLecturerIds(key: String) = + this.getJsonArray(key)?.asJsonObjectList()?.mapNotNull { + val id = it.getLong("id") ?: return@mapNotNull null + val firstName = it.getString("first_name") ?: return@mapNotNull null + val lastName = it.getString("last_name") ?: return@mapNotNull null + data.getTeacher(firstName, lastName, id = id).id + } ?: listOf() + private fun valueToString(value: Any) = when (value) { is String -> value is Number -> value.toString() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt index 6f3b6039..d688bebf 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt @@ -10,6 +10,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.usos.* import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiCourses import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiTerms import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiTimetable +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiUser import pl.szczodrzynski.edziennik.utils.Utils.d class UsosData(val data: DataUsos, val onSuccess: () -> Unit) { @@ -41,10 +42,10 @@ class UsosData(val data: DataUsos, val onSuccess: () -> Unit) { private fun useEndpoint(endpointId: Int, lastSync: Long?, onSuccess: (endpointId: Int) -> Unit) { d(TAG, "Using endpoint $endpointId. Last sync time = $lastSync") when (endpointId) { - /*ENDPOINT_USOS_API_USER -> { + ENDPOINT_USOS_API_USER -> { data.startProgress(R.string.edziennik_progress_endpoint_student_info) -// TemplateWebSample(data, lastSync, onSuccess) - }*/ + UsosApiUser(data, lastSync, onSuccess) + } ENDPOINT_USOS_API_TERMS -> { data.startProgress(R.string.edziennik_progress_endpoint_school_info) UsosApiTerms(data, lastSync, onSuccess) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiCourses.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiCourses.kt index 613d9f08..e93c63dd 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiCourses.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiCourses.kt @@ -36,7 +36,7 @@ class UsosApiCourses( "group_number", // "class_type", "class_type_id", - // "lecturers", + "lecturers", ), ), ), @@ -69,6 +69,7 @@ class UsosApiCourses( val groupNumber = userGroup.getInt("group_number") ?: continue // val classType = userGroup.getLangString("class_type") ?: continue val classTypeId = userGroup.getString("class_type_id") ?: continue + val lecturers = userGroup.getLecturerIds("lecturers") data.teamList.put(courseUnitId, Team( profileId, @@ -76,7 +77,7 @@ class UsosApiCourses( "${profile?.studentClassName} $classTypeId$groupNumber - $courseName", 2, "${data.schoolId}:${courseId} $classTypeId$groupNumber", - -1, + lecturers.firstOrNull() ?: -1L, )) hasValidTeam = true } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiUser.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiUser.kt new file mode 100644 index 00000000..29a3e22b --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiUser.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-16. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api + +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_NO_STUDENT_PROGRAMMES +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_USER +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.ext.* + +class UsosApiUser( + override val data: DataUsos, + override val lastSync: Long?, + val onSuccess: (endpointId: Int) -> Unit, +) : UsosApi(data, lastSync) { + companion object { + const val TAG = "UsosApiUser" + } + + init { + apiRequest( + tag = TAG, + service = "users/user", + params = mapOf( + "fields" to listOf( + "id", + "first_name", + "last_name", + "student_number", + "student_programmes" to listOf( + "programme" to listOf("id"), + ), + ), + ), + responseType = ResponseType.OBJECT, + ) { json, response -> + val programmes = json.getJsonArray("student_programmes") + if (programmes.isNullOrEmpty()) { + data.error(ApiError(TAG, ERROR_USOS_NO_STUDENT_PROGRAMMES) + .withApiResponse(json) + .withResponse(response)) + return@apiRequest + } + + val firstName = json.getString("first_name") + val lastName = json.getString("last_name") + val studentName = buildFullName(firstName, lastName) + + data.studentId = json.getInt("id") ?: data.studentId + profile?.studentNameLong = studentName + profile?.studentNameShort = studentName.getShortName() + profile?.studentNumber = json.getInt("student_number", -1) + profile?.studentClassName = programmes.getJsonObject(0).getJsonObject("programme").getString("id") + + profile?.studentClassName?.let { + data.getTeam( + id = null, + name = it, + schoolCode = data.schoolId ?: "", + isTeamClass = true, + ) + } + + data.setSyncNext(ENDPOINT_USOS_API_USER, 4 * DAY) + onSuccess(ENDPOINT_USOS_API_USER) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt index c02c666a..9b7137fc 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt @@ -449,14 +449,14 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt return team } - fun getTeacher(firstName: String, lastName: String, loginId: String? = null): Teacher { + fun getTeacher(firstName: String, lastName: String, loginId: String? = null, id: Long? = null): Teacher { val teacher = teacherList.singleOrNull { it.fullName == "$firstName $lastName" } - return validateTeacher(teacher, firstName, lastName, loginId) + return validateTeacher(teacher, firstName, lastName, loginId, id) } fun getTeacher(firstNameChar: Char, lastName: String, loginId: String? = null): Teacher { val teacher = teacherList.singleOrNull { it.shortName == "$firstNameChar.$lastName" } - return validateTeacher(teacher, firstNameChar.toString(), lastName, loginId) + return validateTeacher(teacher, firstNameChar.toString(), lastName, loginId, null) } fun getTeacherByLastFirst(nameLastFirst: String, loginId: String? = null): Teacher { @@ -464,9 +464,9 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt val teacher = teacherList.singleOrNull { it.fullNameLastFirst == nameLastFirst } val nameParts = nameLastFirst.split(" ", limit = 2) return if (nameParts.size == 1) - validateTeacher(teacher, nameParts[0], "", loginId) + validateTeacher(teacher, nameParts[0], "", loginId, null) else - validateTeacher(teacher, nameParts[1], nameParts[0], loginId) + validateTeacher(teacher, nameParts[1], nameParts[0], loginId, null) } fun getTeacherByFirstLast(nameFirstLast: String, loginId: String? = null): Teacher { @@ -474,9 +474,9 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt val teacher = teacherList.singleOrNull { it.fullName == nameFirstLast } val nameParts = nameFirstLast.split(" ", limit = 2) return if (nameParts.size == 1) - validateTeacher(teacher, nameParts[0], "", loginId) + validateTeacher(teacher, nameParts[0], "", loginId, null) else - validateTeacher(teacher, nameParts[0], nameParts[1], loginId) + validateTeacher(teacher, nameParts[0], nameParts[1], loginId, null) } fun getTeacherByFDotLast(nameFDotLast: String, loginId: String? = null): Teacher { @@ -495,10 +495,16 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt getTeacher(nameParts[0][0], nameParts[1], loginId) } - private fun validateTeacher(teacher: Teacher?, firstName: String, lastName: String, loginId: String?): Teacher { - val obj = teacher ?: Teacher(profileId, -1, firstName, lastName, loginId).apply { - id = fullName.crc32() - teacherList[id] = this + private fun validateTeacher( + teacher: Teacher?, + firstName: String, + lastName: String, + loginId: String?, + id: Long? + ): Teacher { + val obj = teacher ?: Teacher(profileId, -1, firstName, lastName, loginId).also { + it.id = id ?: it.fullName.crc32() + teacherList[it.id] = it } return obj.also { if (loginId != null) From dc19043f73e85e8361973a428fe5e43df5e9736c Mon Sep 17 00:00:00 2001 From: kuba2k2 Date: Mon, 17 Oct 2022 12:56:07 +0200 Subject: [PATCH 13/15] [API/Usos] Implement basic error handling. --- .../edziennik/data/api/Errors.kt | 1 + .../data/api/edziennik/usos/data/UsosApi.kt | 31 ++++++++++++++++--- 2 files changed, 27 insertions(+), 5 deletions(-) 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 8be89c6e..93a7228f 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 @@ -204,6 +204,7 @@ const val ERROR_USOS_OAUTH_GOT_DIFFERENT_TOKEN = 702 const val ERROR_USOS_OAUTH_INCOMPLETE_RESPONSE = 703 const val ERROR_USOS_NO_STUDENT_PROGRAMMES = 704 const val ERROR_USOS_API_INCOMPLETE_RESPONSE = 705 +const val ERROR_USOS_API_MISSING_RESPONSE = 706 const val ERROR_TEMPLATE_WEB_OTHER = 801 diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt index d81eff58..9f716d50 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt @@ -13,8 +13,10 @@ import im.wangchao.mhttp.callback.JsonArrayCallbackHandler import im.wangchao.mhttp.callback.JsonCallbackHandler import im.wangchao.mhttp.callback.TextCallbackHandler import pl.szczodrzynski.edziennik.data.api.ERROR_REQUEST_FAILURE +import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_MISSING_RESPONSE import pl.szczodrzynski.edziennik.data.api.SERVER_USER_AGENT import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.login.UsosLoginApi import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.Utils.d @@ -141,7 +143,7 @@ open class UsosApi(open val data: DataUsos, open val lastSync: Long?) { ) = when (responseType) { ResponseType.OBJECT -> object : JsonCallbackHandler() { override fun onSuccess(data: JsonObject?, response: Response) { - processResponse(response, data as T, onSuccess) + processResponse(tag, response, data as T?, onSuccess) } override fun onFailure(response: Response?, throwable: Throwable?) { @@ -150,7 +152,7 @@ open class UsosApi(open val data: DataUsos, open val lastSync: Long?) { } ResponseType.ARRAY -> object : JsonArrayCallbackHandler() { override fun onSuccess(data: JsonArray?, response: Response) { - processResponse(response, data as T, onSuccess) + processResponse(tag, response, data as T?, onSuccess) } override fun onFailure(response: Response?, throwable: Throwable?) { @@ -159,7 +161,7 @@ open class UsosApi(open val data: DataUsos, open val lastSync: Long?) { } ResponseType.PLAIN -> object : TextCallbackHandler() { override fun onSuccess(data: String?, response: Response) { - processResponse(response, data as T, onSuccess) + processResponse(tag, response, data as T?, onSuccess) } override fun onFailure(response: Response?, throwable: Throwable?) { @@ -169,11 +171,30 @@ open class UsosApi(open val data: DataUsos, open val lastSync: Long?) { } private fun processResponse( + tag: String, response: Response, - data: T, + value: T?, onSuccess: (data: T, response: Response?) -> Unit, ) { - onSuccess(data, response) + val errorCode = when { + response.code() == HTTP_UNAUTHORIZED -> { + data.oauthTokenKey = null + data.oauthTokenSecret = null + data.oauthTokenIsUser = false + data.oauthLoginResponse = null + UsosLoginApi(data) { } + return + } + value == null -> ERROR_USOS_API_MISSING_RESPONSE + response.code() == HTTP_OK -> { + onSuccess(value, response) + null + } + else -> response.toErrorCode() + } + if (errorCode != null) { + data.error(tag, errorCode, response, value.toString()) + } } private fun processError( From 3ab9602865073a5dbb47e45be6484063021e5e2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Mon, 17 Oct 2022 16:06:13 +0200 Subject: [PATCH 14/15] [API/Usos] Fix re-logging in after user action. --- .../data/api/edziennik/usos/login/UsosLoginApi.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt index 11172d7d..06651477 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt @@ -10,10 +10,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent import pl.szczodrzynski.edziennik.data.api.models.ApiError -import pl.szczodrzynski.edziennik.ext.Bundle -import pl.szczodrzynski.edziennik.ext.fromQueryString -import pl.szczodrzynski.edziennik.ext.toBundle -import pl.szczodrzynski.edziennik.ext.toQueryString +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.Utils.d class UsosLoginApi(val data: DataUsos, val onSuccess: () -> Unit) { @@ -25,6 +22,9 @@ class UsosLoginApi(val data: DataUsos, val onSuccess: () -> Unit) { init { run { + data.arguments?.getString("oauthLoginResponse")?.let { + data.oauthLoginResponse = it + } if (data.isApiLoginValid()) { onSuccess() } else if (data.oauthLoginResponse != null) { @@ -36,6 +36,8 @@ class UsosLoginApi(val data: DataUsos, val onSuccess: () -> Unit) { } private fun authorize() { + data.oauthTokenKey = null + data.oauthTokenSecret = null api.apiRequest( tag = TAG, service = "oauth/request_token", From 52a53334cac01a47726b41e6296b169e69a1b6a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Mon, 17 Oct 2022 22:30:23 +0200 Subject: [PATCH 15/15] [Strings] Add USOS error descriptions. --- app/src/main/res/values/errors.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/src/main/res/values/errors.xml b/app/src/main/res/values/errors.xml index a3b498da..557eaea8 100644 --- a/app/src/main/res/values/errors.xml +++ b/app/src/main/res/values/errors.xml @@ -30,6 +30,7 @@ ERROR_LOGIN_METHOD_NOT_SATISFIED ERROR_NOT_IMPLEMENTED ERROR_FILE_DOWNLOAD + ERROR_REQUIRES_USER_ACTION ERROR_API_PDO_ERROR ERROR_API_INVALID_CLIENT @@ -169,6 +170,12 @@ ERROR_PODLASIE_API_OTHER ERROR_PODLASIE_API_DATA_MISSING + ERROR_USOS_OAUTH_GOT_DIFFERENT_TOKEN + ERROR_USOS_OAUTH_INCOMPLETE_RESPONSE + ERROR_USOS_NO_STUDENT_PROGRAMMES + ERROR_USOS_API_INCOMPLETE_RESPONSE + ERROR_USOS_API_MISSING_RESPONSE + ERROR_TEMPLATE_WEB_OTHER EXCEPTION_API_TASK @@ -215,6 +222,7 @@ Nie można wywołać metody logowania. Skontaktuj się z twórcą aplikacji. Nie zaimplementowano Wystąpił błąd podczas pobierania pliku. Dziennik może być przeciążony lub mieć przerwę techniczną. + Wymagana akcja w aplikacji ERROR_API_PDO_ERROR Nieprawidłowy ID klienta API @@ -354,6 +362,12 @@ ERROR_PODLASIE_API_OTHER Brak danych. Zgłoś błąd programiście. + Błąd logowania: otrzymano nieprawidłowy token + Błąd logowania: niekompletna odpowiedź serwera + Student nie jest zapisany na żaden kierunek + Brakujące dane w odpowiedzi serwera + Brakująca odpowiedź serwera + ERROR_TEMPLATE_WEB_OTHER Błąd synchronizacji. Upewnij się, że masz połączenie z internetem, a następnie zgłoś błąd.