forked from github/szkolny
Merge pull request #142 from szkolny-eu/feature/usos
[API] Add USOS API support.
This commit is contained in:
commit
3a91f87ccd
1
.idea/dictionaries/Kuba.xml
generated
1
.idea/dictionaries/Kuba.xml
generated
@ -13,6 +13,7 @@
|
|||||||
<w>synergia</w>
|
<w>synergia</w>
|
||||||
<w>szczodrzyński</w>
|
<w>szczodrzyński</w>
|
||||||
<w>szkolny</w>
|
<w>szkolny</w>
|
||||||
|
<w>usos</w>
|
||||||
</words>
|
</words>
|
||||||
</dictionary>
|
</dictionary>
|
||||||
</component>
|
</component>
|
2314
app/schemas/pl.szczodrzynski.edziennik.data.db.AppDb/98.json
Normal file
2314
app/schemas/pl.szczodrzynski.edziennik.data.db.AppDb/98.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -157,6 +157,10 @@
|
|||||||
android:configChanges="orientation|keyboardHidden"
|
android:configChanges="orientation|keyboardHidden"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:theme="@style/Base.Theme.AppCompat" />
|
android:theme="@style/Base.Theme.AppCompat" />
|
||||||
|
<activity android:name=".ui.login.oauth.OAuthLoginActivity"
|
||||||
|
android:configChanges="orientation|keyboardHidden"
|
||||||
|
android:exported="false"
|
||||||
|
android:theme="@style/AppTheme.Light" />
|
||||||
<activity android:name=".ui.base.BuildInvalidActivity" android:exported="false" />
|
<activity android:name=".ui.base.BuildInvalidActivity" android:exported="false" />
|
||||||
<activity android:name=".ui.settings.contributors.ContributorsActivity" android:exported="false" />
|
<activity android:name=".ui.settings.contributors.ContributorsActivity" android:exported="false" />
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ import org.greenrobot.eventbus.EventBus
|
|||||||
import org.greenrobot.eventbus.Subscribe
|
import org.greenrobot.eventbus.Subscribe
|
||||||
import org.greenrobot.eventbus.ThreadMode
|
import org.greenrobot.eventbus.ThreadMode
|
||||||
import pl.droidsonroids.gif.GifDrawable
|
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.ERROR_VULCAN_API_DEPRECATED
|
||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
|
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
|
||||||
import pl.szczodrzynski.edziennik.data.api.events.*
|
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.home.HomeFragment
|
||||||
import pl.szczodrzynski.edziennik.ui.homework.HomeworkFragment
|
import pl.szczodrzynski.edziennik.ui.homework.HomeworkFragment
|
||||||
import pl.szczodrzynski.edziennik.ui.login.LoginActivity
|
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.compose.MessagesComposeFragment
|
||||||
import pl.szczodrzynski.edziennik.ui.messages.list.MessagesFragment
|
import pl.szczodrzynski.edziennik.ui.messages.list.MessagesFragment
|
||||||
import pl.szczodrzynski.edziennik.ui.messages.single.MessageFragment
|
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.d
|
||||||
import pl.szczodrzynski.edziennik.utils.Utils.dpToPx
|
import pl.szczodrzynski.edziennik.utils.Utils.dpToPx
|
||||||
import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager.Error.Type
|
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.Date
|
||||||
import pl.szczodrzynski.edziennik.utils.models.NavTarget
|
import pl.szczodrzynski.edziennik.utils.models.NavTarget
|
||||||
import pl.szczodrzynski.navlib.*
|
import pl.szczodrzynski.navlib.*
|
||||||
@ -853,7 +856,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
|
|||||||
|
|
||||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||||
fun onUserActionRequiredEvent(event: UserActionRequiredEvent) {
|
fun onUserActionRequiredEvent(event: UserActionRequiredEvent) {
|
||||||
app.userActionManager.execute(this, event.profileId, event.type)
|
app.userActionManager.execute(this, event, UserActionManager.UserActionCallback())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fragmentToSyncName(currentFragment: Int): Int {
|
private fun fragmentToSyncName(currentFragment: Int): Int {
|
||||||
@ -911,11 +914,13 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
"userActionRequired" -> {
|
"userActionRequired" -> {
|
||||||
app.userActionManager.execute(
|
val event = UserActionRequiredEvent(
|
||||||
this,
|
profileId = extras.getInt("profileId"),
|
||||||
extras.getInt("profileId"),
|
type = extras.getEnum<UserActionRequiredEvent.Type>("type") ?: return,
|
||||||
extras.getInt("type")
|
params = extras.getBundle("params") ?: return,
|
||||||
|
errorText = 0,
|
||||||
)
|
)
|
||||||
|
app.userActionManager.execute(this, event, UserActionManager.UserActionCallback())
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
"createManualEvent" -> {
|
"createManualEvent" -> {
|
||||||
|
@ -84,19 +84,21 @@ class ApiService : Service() {
|
|||||||
runTask()
|
runTask()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRequiresUserAction(event: UserActionRequiredEvent) {
|
||||||
|
app.userActionManager.sendToUser(event)
|
||||||
|
taskRunning?.cancel()
|
||||||
|
clearTask()
|
||||||
|
runTask()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onError(apiError: ApiError) {
|
override fun onError(apiError: ApiError) {
|
||||||
lastEventTime = System.currentTimeMillis()
|
lastEventTime = System.currentTimeMillis()
|
||||||
d(TAG, "Task $taskRunningId threw an error - $apiError")
|
d(TAG, "Task $taskRunningId threw an error - $apiError")
|
||||||
apiError.profileId = taskProfileId
|
apiError.profileId = taskProfileId
|
||||||
|
|
||||||
if (app.userActionManager.requiresUserAction(apiError)) {
|
|
||||||
app.userActionManager.sendToUser(apiError)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
EventBus.getDefault().postSticky(ApiTaskErrorEvent(apiError))
|
EventBus.getDefault().postSticky(ApiTaskErrorEvent(apiError))
|
||||||
errorList.add(apiError)
|
errorList.add(apiError)
|
||||||
apiError.throwable?.printStackTrace()
|
apiError.throwable?.printStackTrace()
|
||||||
}
|
|
||||||
|
|
||||||
if (apiError.isCritical) {
|
if (apiError.isCritical) {
|
||||||
taskRunning?.cancel()
|
taskRunning?.cancel()
|
||||||
|
@ -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_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_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
|
val MOBIDZIENNIK_USER_AGENT = SYSTEM_USER_AGENT
|
||||||
|
|
||||||
@ -99,3 +102,12 @@ const val PODLASIE_API_VERSION = "1.0.62"
|
|||||||
const val PODLASIE_API_URL = "https://cpdklaser.zeto.bialystok.pl/api"
|
const val PODLASIE_API_URL = "https://cpdklaser.zeto.bialystok.pl/api"
|
||||||
const val PODLASIE_API_USER_ENDPOINT = "/pobierzDaneUcznia"
|
const val PODLASIE_API_USER_ENDPOINT = "/pobierzDaneUcznia"
|
||||||
const val PODLASIE_API_LOGOUT_DEVICES_ENDPOINT = "/wyczyscUrzadzenia"
|
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",
|
||||||
|
) }
|
||||||
|
@ -58,11 +58,7 @@ const val ERROR_INVALID_LOGIN_MODE = 110
|
|||||||
const val ERROR_LOGIN_METHOD_NOT_SATISFIED = 111
|
const val ERROR_LOGIN_METHOD_NOT_SATISFIED = 111
|
||||||
const val ERROR_NOT_IMPLEMENTED = 112
|
const val ERROR_NOT_IMPLEMENTED = 112
|
||||||
const val ERROR_FILE_DOWNLOAD = 113
|
const val ERROR_FILE_DOWNLOAD = 113
|
||||||
|
const val ERROR_REQUIRES_USER_ACTION = 114
|
||||||
const val ERROR_NO_STUDENTS_IN_ACCOUNT = 115
|
|
||||||
|
|
||||||
const val ERROR_CAPTCHA_NEEDED = 3000
|
|
||||||
const val ERROR_CAPTCHA_LIBRUS_PORTAL = 3001
|
|
||||||
|
|
||||||
const val ERROR_API_PDO_ERROR = 5000
|
const val ERROR_API_PDO_ERROR = 5000
|
||||||
const val ERROR_API_INVALID_CLIENT = 5001
|
const val ERROR_API_INVALID_CLIENT = 5001
|
||||||
@ -204,6 +200,12 @@ const val ERROR_PODLASIE_API_NO_TOKEN = 630
|
|||||||
const val ERROR_PODLASIE_API_OTHER = 631
|
const val ERROR_PODLASIE_API_OTHER = 631
|
||||||
const val ERROR_PODLASIE_API_DATA_MISSING = 632
|
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_USOS_API_MISSING_RESPONSE = 706
|
||||||
|
|
||||||
const val ERROR_TEMPLATE_WEB_OTHER = 801
|
const val ERROR_TEMPLATE_WEB_OTHER = 801
|
||||||
|
|
||||||
const val EXCEPTION_API_TASK = 900
|
const val EXCEPTION_API_TASK = 900
|
||||||
|
@ -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.podlasie.login.PodlasieLoginApi
|
||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.template.login.TemplateLoginApi
|
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.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.VulcanLoginHebe
|
||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginWebMain
|
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginWebMain
|
||||||
import pl.szczodrzynski.edziennik.data.api.models.LoginMethod
|
import pl.szczodrzynski.edziennik.data.api.models.LoginMethod
|
||||||
@ -127,6 +128,15 @@ val podlasieLoginMethods = listOf(
|
|||||||
.withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED }
|
.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(
|
val templateLoginMethods = listOf(
|
||||||
LoginMethod(LOGIN_TYPE_TEMPLATE, LOGIN_METHOD_TEMPLATE_WEB, TemplateLoginWeb::class.java)
|
LoginMethod(LOGIN_TYPE_TEMPLATE, LOGIN_METHOD_TEMPLATE_WEB, TemplateLoginWeb::class.java)
|
||||||
.withIsPossible { _, _ -> true }
|
.withIsPossible { _, _ -> true }
|
||||||
|
@ -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.mobidziennik.Mobidziennik
|
||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.Podlasie
|
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.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.edziennik.vulcan.Vulcan
|
||||||
import pl.szczodrzynski.edziennik.data.api.events.RegisterAvailabilityEvent
|
import pl.szczodrzynski.edziennik.data.api.events.RegisterAvailabilityEvent
|
||||||
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
|
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_VULCAN -> Vulcan(app, profile, loginStore, taskCallback)
|
||||||
LOGIN_TYPE_PODLASIE -> Podlasie(app, profile, loginStore, taskCallback)
|
LOGIN_TYPE_PODLASIE -> Podlasie(app, profile, loginStore, taskCallback)
|
||||||
LOGIN_TYPE_TEMPLATE -> Template(app, profile, loginStore, taskCallback)
|
LOGIN_TYPE_TEMPLATE -> Template(app, profile, loginStore, taskCallback)
|
||||||
|
LOGIN_TYPE_USOS -> Usos(app, profile, loginStore, taskCallback)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
if (edziennikInterface == null) {
|
if (edziennikInterface == null) {
|
||||||
|
@ -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.data.synergia.*
|
||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.firstlogin.LibrusFirstLogin
|
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.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.EdziennikCallback
|
||||||
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
|
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
|
||||||
import pl.szczodrzynski.edziennik.data.api.models.ApiError
|
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 {
|
private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback {
|
||||||
return object : EdziennikCallback {
|
return object : EdziennikCallback {
|
||||||
override fun onCompleted() { callback.onCompleted() }
|
override fun onCompleted() { callback.onCompleted() }
|
||||||
|
override fun onRequiresUserAction(event: UserActionRequiredEvent) { callback.onRequiresUserAction(event) }
|
||||||
override fun onProgress(step: Float) { callback.onProgress(step) }
|
override fun onProgress(step: Float) { callback.onProgress(step) }
|
||||||
override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) }
|
override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) }
|
||||||
override fun onError(apiError: ApiError) {
|
override fun onError(apiError: ApiError) {
|
||||||
|
@ -10,6 +10,7 @@ import im.wangchao.mhttp.callback.TextCallbackHandler
|
|||||||
import pl.szczodrzynski.edziennik.*
|
import pl.szczodrzynski.edziennik.*
|
||||||
import pl.szczodrzynski.edziennik.data.api.*
|
import pl.szczodrzynski.edziennik.data.api.*
|
||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus
|
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.data.api.models.ApiError
|
||||||
import pl.szczodrzynski.edziennik.ext.*
|
import pl.szczodrzynski.edziennik.ext.*
|
||||||
import pl.szczodrzynski.edziennik.utils.Utils.d
|
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
|
val error = if (response.code() == 200) null else
|
||||||
json.getJsonArray("errors")?.getString(0)
|
json.getJsonArray("errors")?.getString(0)
|
||||||
?: json.getJsonObject("errors")?.entrySet()?.firstOrNull()?.value?.asString
|
?: 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 ->
|
error?.let { code ->
|
||||||
when {
|
when {
|
||||||
code.contains("Sesja logowania wygasła") -> ERROR_LOGIN_LIBRUS_PORTAL_CSRF_EXPIRED
|
code.contains("Sesja logowania wygasła") -> ERROR_LOGIN_LIBRUS_PORTAL_CSRF_EXPIRED
|
||||||
code.contains("Upewnij się, że nie") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN
|
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
|
code.contains("Podany adres e-mail jest nieprawidłowy.") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN
|
||||||
else -> ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR
|
else -> ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR
|
||||||
}.let { errorCode ->
|
}.let { errorCode ->
|
||||||
@ -163,12 +175,6 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
|
|||||||
return
|
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))
|
authorize(json.getString("redirect", LIBRUS_AUTHORIZE_URL))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.data.web.*
|
||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.firstlogin.MobidziennikFirstLogin
|
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.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.EdziennikCallback
|
||||||
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
|
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
|
||||||
import pl.szczodrzynski.edziennik.data.api.models.ApiError
|
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 {
|
private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback {
|
||||||
return object : EdziennikCallback {
|
return object : EdziennikCallback {
|
||||||
override fun onCompleted() { callback.onCompleted() }
|
override fun onCompleted() { callback.onCompleted() }
|
||||||
|
override fun onRequiresUserAction(event: UserActionRequiredEvent) { callback.onRequiresUserAction(event) }
|
||||||
override fun onProgress(step: Float) { callback.onProgress(step) }
|
override fun onProgress(step: Float) { callback.onProgress(step) }
|
||||||
override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) }
|
override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) }
|
||||||
override fun onError(apiError: ApiError) {
|
override fun onError(apiError: ApiError) {
|
||||||
|
@ -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.firstlogin.PodlasieFirstLogin
|
||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.login.PodlasieLogin
|
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.AttachmentGetEvent
|
||||||
|
import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
|
||||||
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
|
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
|
||||||
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
|
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
|
||||||
import pl.szczodrzynski.edziennik.data.api.models.ApiError
|
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()
|
callback.onCompleted()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRequiresUserAction(event: UserActionRequiredEvent) {
|
||||||
|
callback.onRequiresUserAction(event)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onProgress(step: Float) {
|
override fun onProgress(step: Float) {
|
||||||
callback.onProgress(step)
|
callback.onProgress(step)
|
||||||
}
|
}
|
||||||
|
@ -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.data.TemplateData
|
||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.template.firstlogin.TemplateFirstLogin
|
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.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.EdziennikCallback
|
||||||
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
|
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
|
||||||
import pl.szczodrzynski.edziennik.data.api.models.ApiError
|
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()
|
callback.onCompleted()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRequiresUserAction(event: UserActionRequiredEvent) {
|
||||||
|
callback.onRequiresUserAction(event)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onProgress(step: Float) {
|
override fun onProgress(step: Float) {
|
||||||
callback.onProgress(step)
|
callback.onProgress(step)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* 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 && oauthTokenIsUser
|
||||||
|
|
||||||
|
override fun satisfyLoginMethods() {
|
||||||
|
loginMethods.clear()
|
||||||
|
if (isApiLoginValid()) {
|
||||||
|
loginMethods += LOGIN_METHOD_USOS_API
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun generateUserCode() = "$schoolId:${profile?.studentNumber ?: studentId}"
|
||||||
|
|
||||||
|
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 }
|
||||||
|
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 }
|
||||||
|
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
|
||||||
|
|
||||||
|
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: Int
|
||||||
|
get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", 0); return mStudentId ?: 0 }
|
||||||
|
set(value) { profile?.putStudentData("studentId", value) ?: return; mStudentId = value }
|
||||||
|
private var mStudentId: Int? = null
|
||||||
|
}
|
@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
* 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.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
|
||||||
|
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<Int>()
|
||||||
|
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<Int>,
|
||||||
|
viewId: Int?,
|
||||||
|
onlyEndpoints: List<Int>?,
|
||||||
|
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<Teacher>, 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 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) {
|
||||||
|
when (apiError.errorCode) {
|
||||||
|
in internalErrorList -> {
|
||||||
|
// finish immediately if the same error occurs twice during the same sync
|
||||||
|
callback.onError(apiError)
|
||||||
|
}
|
||||||
|
else -> callback.onError(apiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) Kuba Szczodrzyński 2022-10-11.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package pl.szczodrzynski.edziennik.data.api.edziennik.usos
|
||||||
|
|
||||||
|
import pl.szczodrzynski.edziennik.data.api.*
|
||||||
|
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_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)),
|
||||||
|
)
|
@ -0,0 +1,209 @@
|
|||||||
|
/*
|
||||||
|
* 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.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.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
|
||||||
|
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
|
||||||
|
|
||||||
|
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()
|
||||||
|
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, String>): 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 <T> apiRequest(
|
||||||
|
tag: String,
|
||||||
|
service: String,
|
||||||
|
params: Map<String, Any>? = null,
|
||||||
|
fields: List<Any>? = null,
|
||||||
|
responseType: ResponseType,
|
||||||
|
onSuccess: (data: T, response: Response?) -> Unit,
|
||||||
|
) {
|
||||||
|
val url = "${data.instanceUrl}services/$service"
|
||||||
|
d(tag, "Request: Usos/Api - $url")
|
||||||
|
|
||||||
|
val formData = mutableMapOf<String, String>()
|
||||||
|
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 ?: ""),
|
||||||
|
"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(
|
||||||
|
method = "POST",
|
||||||
|
url = url,
|
||||||
|
params = formData + auth.filterKeys { it.startsWith("oauth_") },
|
||||||
|
)
|
||||||
|
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 <T> getCallback(
|
||||||
|
tag: String,
|
||||||
|
responseType: ResponseType,
|
||||||
|
onSuccess: (data: T, response: Response?) -> Unit,
|
||||||
|
) = when (responseType) {
|
||||||
|
ResponseType.OBJECT -> object : JsonCallbackHandler() {
|
||||||
|
override fun onSuccess(data: JsonObject?, response: Response) {
|
||||||
|
processResponse(tag, 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(tag, 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(tag, response, data as T?, onSuccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(response: Response?, throwable: Throwable?) {
|
||||||
|
processError(tag, response, throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> processResponse(
|
||||||
|
tag: String,
|
||||||
|
response: Response,
|
||||||
|
value: T?,
|
||||||
|
onSuccess: (data: T, response: Response?) -> Unit,
|
||||||
|
) {
|
||||||
|
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(
|
||||||
|
tag: String,
|
||||||
|
response: Response?,
|
||||||
|
throwable: Throwable?,
|
||||||
|
) {
|
||||||
|
data.error(ApiError(tag, ERROR_REQUEST_FAILURE)
|
||||||
|
.withResponse(response)
|
||||||
|
.withThrowable(throwable))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* 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.*
|
||||||
|
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) {
|
||||||
|
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)
|
||||||
|
UsosApiUser(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)
|
||||||
|
}
|
||||||
|
ENDPOINT_USOS_API_TIMETABLE -> {
|
||||||
|
data.startProgress(R.string.edziennik_progress_endpoint_timetable)
|
||||||
|
UsosApiTimetable(data, lastSync, onSuccess)
|
||||||
|
}
|
||||||
|
else -> onSuccess(endpointId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* 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<JsonObject>(
|
||||||
|
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
|
||||||
|
val lecturers = userGroup.getLecturerIds("lecturers")
|
||||||
|
|
||||||
|
data.teamList.put(courseUnitId, Team(
|
||||||
|
profileId,
|
||||||
|
courseUnitId,
|
||||||
|
"${profile?.studentClassName} $classTypeId$groupNumber - $courseName",
|
||||||
|
2,
|
||||||
|
"${data.schoolId}:${courseId} $classTypeId$groupNumber",
|
||||||
|
lecturers.firstOrNull() ?: -1L,
|
||||||
|
))
|
||||||
|
hasValidTeam = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hasValidTeam
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) Kuba Szczodrzyński 2022-10-15.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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_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<JsonArray>(
|
||||||
|
tag = TAG,
|
||||||
|
service = "terms/search",
|
||||||
|
params = mapOf(
|
||||||
|
"query" to Date.getToday().year.toString(),
|
||||||
|
),
|
||||||
|
responseType = ResponseType.ARRAY,
|
||||||
|
) { json, response ->
|
||||||
|
if (!processResponse(json)) {
|
||||||
|
data.error(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<Date>()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
* 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<JsonArray>(
|
||||||
|
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<Date>): Boolean {
|
||||||
|
val foundDates = mutableSetOf<Date>()
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
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 }
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -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<JsonObject>(
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) Kuba Szczodrzyński 2022-10-14.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 {
|
||||||
|
private const val TAG = "UsosFirstLogin"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val api = UsosApi(data, null)
|
||||||
|
|
||||||
|
init {
|
||||||
|
val loginStoreId = data.loginStore.id
|
||||||
|
val loginStoreType = LOGIN_TYPE_USOS
|
||||||
|
var firstProfileId = loginStoreId
|
||||||
|
|
||||||
|
UsosLoginApi(data) {
|
||||||
|
api.apiRequest<JsonObject>(
|
||||||
|
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"),
|
||||||
|
),
|
||||||
|
).also {
|
||||||
|
it.studentNumber = json.getInt("student_number", -1)
|
||||||
|
it.studentClassName = programmes.getJsonObject(0).getJsonObject("programme").getString("id")
|
||||||
|
}
|
||||||
|
|
||||||
|
EventBus.getDefault().postSticky(
|
||||||
|
FirstLoginFinishedEvent(listOf(profile), data.loginStore),
|
||||||
|
)
|
||||||
|
onSuccess()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* 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.*
|
||||||
|
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.*
|
||||||
|
import pl.szczodrzynski.edziennik.utils.Utils.d
|
||||||
|
|
||||||
|
class UsosLoginApi(val data: DataUsos, val onSuccess: () -> Unit) {
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "UsosLoginApi"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val api = UsosApi(data, null)
|
||||||
|
|
||||||
|
init {
|
||||||
|
run {
|
||||||
|
data.arguments?.getString("oauthLoginResponse")?.let {
|
||||||
|
data.oauthLoginResponse = it
|
||||||
|
}
|
||||||
|
if (data.isApiLoginValid()) {
|
||||||
|
onSuccess()
|
||||||
|
} else if (data.oauthLoginResponse != null) {
|
||||||
|
login()
|
||||||
|
} else {
|
||||||
|
authorize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun authorize() {
|
||||||
|
data.oauthTokenKey = null
|
||||||
|
data.oauthTokenSecret = null
|
||||||
|
api.apiRequest<String>(
|
||||||
|
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,
|
||||||
|
) { 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(
|
||||||
|
"interactivity" to "confirm_user",
|
||||||
|
"oauth_token" to (data.oauthTokenKey ?: ""),
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun login() {
|
||||||
|
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<String>(
|
||||||
|
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
|
||||||
|
data.loginStore.removeLoginData("oauthLoginResponse")
|
||||||
|
|
||||||
|
if (!data.oauthTokenIsUser)
|
||||||
|
data.error(ApiError(TAG, ERROR_USOS_OAUTH_INCOMPLETE_RESPONSE)
|
||||||
|
.withApiResponse(text)
|
||||||
|
.withResponse(response))
|
||||||
|
else
|
||||||
|
onSuccess()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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.AttachmentGetEvent
|
||||||
import pl.szczodrzynski.edziennik.data.api.events.EventGetEvent
|
import pl.szczodrzynski.edziennik.data.api.events.EventGetEvent
|
||||||
import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent
|
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.EdziennikCallback
|
||||||
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
|
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
|
||||||
import pl.szczodrzynski.edziennik.data.api.models.ApiError
|
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 {
|
private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback {
|
||||||
return object : EdziennikCallback {
|
return object : EdziennikCallback {
|
||||||
override fun onCompleted() { callback.onCompleted() }
|
override fun onCompleted() { callback.onCompleted() }
|
||||||
|
override fun onRequiresUserAction(event: UserActionRequiredEvent) { callback.onRequiresUserAction(event) }
|
||||||
override fun onProgress(step: Float) { callback.onProgress(step) }
|
override fun onProgress(step: Float) { callback.onProgress(step) }
|
||||||
override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) }
|
override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) }
|
||||||
override fun onError(apiError: ApiError) {
|
override fun onError(apiError: ApiError) {
|
||||||
|
@ -4,11 +4,16 @@
|
|||||||
|
|
||||||
package pl.szczodrzynski.edziennik.data.api.events
|
package pl.szczodrzynski.edziennik.data.api.events
|
||||||
|
|
||||||
data class UserActionRequiredEvent(val profileId: Int, val type: Int) {
|
import android.os.Bundle
|
||||||
companion object {
|
|
||||||
const val LOGIN_DATA_MOBIDZIENNIK = 101
|
data class UserActionRequiredEvent(
|
||||||
const val LOGIN_DATA_LIBRUS = 102
|
val profileId: Int?,
|
||||||
const val LOGIN_DATA_VULCAN = 104
|
val type: Type,
|
||||||
const val CAPTCHA_LIBRUS = 202
|
val params: Bundle,
|
||||||
|
val errorText: Int,
|
||||||
|
) {
|
||||||
|
enum class Type {
|
||||||
|
RECAPTCHA,
|
||||||
|
OAUTH,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
package pl.szczodrzynski.edziennik.data.api.interfaces
|
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.Feature
|
||||||
import pl.szczodrzynski.edziennik.data.api.models.LoginMethod
|
import pl.szczodrzynski.edziennik.data.api.models.LoginMethod
|
||||||
|
|
||||||
@ -14,4 +15,5 @@ import pl.szczodrzynski.edziennik.data.api.models.LoginMethod
|
|||||||
*/
|
*/
|
||||||
interface EdziennikCallback : EndpointCallback {
|
interface EdziennikCallback : EndpointCallback {
|
||||||
fun onCompleted()
|
fun onCompleted()
|
||||||
|
fun onRequiresUserAction(event: UserActionRequiredEvent)
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
package pl.szczodrzynski.edziennik.data.api.models
|
package pl.szczodrzynski.edziennik.data.api.models
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import im.wangchao.mhttp.Request
|
import im.wangchao.mhttp.Request
|
||||||
import im.wangchao.mhttp.Response
|
import im.wangchao.mhttp.Response
|
||||||
@ -30,6 +31,7 @@ class ApiError(val tag: String, var errorCode: Int) {
|
|||||||
var request: Request? = null
|
var request: Request? = null
|
||||||
var response: Response? = null
|
var response: Response? = null
|
||||||
var isCritical = true
|
var isCritical = true
|
||||||
|
var params: Bundle? = null
|
||||||
|
|
||||||
fun withThrowable(throwable: Throwable?): ApiError {
|
fun withThrowable(throwable: Throwable?): ApiError {
|
||||||
this.throwable = throwable
|
this.throwable = throwable
|
||||||
@ -58,6 +60,11 @@ class ApiError(val tag: String, var errorCode: Int) {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun withParams(bundle: Bundle): ApiError {
|
||||||
|
this.params = bundle
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
fun getStringText(context: Context): String {
|
fun getStringText(context: Context): String {
|
||||||
return context.resources.getIdentifier("error_${errorCode}", "string", context.packageName).let {
|
return context.resources.getIdentifier("error_${errorCode}", "string", context.packageName).let {
|
||||||
if (it != 0)
|
if (it != 0)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package pl.szczodrzynski.edziennik.data.api.models
|
package pl.szczodrzynski.edziennik.data.api.models
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
import android.util.LongSparseArray
|
import android.util.LongSparseArray
|
||||||
import android.util.SparseArray
|
import android.util.SparseArray
|
||||||
import androidx.core.util.set
|
import androidx.core.util.set
|
||||||
@ -12,7 +13,8 @@ import pl.szczodrzynski.edziennik.BuildConfig
|
|||||||
import pl.szczodrzynski.edziennik.R
|
import pl.szczodrzynski.edziennik.R
|
||||||
import pl.szczodrzynski.edziennik.data.api.ERROR_REQUEST_FAILURE
|
import pl.szczodrzynski.edziennik.data.api.ERROR_REQUEST_FAILURE
|
||||||
import pl.szczodrzynski.edziennik.data.api.Regexes.MESSAGE_META
|
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.AppDb
|
||||||
import pl.szczodrzynski.edziennik.data.db.entity.*
|
import pl.szczodrzynski.edziennik.data.db.entity.*
|
||||||
import pl.szczodrzynski.edziennik.ext.*
|
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
|
* 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.
|
* 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)
|
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) {
|
fun progress(step: Float) {
|
||||||
callback.onProgress(step)
|
callback.onProgress(step)
|
||||||
}
|
}
|
||||||
@ -438,14 +449,14 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
|
|||||||
return team
|
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" }
|
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 {
|
fun getTeacher(firstNameChar: Char, lastName: String, loginId: String? = null): Teacher {
|
||||||
val teacher = teacherList.singleOrNull { it.shortName == "$firstNameChar.$lastName" }
|
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 {
|
fun getTeacherByLastFirst(nameLastFirst: String, loginId: String? = null): Teacher {
|
||||||
@ -453,9 +464,9 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
|
|||||||
val teacher = teacherList.singleOrNull { it.fullNameLastFirst == nameLastFirst }
|
val teacher = teacherList.singleOrNull { it.fullNameLastFirst == nameLastFirst }
|
||||||
val nameParts = nameLastFirst.split(" ", limit = 2)
|
val nameParts = nameLastFirst.split(" ", limit = 2)
|
||||||
return if (nameParts.size == 1)
|
return if (nameParts.size == 1)
|
||||||
validateTeacher(teacher, nameParts[0], "", loginId)
|
validateTeacher(teacher, nameParts[0], "", loginId, null)
|
||||||
else
|
else
|
||||||
validateTeacher(teacher, nameParts[1], nameParts[0], loginId)
|
validateTeacher(teacher, nameParts[1], nameParts[0], loginId, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTeacherByFirstLast(nameFirstLast: String, loginId: String? = null): Teacher {
|
fun getTeacherByFirstLast(nameFirstLast: String, loginId: String? = null): Teacher {
|
||||||
@ -463,9 +474,9 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
|
|||||||
val teacher = teacherList.singleOrNull { it.fullName == nameFirstLast }
|
val teacher = teacherList.singleOrNull { it.fullName == nameFirstLast }
|
||||||
val nameParts = nameFirstLast.split(" ", limit = 2)
|
val nameParts = nameFirstLast.split(" ", limit = 2)
|
||||||
return if (nameParts.size == 1)
|
return if (nameParts.size == 1)
|
||||||
validateTeacher(teacher, nameParts[0], "", loginId)
|
validateTeacher(teacher, nameParts[0], "", loginId, null)
|
||||||
else
|
else
|
||||||
validateTeacher(teacher, nameParts[0], nameParts[1], loginId)
|
validateTeacher(teacher, nameParts[0], nameParts[1], loginId, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTeacherByFDotLast(nameFDotLast: String, loginId: String? = null): Teacher {
|
fun getTeacherByFDotLast(nameFDotLast: String, loginId: String? = null): Teacher {
|
||||||
@ -484,10 +495,16 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
|
|||||||
getTeacher(nameParts[0][0], nameParts[1], loginId)
|
getTeacher(nameParts[0][0], nameParts[1], loginId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateTeacher(teacher: Teacher?, firstName: String, lastName: String, loginId: String?): Teacher {
|
private fun validateTeacher(
|
||||||
val obj = teacher ?: Teacher(profileId, -1, firstName, lastName, loginId).apply {
|
teacher: Teacher?,
|
||||||
id = fullName.crc32()
|
firstName: String,
|
||||||
teacherList[id] = this
|
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 {
|
return obj.also {
|
||||||
if (loginId != null)
|
if (loginId != null)
|
||||||
|
@ -451,9 +451,9 @@ class SzkolnyApi(val app: App) : CoroutineScope {
|
|||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun getRealms(registerName: String): List<LoginInfo.Platform> {
|
fun getRealms(registerName: String): List<LoginInfo.Platform> {
|
||||||
val response = api.fsLoginRealms(registerName).execute()
|
val response = api.platforms(registerName).execute()
|
||||||
if (response.isSuccessful && response.body() != null) {
|
if (response.isSuccessful && response.body() != null) {
|
||||||
return response.body()!!
|
return parseResponse(response)
|
||||||
}
|
}
|
||||||
throw SzkolnyApiException(null)
|
throw SzkolnyApiException(null)
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,6 @@ interface SzkolnyService {
|
|||||||
@GET("registerAvailability")
|
@GET("registerAvailability")
|
||||||
fun registerAvailability(): Call<ApiResponse<Map<String, RegisterAvailabilityStatus>>>
|
fun registerAvailability(): Call<ApiResponse<Map<String, RegisterAvailabilityStatus>>>
|
||||||
|
|
||||||
@GET("https://szkolny-eu.github.io/FSLogin/realms/{registerName}.json")
|
@GET("platforms/{registerName}")
|
||||||
fun fsLoginRealms(@Path("registerName") registerName: String): Call<List<LoginInfo.Platform>>
|
fun platforms(@Path("registerName") registerName: String): Call<ApiResponse<List<LoginInfo.Platform>>>
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ import pl.szczodrzynski.edziennik.data.db.migration.*
|
|||||||
TimetableManual::class,
|
TimetableManual::class,
|
||||||
Note::class,
|
Note::class,
|
||||||
Metadata::class
|
Metadata::class
|
||||||
], version = 97)
|
], version = 98)
|
||||||
@TypeConverters(
|
@TypeConverters(
|
||||||
ConverterTime::class,
|
ConverterTime::class,
|
||||||
ConverterDate::class,
|
ConverterDate::class,
|
||||||
@ -185,6 +185,7 @@ abstract class AppDb : RoomDatabase() {
|
|||||||
Migration95(),
|
Migration95(),
|
||||||
Migration96(),
|
Migration96(),
|
||||||
Migration97(),
|
Migration97(),
|
||||||
|
Migration98(),
|
||||||
).allowMainThreadQueries().build()
|
).allowMainThreadQueries().build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,6 +74,8 @@ open class Lesson(
|
|||||||
@Ignore
|
@Ignore
|
||||||
var showAsUnseen = false
|
var showAsUnseen = false
|
||||||
|
|
||||||
|
var color: Int? = null
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "Lesson(profileId=$profileId, " +
|
return "Lesson(profileId=$profileId, " +
|
||||||
"id=$id, " +
|
"id=$id, " +
|
||||||
|
@ -233,6 +233,10 @@ open class Profile(
|
|||||||
MainActivity.DRAWER_ITEM_GRADES,
|
MainActivity.DRAWER_ITEM_GRADES,
|
||||||
MainActivity.DRAWER_ITEM_HOMEWORK
|
MainActivity.DRAWER_ITEM_HOMEWORK
|
||||||
)
|
)
|
||||||
|
LOGIN_TYPE_USOS -> listOf(
|
||||||
|
MainActivity.DRAWER_ITEM_TIMETABLE,
|
||||||
|
MainActivity.DRAWER_ITEM_AGENDA
|
||||||
|
)
|
||||||
else -> listOf(
|
else -> listOf(
|
||||||
MainActivity.DRAWER_ITEM_TIMETABLE,
|
MainActivity.DRAWER_ITEM_TIMETABLE,
|
||||||
MainActivity.DRAWER_ITEM_AGENDA,
|
MainActivity.DRAWER_ITEM_AGENDA,
|
||||||
|
@ -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;")
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,15 @@ fun Bundle?.getFloat(key: String, defaultValue: Float): Float {
|
|||||||
fun Bundle?.getString(key: String, defaultValue: String): String {
|
fun Bundle?.getString(key: String, defaultValue: String): String {
|
||||||
return this?.getString(key, defaultValue) ?: defaultValue
|
return this?.getString(key, defaultValue) ?: defaultValue
|
||||||
}
|
}
|
||||||
|
inline fun <reified E : Enum<E>> Bundle?.getEnum(key: String): E? {
|
||||||
|
return this?.getString(key)?.let {
|
||||||
|
try {
|
||||||
|
enumValueOf<E>(it)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun Bundle?.getIntOrNull(key: String): Int? {
|
fun Bundle?.getIntOrNull(key: String): Int? {
|
||||||
return this?.get(key) as? Int
|
return this?.get(key) as? Int
|
||||||
@ -48,6 +57,7 @@ fun Bundle(vararg properties: Pair<String, Any?>): Bundle {
|
|||||||
is Bundle -> putBundle(property.first, property.second as Bundle)
|
is Bundle -> putBundle(property.first, property.second as Bundle)
|
||||||
is Parcelable -> putParcelable(property.first, property.second as Parcelable)
|
is Parcelable -> putParcelable(property.first, property.second as Parcelable)
|
||||||
is Array<*> -> putParcelableArray(property.first, property.second as Array<out Parcelable>)
|
is Array<*> -> putParcelableArray(property.first, property.second as Array<out Parcelable>)
|
||||||
|
is Enum<*> -> putString(property.first, (property.second as Enum<*>).name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,8 @@ import com.google.gson.JsonElement
|
|||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import com.google.gson.JsonPrimitive
|
import com.google.gson.JsonPrimitive
|
||||||
|
import kotlin.contracts.ExperimentalContracts
|
||||||
|
import kotlin.contracts.contract
|
||||||
|
|
||||||
fun JsonObject?.get(key: String): JsonElement? = this?.get(key)
|
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: JsonElement) = this.add(o)
|
||||||
operator fun JsonArray.plusAssign(o: String) = this.add(o)
|
operator fun JsonArray.plusAssign(o: String) = this.add(o)
|
||||||
operator fun JsonArray.plusAssign(o: Char) = this.add(o)
|
operator fun JsonArray.plusAssign(o: Char) = this.add(o)
|
||||||
|
@ -18,6 +18,8 @@ import android.text.style.StyleSpan
|
|||||||
import androidx.annotation.PluralsRes
|
import androidx.annotation.PluralsRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import com.mikepenz.materialdrawer.holder.StringHolder
|
import com.mikepenz.materialdrawer.holder.StringHolder
|
||||||
|
import java.net.URLDecoder
|
||||||
|
import java.net.URLEncoder
|
||||||
|
|
||||||
fun CharSequence?.isNotNullNorEmpty(): Boolean {
|
fun CharSequence?.isNotNullNorEmpty(): Boolean {
|
||||||
return this != null && this.isNotEmpty()
|
return this != null && this.isNotEmpty()
|
||||||
@ -343,3 +345,17 @@ fun Int.toStringHolder() = StringHolder(this)
|
|||||||
fun CharSequence.toStringHolder() = StringHolder(this)
|
fun CharSequence.toStringHolder() = StringHolder(this)
|
||||||
|
|
||||||
fun @receiver:StringRes Int.resolveString(context: Context) = context.getString(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<String, String>.toQueryString() = this
|
||||||
|
.map { it.key.urlEncode() to it.value.urlEncode() }
|
||||||
|
.sortedBy { it.first }
|
||||||
|
.joinToString("&") { "${it.first}=${it.second}" }
|
||||||
|
|
||||||
|
fun String.fromQueryString() = this
|
||||||
|
.substringAfter('?')
|
||||||
|
.split("&")
|
||||||
|
.map { it.split("=") }
|
||||||
|
.associate { it[0].urlDecode() to it[1].urlDecode() }
|
||||||
|
@ -7,9 +7,10 @@ package pl.szczodrzynski.edziennik.ext
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import im.wangchao.mhttp.Response
|
import im.wangchao.mhttp.Response
|
||||||
import pl.szczodrzynski.edziennik.R
|
import pl.szczodrzynski.edziennik.R
|
||||||
|
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||||
import pl.szczodrzynski.edziennik.utils.models.Time
|
import pl.szczodrzynski.edziennik.utils.models.Time
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
|
||||||
const val MINUTE = 60L
|
const val MINUTE = 60L
|
||||||
const val HOUR = 60L*MINUTE
|
const val HOUR = 60L*MINUTE
|
||||||
@ -115,3 +116,11 @@ fun Context.getSyncInterval(interval: Int): String {
|
|||||||
""
|
""
|
||||||
return hoursText?.plus(" $minutesText") ?: minutesText
|
return hoursText?.plus(" $minutesText") ?: minutesText
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ClosedRange<Date>.asSequence(): Sequence<Date> = sequence {
|
||||||
|
val date = this@asSequence.start.clone()
|
||||||
|
while (date in this@asSequence) {
|
||||||
|
yield(date.clone())
|
||||||
|
date.stepForward(0, 0, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -13,14 +13,16 @@ import pl.szczodrzynski.edziennik.databinding.RecaptchaViewBinding
|
|||||||
import pl.szczodrzynski.edziennik.ext.onClick
|
import pl.szczodrzynski.edziennik.ext.onClick
|
||||||
import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog
|
import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog
|
||||||
|
|
||||||
class LibrusCaptchaDialog(
|
class RecaptchaPromptDialog(
|
||||||
activity: AppCompatActivity,
|
activity: AppCompatActivity,
|
||||||
|
private val siteKey: String,
|
||||||
|
private val referer: String,
|
||||||
private val onSuccess: (recaptchaCode: String) -> Unit,
|
private val onSuccess: (recaptchaCode: String) -> Unit,
|
||||||
private val onFailure: (() -> Unit)?,
|
private val onCancel: (() -> Unit)?,
|
||||||
onShowListener: ((tag: String) -> Unit)? = null,
|
onShowListener: ((tag: String) -> Unit)? = null,
|
||||||
onDismissListener: ((tag: String) -> Unit)? = null,
|
onDismissListener: ((tag: String) -> Unit)? = null,
|
||||||
) : BindingDialog<RecaptchaViewBinding>(activity, onShowListener, onDismissListener) {
|
) : BindingDialog<RecaptchaViewBinding>(activity, onShowListener, onDismissListener) {
|
||||||
override val TAG = "LibrusCaptchaDialog"
|
override val TAG = "RecaptchaPromptDialog"
|
||||||
|
|
||||||
override fun getTitleRes(): Int? = null
|
override fun getTitleRes(): Int? = null
|
||||||
override fun inflate(layoutInflater: LayoutInflater) =
|
override fun inflate(layoutInflater: LayoutInflater) =
|
||||||
@ -46,8 +48,8 @@ class LibrusCaptchaDialog(
|
|||||||
b.progress.visibility = View.VISIBLE
|
b.progress.visibility = View.VISIBLE
|
||||||
RecaptchaDialog(
|
RecaptchaDialog(
|
||||||
activity,
|
activity,
|
||||||
siteKey = "6Lf48moUAAAAAB9ClhdvHr46gRWR-CN31CXQPG2U",
|
siteKey = siteKey,
|
||||||
referer = "https://portal.librus.pl/rodzina/login",
|
referer = referer,
|
||||||
onSuccess = { recaptchaCode ->
|
onSuccess = { recaptchaCode ->
|
||||||
b.checkbox.background = checkboxBackground
|
b.checkbox.background = checkboxBackground
|
||||||
b.checkbox.foreground = checkboxForeground
|
b.checkbox.foreground = checkboxForeground
|
||||||
@ -67,6 +69,6 @@ class LibrusCaptchaDialog(
|
|||||||
|
|
||||||
override fun onDismiss() {
|
override fun onDismiss() {
|
||||||
if (!success)
|
if (!success)
|
||||||
onFailure?.invoke()
|
onCancel?.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -27,7 +27,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Profile
|
|||||||
import pl.szczodrzynski.edziennik.databinding.CardHomeDebugBinding
|
import pl.szczodrzynski.edziennik.databinding.CardHomeDebugBinding
|
||||||
import pl.szczodrzynski.edziennik.ext.dp
|
import pl.szczodrzynski.edziennik.ext.dp
|
||||||
import pl.szczodrzynski.edziennik.ext.onClick
|
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.HomeCard
|
||||||
import pl.szczodrzynski.edziennik.ui.home.HomeCardAdapter
|
import pl.szczodrzynski.edziennik.ui.home.HomeCardAdapter
|
||||||
import pl.szczodrzynski.edziennik.ui.home.HomeFragment
|
import pl.szczodrzynski.edziennik.ui.home.HomeFragment
|
||||||
@ -85,11 +85,6 @@ class HomeDebugCard(
|
|||||||
app.startActivity(Chucker.getLaunchIntent(activity, 1));
|
app.startActivity(Chucker.getLaunchIntent(activity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
b.librusCaptchaButton.onClick {
|
|
||||||
//app.startActivity(Intent(activity, LoginLibrusCaptchaActivity::class.java))
|
|
||||||
LibrusCaptchaDialog(activity, onSuccess = {}, onFailure = {}).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
b.getLogs.onClick {
|
b.getLogs.onClick {
|
||||||
val logs = HyperLog.getDeviceLogsInFile(activity, true)
|
val logs = HyperLog.getDeviceLogsInFile(activity, true)
|
||||||
val intent = Intent(Intent.ACTION_SEND)
|
val intent = Intent(Intent.ACTION_SEND)
|
||||||
|
@ -31,6 +31,7 @@ class LoginActivity : AppCompatActivity(), CoroutineScope {
|
|||||||
private val app: App by lazy { applicationContext as App }
|
private val app: App by lazy { applicationContext as App }
|
||||||
private lateinit var b: LoginActivityBinding
|
private lateinit var b: LoginActivityBinding
|
||||||
lateinit var navOptions: NavOptions
|
lateinit var navOptions: NavOptions
|
||||||
|
lateinit var navOptionsBuilder: NavOptions.Builder
|
||||||
val nav by lazy { Navigation.findNavController(this, R.id.nav_host_fragment) }
|
val nav by lazy { Navigation.findNavController(this, R.id.nav_host_fragment) }
|
||||||
val errorSnackbar: ErrorSnackbar by lazy { ErrorSnackbar(this) }
|
val errorSnackbar: ErrorSnackbar by lazy { ErrorSnackbar(this) }
|
||||||
val swipeRefreshLayout: SwipeRefreshLayoutNoTouch by lazy { b.swipeRefreshLayout }
|
val swipeRefreshLayout: SwipeRefreshLayoutNoTouch by lazy { b.swipeRefreshLayout }
|
||||||
@ -87,12 +88,12 @@ class LoginActivity : AppCompatActivity(), CoroutineScope {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setTheme(R.style.AppTheme_Light)
|
setTheme(R.style.AppTheme_Light)
|
||||||
|
|
||||||
navOptions = NavOptions.Builder()
|
navOptionsBuilder = NavOptions.Builder()
|
||||||
.setEnterAnim(R.anim.slide_in_right)
|
.setEnterAnim(R.anim.slide_in_right)
|
||||||
.setExitAnim(R.anim.slide_out_left)
|
.setExitAnim(R.anim.slide_out_left)
|
||||||
.setPopEnterAnim(R.anim.slide_in_left)
|
.setPopEnterAnim(R.anim.slide_in_left)
|
||||||
.setPopExitAnim(R.anim.slide_out_right)
|
.setPopExitAnim(R.anim.slide_out_right)
|
||||||
.build()
|
navOptions = navOptionsBuilder.build()
|
||||||
|
|
||||||
b = LoginActivityBinding.inflate(layoutInflater)
|
b = LoginActivityBinding.inflate(layoutInflater)
|
||||||
setContentView(b.root)
|
setContentView(b.root)
|
||||||
|
@ -14,6 +14,8 @@ import android.widget.Toast
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.widget.addTextChangedListener
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.navigation.NavOptions
|
||||||
|
import androidx.navigation.navOptions
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.mikepenz.iconics.IconicsDrawable
|
import com.mikepenz.iconics.IconicsDrawable
|
||||||
@ -33,7 +35,6 @@ import pl.szczodrzynski.edziennik.ui.login.LoginInfo.BaseCredential
|
|||||||
import pl.szczodrzynski.edziennik.ui.login.LoginInfo.FormCheckbox
|
import pl.szczodrzynski.edziennik.ui.login.LoginInfo.FormCheckbox
|
||||||
import pl.szczodrzynski.edziennik.ui.login.LoginInfo.FormField
|
import pl.szczodrzynski.edziennik.ui.login.LoginInfo.FormField
|
||||||
import pl.szczodrzynski.navlib.colorAttr
|
import pl.szczodrzynski.navlib.colorAttr
|
||||||
import java.util.*
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
class LoginFormFragment : Fragment(), CoroutineScope {
|
class LoginFormFragment : Fragment(), CoroutineScope {
|
||||||
@ -63,8 +64,10 @@ class LoginFormFragment : Fragment(), CoroutineScope {
|
|||||||
get() = arguments?.getString("platformDescription")
|
get() = arguments?.getString("platformDescription")
|
||||||
private val platformFormFields
|
private val platformFormFields
|
||||||
get() = arguments?.getString("platformFormFields")?.split(";")
|
get() = arguments?.getString("platformFormFields")?.split(";")
|
||||||
private val platformRealmData
|
private val platformData
|
||||||
get() = arguments?.getString("platformRealmData")?.toJsonObject()
|
get() = arguments?.getString("platformData")?.toJsonObject()
|
||||||
|
private val platformStoreKey
|
||||||
|
get() = arguments?.getString("platformStoreKey")
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
@ -90,6 +93,11 @@ class LoginFormFragment : Fragment(), CoroutineScope {
|
|||||||
val loginMode = arguments?.getInt("loginMode") ?: return
|
val loginMode = arguments?.getInt("loginMode") ?: return
|
||||||
val mode = register.loginModes.firstOrNull { it.loginMode == 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.title.setText(R.string.login_form_title_format, app.getString(register.registerName))
|
||||||
b.subTitle.text = platformName ?: app.getString(mode.name)
|
b.subTitle.text = platformName ?: app.getString(mode.name)
|
||||||
b.text.text = platformGuideText ?: app.getString(mode.guideText)
|
b.text.text = platformGuideText ?: app.getString(mode.guideText)
|
||||||
@ -250,7 +258,10 @@ class LoginFormFragment : Fragment(), CoroutineScope {
|
|||||||
payload.putBoolean("fakeLogin", true)
|
payload.putBoolean("fakeLogin", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
payload.putBundle("webRealmData", platformRealmData?.toBundle())
|
if (platformStoreKey == null)
|
||||||
|
payload.putAll(platformData?.toBundle() ?: Bundle())
|
||||||
|
else
|
||||||
|
payload.putBundle(platformStoreKey, platformData?.toBundle())
|
||||||
|
|
||||||
var hasErrors = false
|
var hasErrors = false
|
||||||
credentials.forEach { (credential, b) ->
|
credentials.forEach { (credential, b) ->
|
||||||
@ -295,6 +306,14 @@ class LoginFormFragment : Fragment(), CoroutineScope {
|
|||||||
if (hasErrors)
|
if (hasErrors)
|
||||||
return
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ package pl.szczodrzynski.edziennik.ui.login
|
|||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import com.google.gson.JsonObject
|
||||||
import com.mikepenz.iconics.typeface.IIcon
|
import com.mikepenz.iconics.typeface.IIcon
|
||||||
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||||
import pl.szczodrzynski.edziennik.R
|
import pl.szczodrzynski.edziennik.R
|
||||||
@ -64,7 +65,6 @@ object LoginInfo {
|
|||||||
errorCodes = mapOf(
|
errorCodes = mapOf(
|
||||||
ERROR_LOGIN_LIBRUS_PORTAL_NOT_ACTIVATED to R.string.login_error_account_not_activated,
|
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_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(
|
/*Mode(
|
||||||
@ -313,7 +313,24 @@ object LoginInfo {
|
|||||||
errorCodes = mapOf()
|
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_mode_usos_api,
|
||||||
|
guideText = R.string.login_mode_usos_oauth_guide,
|
||||||
|
isPlatformSelection = true,
|
||||||
|
credentials = listOf(),
|
||||||
|
errorCodes = mapOf(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -357,7 +374,8 @@ object LoginInfo {
|
|||||||
val icon: String,
|
val icon: String,
|
||||||
val screenshot: String?,
|
val screenshot: String?,
|
||||||
val formFields: List<String>,
|
val formFields: List<String>,
|
||||||
val realmData: RealmData
|
val data: JsonObject,
|
||||||
|
val storeKey: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
open class BaseCredential(
|
open class BaseCredential(
|
||||||
|
@ -68,7 +68,8 @@ class LoginPlatformListFragment : Fragment(), CoroutineScope {
|
|||||||
"platformName" to platform.name,
|
"platformName" to platform.name,
|
||||||
"platformDescription" to platform.description,
|
"platformDescription" to platform.description,
|
||||||
"platformFormFields" to platform.formFields.joinToString(";"),
|
"platformFormFields" to platform.formFields.joinToString(";"),
|
||||||
"platformRealmData" to app.gson.toJson(platform.realmData)
|
"platformData" to platform.data.toString(),
|
||||||
|
"platformStoreKey" to platform.storeKey,
|
||||||
), activity.navOptions)
|
), activity.navOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ import org.greenrobot.eventbus.Subscribe
|
|||||||
import org.greenrobot.eventbus.ThreadMode
|
import org.greenrobot.eventbus.ThreadMode
|
||||||
import pl.szczodrzynski.edziennik.App
|
import pl.szczodrzynski.edziennik.App
|
||||||
import pl.szczodrzynski.edziennik.R
|
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.LOGIN_NO_ARGUMENTS
|
||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
|
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
|
||||||
import pl.szczodrzynski.edziennik.data.api.events.ApiTaskErrorEvent
|
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.data.db.entity.LoginStore
|
||||||
import pl.szczodrzynski.edziennik.databinding.LoginProgressFragmentBinding
|
import pl.szczodrzynski.edziennik.databinding.LoginProgressFragmentBinding
|
||||||
import pl.szczodrzynski.edziennik.ext.joinNotNullStrings
|
import pl.szczodrzynski.edziennik.ext.joinNotNullStrings
|
||||||
|
import pl.szczodrzynski.edziennik.utils.managers.UserActionManager
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
@ -137,14 +138,21 @@ class LoginProgressFragment : Fragment(), CoroutineScope {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
app.userActionManager.execute(activity, event.profileId, event.type, onSuccess = { code ->
|
val callback = UserActionManager.UserActionCallback(
|
||||||
args.putString("recaptchaCode", code)
|
onSuccess = { data ->
|
||||||
args.putLong("recaptchaTime", System.currentTimeMillis())
|
args.putAll(data)
|
||||||
doFirstLogin(args)
|
doFirstLogin(args)
|
||||||
}, onFailure = {
|
},
|
||||||
activity.error(ApiError(TAG, ERROR_CAPTCHA_NEEDED))
|
onFailure = {
|
||||||
|
activity.error(ApiError(TAG, ERROR_REQUIRES_USER_ACTION))
|
||||||
nav.navigateUp()
|
nav.navigateUp()
|
||||||
})
|
},
|
||||||
|
onCancel = {
|
||||||
|
nav.navigateUp()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
app.userActionManager.execute(activity, event, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
|
@ -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,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
@ -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?,
|
||||||
|
)
|
@ -21,8 +21,11 @@ import com.mikepenz.iconics.IconicsDrawable
|
|||||||
import com.mikepenz.iconics.utils.colorInt
|
import com.mikepenz.iconics.utils.colorInt
|
||||||
import com.mikepenz.iconics.utils.sizeDp
|
import com.mikepenz.iconics.utils.sizeDp
|
||||||
import kotlinx.coroutines.*
|
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.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.api.edziennik.EdziennikTask
|
||||||
import pl.szczodrzynski.edziennik.data.db.entity.Lesson
|
import pl.szczodrzynski.edziennik.data.db.entity.Lesson
|
||||||
import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull
|
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.Date
|
||||||
import pl.szczodrzynski.edziennik.utils.models.Time
|
import pl.szczodrzynski.edziennik.utils.models.Time
|
||||||
import pl.szczodrzynski.edziennik.utils.mutableLazy
|
import pl.szczodrzynski.edziennik.utils.mutableLazy
|
||||||
import java.util.*
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
class TimetableDayFragment : LazyFragment(), CoroutineScope {
|
class TimetableDayFragment : LazyFragment(), CoroutineScope {
|
||||||
@ -82,7 +85,7 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
|
|||||||
startHour = startHour,
|
startHour = startHour,
|
||||||
endHour = endHour,
|
endHour = endHour,
|
||||||
dividerHeight = 1.dp,
|
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),
|
hourDividerColor = R.attr.hourDividerColor.resolveAttr(context),
|
||||||
halfHourDividerColor = R.attr.halfHourDividerColor.resolveAttr(context),
|
halfHourDividerColor = R.attr.halfHourDividerColor.resolveAttr(context),
|
||||||
hourLabelWidth = 40.dp,
|
hourLabelWidth = 40.dp,
|
||||||
@ -184,11 +187,18 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
|
|||||||
|
|
||||||
val lessonsActual = lessons.filter { it.type != Lesson.TYPE_NO_LESSONS }
|
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) {
|
if (profileConfig.timetableTrimHourRange) {
|
||||||
dayViewDelegate.deinitialize()
|
dayViewDelegate.deinitialize()
|
||||||
// end/start defaults are swapped on purpose
|
// end/start defaults are swapped on purpose
|
||||||
startHour = lessonsActual.minOf { it.displayStartTime?.hour ?: DEFAULT_END_HOUR }
|
startHour = minStartHour
|
||||||
endHour = lessonsActual.maxOf { it.displayEndTime?.hour?.plus(1) ?: DEFAULT_START_HOUR }
|
endHour = maxEndHour
|
||||||
|
} else if (startHour > minStartHour || endHour < maxEndHour) {
|
||||||
|
dayViewDelegate.deinitialize()
|
||||||
|
startHour = min(startHour, minStartHour)
|
||||||
|
endHour = max(endHour, maxEndHour)
|
||||||
}
|
}
|
||||||
|
|
||||||
b.scrollView.isVisible = true
|
b.scrollView.isVisible = true
|
||||||
@ -318,7 +328,7 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
|
|||||||
lesson.getNoteSubstituteText(showNotes = true) ?: lesson.displaySubjectName
|
lesson.getNoteSubstituteText(showNotes = true) ?: lesson.displaySubjectName
|
||||||
|
|
||||||
val (subjectTextPrimary, subjectTextSecondary) = if (profileConfig.timetableColorSubjectName) {
|
val (subjectTextPrimary, subjectTextSecondary) = if (profileConfig.timetableColorSubjectName) {
|
||||||
val subjectColor = Colors.stringToMaterialColorCRC(lessonText?.toString() ?: "")
|
val subjectColor = lesson.color ?: Colors.stringToMaterialColorCRC(lessonText?.toString() ?: "")
|
||||||
if (lb.annotationVisible) {
|
if (lb.annotationVisible) {
|
||||||
lb.subjectContainer.background = ColorDrawable(subjectColor)
|
lb.subjectContainer.background = ColorDrawable(subjectColor)
|
||||||
} else {
|
} else {
|
||||||
@ -377,9 +387,8 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
|
|||||||
|
|
||||||
// The day view needs the event time ranges in the start minute/end minute format,
|
// The day view needs the event time ranges in the start minute/end minute format,
|
||||||
// so calculate those here
|
// so calculate those here
|
||||||
val startMinute = 60 * (lesson.displayStartTime?.hour
|
val startMinute = 60 * startTime.hour + startTime.minute
|
||||||
?: 0) + (lesson.displayStartTime?.minute ?: 0)
|
val endMinute = 60 * endTime.hour + endTime.minute
|
||||||
val endMinute = startMinute + 45
|
|
||||||
eventTimeRanges.add(DayView.EventTimeRange(startMinute, endMinute))
|
eventTimeRanges.add(DayView.EventTimeRange(startMinute, endMinute))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,58 +7,53 @@ package pl.szczodrzynski.edziennik.utils.managers
|
|||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import org.greenrobot.eventbus.Subscribe
|
||||||
|
import org.greenrobot.eventbus.ThreadMode
|
||||||
import pl.szczodrzynski.edziennik.App
|
import pl.szczodrzynski.edziennik.App
|
||||||
import pl.szczodrzynski.edziennik.MainActivity
|
import pl.szczodrzynski.edziennik.MainActivity
|
||||||
import pl.szczodrzynski.edziennik.R
|
import pl.szczodrzynski.edziennik.R
|
||||||
import pl.szczodrzynski.edziennik.data.api.ERROR_CAPTCHA_LIBRUS_PORTAL
|
|
||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
|
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
|
||||||
import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
|
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.ext.Intent
|
import pl.szczodrzynski.edziennik.ui.captcha.RecaptchaPromptDialog
|
||||||
import pl.szczodrzynski.edziennik.ext.JsonObject
|
import pl.szczodrzynski.edziennik.ui.login.oauth.OAuthLoginActivity
|
||||||
import pl.szczodrzynski.edziennik.ext.pendingIntentFlag
|
import pl.szczodrzynski.edziennik.ui.login.oauth.OAuthLoginResult
|
||||||
import pl.szczodrzynski.edziennik.ui.captcha.LibrusCaptchaDialog
|
import pl.szczodrzynski.edziennik.utils.Utils.d
|
||||||
|
|
||||||
class UserActionManager(val app: App) {
|
class UserActionManager(val app: App) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "UserActionManager"
|
private const val TAG = "UserActionManager"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requiresUserAction(apiError: ApiError): Boolean {
|
fun sendToUser(event: UserActionRequiredEvent) {
|
||||||
return apiError.errorCode == ERROR_CAPTCHA_LIBRUS_PORTAL
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendToUser(apiError: ApiError) {
|
|
||||||
val type = when (apiError.errorCode) {
|
|
||||||
ERROR_CAPTCHA_LIBRUS_PORTAL -> UserActionRequiredEvent.CAPTCHA_LIBRUS
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if (EventBus.getDefault().hasSubscriberForEvent(UserActionRequiredEvent::class.java)) {
|
if (EventBus.getDefault().hasSubscriberForEvent(UserActionRequiredEvent::class.java)) {
|
||||||
EventBus.getDefault().post(UserActionRequiredEvent(apiError.profileId ?: -1, type))
|
EventBus.getDefault().post(event)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val manager = app.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
val manager = app.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
val text = app.getString(event.errorText, event.profileId)
|
||||||
val text = app.getString(when (type) {
|
|
||||||
UserActionRequiredEvent.CAPTCHA_LIBRUS -> R.string.notification_user_action_required_captcha_librus
|
|
||||||
else -> R.string.notification_user_action_required_text
|
|
||||||
}, apiError.profileId)
|
|
||||||
|
|
||||||
val intent = Intent(
|
val intent = Intent(
|
||||||
app,
|
app,
|
||||||
MainActivity::class.java,
|
MainActivity::class.java,
|
||||||
"action" to "userActionRequired",
|
"action" to "userActionRequired",
|
||||||
"profileId" to (apiError.profileId ?: -1),
|
"profileId" to event.profileId,
|
||||||
"type" to type
|
"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))
|
.setContentTitle(app.getString(R.string.notification_user_action_required_title))
|
||||||
.setContentText(text)
|
.setContentText(text)
|
||||||
.setSmallIcon(R.drawable.ic_error_outline)
|
.setSmallIcon(R.drawable.ic_error_outline)
|
||||||
@ -74,29 +69,98 @@ class UserActionManager(val app: App) {
|
|||||||
manager.notify(System.currentTimeMillis().toInt(), notification)
|
manager.notify(System.currentTimeMillis().toInt(), notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class UserActionCallback(
|
||||||
|
val onSuccess: ((data: Bundle) -> Unit)? = null,
|
||||||
|
val onFailure: (() -> Unit)? = null,
|
||||||
|
val onCancel: (() -> Unit)? = null,
|
||||||
|
)
|
||||||
|
|
||||||
fun execute(
|
fun execute(
|
||||||
activity: AppCompatActivity,
|
activity: AppCompatActivity,
|
||||||
profileId: Int?,
|
event: UserActionRequiredEvent,
|
||||||
type: Int,
|
callback: UserActionCallback,
|
||||||
onSuccess: ((code: String) -> Unit)? = null,
|
|
||||||
onFailure: (() -> Unit)? = null
|
|
||||||
) {
|
) {
|
||||||
if (type != UserActionRequiredEvent.CAPTCHA_LIBRUS)
|
d(TAG, "Running user action (${event.type}) with params: ${event.params}")
|
||||||
return
|
val isSuccessful = when (event.type) {
|
||||||
|
UserActionRequiredEvent.Type.RECAPTCHA -> executeRecaptcha(activity, event, callback)
|
||||||
|
UserActionRequiredEvent.Type.OAUTH -> executeOauth(activity, event, callback)
|
||||||
|
}
|
||||||
|
if (!isSuccessful)
|
||||||
|
callback.onFailure?.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
if (profileId == null)
|
private fun executeRecaptcha(
|
||||||
return
|
activity: AppCompatActivity,
|
||||||
// show captcha dialog
|
event: UserActionRequiredEvent,
|
||||||
// use passed onSuccess listener, else sync profile
|
callback: UserActionCallback,
|
||||||
LibrusCaptchaDialog(
|
): Boolean {
|
||||||
|
val siteKey = event.params.getString("siteKey") ?: return false
|
||||||
|
val referer = event.params.getString("referer") ?: return false
|
||||||
|
RecaptchaPromptDialog(
|
||||||
activity = activity,
|
activity = activity,
|
||||||
onSuccess = onSuccess ?: { code ->
|
siteKey = siteKey,
|
||||||
EdziennikTask.syncProfile(profileId, arguments = JsonObject(
|
referer = referer,
|
||||||
|
onSuccess = { code ->
|
||||||
|
finishAction(activity, event, callback, Bundle(
|
||||||
"recaptchaCode" to code,
|
"recaptchaCode" to code,
|
||||||
"recaptchaTime" to System.currentTimeMillis()
|
"recaptchaTime" to System.currentTimeMillis(),
|
||||||
)).enqueue(activity)
|
))
|
||||||
},
|
},
|
||||||
onFailure = onFailure
|
onCancel = callback.onCancel,
|
||||||
).show()
|
).show()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun executeOauth(
|
||||||
|
activity: AppCompatActivity,
|
||||||
|
event: UserActionRequiredEvent,
|
||||||
|
callback: UserActionCallback,
|
||||||
|
): Boolean {
|
||||||
|
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 {
|
||||||
|
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||||
|
fun onOAuthLoginResult(result: OAuthLoginResult) {
|
||||||
|
EventBus.getDefault().unregister(listener)
|
||||||
|
when {
|
||||||
|
result.isError -> callback.onFailure?.invoke()
|
||||||
|
result.responseUrl != null -> {
|
||||||
|
finishAction(activity, event, callback, Bundle(
|
||||||
|
storeKey to result.responseUrl,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
else -> callback.onCancel?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EventBus.getDefault().register(listener)
|
||||||
|
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
BIN
app/src/main/res/drawable/login_logo_usos.png
Normal file
BIN
app/src/main/res/drawable/login_logo_usos.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
app/src/main/res/drawable/login_mode_usos_api.png
Normal file
BIN
app/src/main/res/drawable/login_mode_usos_api.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
@ -25,13 +25,6 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Save Debug Logs" />
|
android:text="Save Debug Logs" />
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/librusCaptchaButton"
|
|
||||||
style="@style/Widget.MaterialComponents.Button"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="LIBRUS® Captcha" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/refreshWidget"
|
android:id="@+id/refreshWidget"
|
||||||
style="@style/Widget.MaterialComponents.Button"
|
style="@style/Widget.MaterialComponents.Button"
|
||||||
|
@ -30,11 +30,7 @@
|
|||||||
<string name="error_111" translatable="false">ERROR_LOGIN_METHOD_NOT_SATISFIED</string>
|
<string name="error_111" translatable="false">ERROR_LOGIN_METHOD_NOT_SATISFIED</string>
|
||||||
<string name="error_112" translatable="false">ERROR_NOT_IMPLEMENTED</string>
|
<string name="error_112" translatable="false">ERROR_NOT_IMPLEMENTED</string>
|
||||||
<string name="error_113" translatable="false">ERROR_FILE_DOWNLOAD</string>
|
<string name="error_113" translatable="false">ERROR_FILE_DOWNLOAD</string>
|
||||||
|
<string name="error_114" translatable="false">ERROR_REQUIRES_USER_ACTION</string>
|
||||||
<string name="error_115" translatable="false">ERROR_NO_STUDENTS_IN_ACCOUNT</string>
|
|
||||||
|
|
||||||
<string name="error_3000" translatable="false">ERROR_CAPTCHA_NEEDED</string>
|
|
||||||
<string name="error_3001" translatable="false">ERROR_CAPTCHA_LIBRUS_PORTAL</string>
|
|
||||||
|
|
||||||
<string name="error_5000" translatable="false">ERROR_API_PDO_ERROR</string>
|
<string name="error_5000" translatable="false">ERROR_API_PDO_ERROR</string>
|
||||||
<string name="error_5001" translatable="false">ERROR_API_INVALID_CLIENT</string>
|
<string name="error_5001" translatable="false">ERROR_API_INVALID_CLIENT</string>
|
||||||
@ -174,6 +170,12 @@
|
|||||||
<string name="error_631" translatable="false">ERROR_PODLASIE_API_OTHER</string>
|
<string name="error_631" translatable="false">ERROR_PODLASIE_API_OTHER</string>
|
||||||
<string name="error_632" translatable="false">ERROR_PODLASIE_API_DATA_MISSING</string>
|
<string name="error_632" translatable="false">ERROR_PODLASIE_API_DATA_MISSING</string>
|
||||||
|
|
||||||
|
<string name="error_702" translatable="false">ERROR_USOS_OAUTH_GOT_DIFFERENT_TOKEN</string>
|
||||||
|
<string name="error_703" translatable="false">ERROR_USOS_OAUTH_INCOMPLETE_RESPONSE</string>
|
||||||
|
<string name="error_704" translatable="false">ERROR_USOS_NO_STUDENT_PROGRAMMES</string>
|
||||||
|
<string name="error_705" translatable="false">ERROR_USOS_API_INCOMPLETE_RESPONSE</string>
|
||||||
|
<string name="error_706" translatable="false">ERROR_USOS_API_MISSING_RESPONSE</string>
|
||||||
|
|
||||||
<string name="error_801" translatable="false">ERROR_TEMPLATE_WEB_OTHER</string>
|
<string name="error_801" translatable="false">ERROR_TEMPLATE_WEB_OTHER</string>
|
||||||
|
|
||||||
<string name="error_900" translatable="false">EXCEPTION_API_TASK</string>
|
<string name="error_900" translatable="false">EXCEPTION_API_TASK</string>
|
||||||
@ -220,11 +222,7 @@
|
|||||||
<string name="error_111_reason">Nie można wywołać metody logowania. Skontaktuj się z twórcą aplikacji.</string>
|
<string name="error_111_reason">Nie można wywołać metody logowania. Skontaktuj się z twórcą aplikacji.</string>
|
||||||
<string name="error_112_reason">Nie zaimplementowano</string>
|
<string name="error_112_reason">Nie zaimplementowano</string>
|
||||||
<string name="error_113_reason">Wystąpił błąd podczas pobierania pliku. Dziennik może być przeciążony lub mieć przerwę techniczną.</string>
|
<string name="error_113_reason">Wystąpił błąd podczas pobierania pliku. Dziennik może być przeciążony lub mieć przerwę techniczną.</string>
|
||||||
|
<string name="error_114_reason">Wymagana akcja w aplikacji</string>
|
||||||
<string name="error_115_reason">Brak uczniów przypisanych do konta</string>
|
|
||||||
|
|
||||||
<string name="error_3000_reason">Wymagane rozwiązanie zadania Captcha</string>
|
|
||||||
<string name="error_3001_reason">LIBRUS®️: wymagane rozwiązanie zadania Captcha</string>
|
|
||||||
|
|
||||||
<string name="error_5000_reason">ERROR_API_PDO_ERROR</string>
|
<string name="error_5000_reason">ERROR_API_PDO_ERROR</string>
|
||||||
<string name="error_5001_reason">Nieprawidłowy ID klienta API</string>
|
<string name="error_5001_reason">Nieprawidłowy ID klienta API</string>
|
||||||
@ -364,6 +362,12 @@
|
|||||||
<string name="error_631_reason">ERROR_PODLASIE_API_OTHER</string>
|
<string name="error_631_reason">ERROR_PODLASIE_API_OTHER</string>
|
||||||
<string name="error_632_reason">Brak danych. Zgłoś błąd programiście.</string>
|
<string name="error_632_reason">Brak danych. Zgłoś błąd programiście.</string>
|
||||||
|
|
||||||
|
<string name="error_702_reason">Błąd logowania: otrzymano nieprawidłowy token</string>
|
||||||
|
<string name="error_703_reason">Błąd logowania: niekompletna odpowiedź serwera</string>
|
||||||
|
<string name="error_704_reason">Student nie jest zapisany na żaden kierunek</string>
|
||||||
|
<string name="error_705_reason">Brakujące dane w odpowiedzi serwera</string>
|
||||||
|
<string name="error_706_reason">Brakująca odpowiedź serwera</string>
|
||||||
|
|
||||||
<string name="error_801_reason">ERROR_TEMPLATE_WEB_OTHER</string>
|
<string name="error_801_reason">ERROR_TEMPLATE_WEB_OTHER</string>
|
||||||
|
|
||||||
<string name="error_900_reason">Błąd synchronizacji. Upewnij się, że masz połączenie z internetem, a następnie zgłoś błąd.</string>
|
<string name="error_900_reason">Błąd synchronizacji. Upewnij się, że masz połączenie z internetem, a następnie zgłoś błąd.</string>
|
||||||
|
@ -1542,4 +1542,10 @@
|
|||||||
<string name="timetable_config_show_attendance">Pokazuj rodzaj obecności na lekcji</string>
|
<string name="timetable_config_show_attendance">Pokazuj rodzaj obecności na lekcji</string>
|
||||||
<string name="timetable_config_color_subject_name">Koloruj nazwę przedmiotu</string>
|
<string name="timetable_config_color_subject_name">Koloruj nazwę przedmiotu</string>
|
||||||
<string name="timetable_config_trim_hour_range">Nie pokazuj godzin bez lekcji</string>
|
<string name="timetable_config_trim_hour_range">Nie pokazuj godzin bez lekcji</string>
|
||||||
|
<string name="edziennik_progress_login_usos_api">Logowanie do USOS API...</string>
|
||||||
|
<string name="login_type_usos">USOS</string>
|
||||||
|
<string name="login_mode_usos_oauth">Logowanie z użyciem przeglądarki</string>
|
||||||
|
<string name="login_mode_usos_oauth_guide">TODO</string>
|
||||||
|
<string name="notification_user_action_required_oauth_usos">USOS - wymagane logowanie z użyciem przeglądarki</string>
|
||||||
|
<string name="oauth_dialog_title">Zaloguj się</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user