[API/Podlasie] Implement first login and login page.

This commit is contained in:
Kacper Ziubryniewicz 2020-05-12 20:25:45 +02:00
parent 7f4e45c57c
commit 0a2323acf3
18 changed files with 408 additions and 16 deletions

View File

@ -1245,3 +1245,5 @@ val SwipeRefreshLayout.onScrollListener: RecyclerView.OnScrollListener
operator fun <K, V> Iterable<Pair<K, V>>.get(key: K): V? {
return firstOrNull { it.first == key }?.second
}
fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }

View File

@ -118,3 +118,7 @@ const val VULCAN_WEB_ENDPOINT_LUCKY_NUMBER = "Start.mvc/GetKidsLuckyNumbers"
const val VULCAN_WEB_ENDPOINT_REGISTER_DEVICE = "RejestracjaUrzadzeniaToken.mvc/Get"
const val EDUDZIENNIK_USER_AGENT = "Szkolny.eu/${BuildConfig.VERSION_NAME}"
const val PODLASIE_API_VERSION = "1.0.31"
const val PODLASIE_API_URL = "https://cpdklaser.zeto.bialystok.pl/api"
const val PODLASIE_API_USER_ENDPOINT = "/pobierzDaneUcznia"

View File

@ -200,6 +200,12 @@ const val ERROR_EDUDZIENNIK_WEB_LIMITED_ACCESS = 521
const val ERROR_EDUDZIENNIK_WEB_SESSION_EXPIRED = 522
const val ERROR_EDUDZIENNIK_WEB_TEAM_MISSING = 530
const val ERROR_LOGIN_PODLASIE_API_INVALID_TOKEN = 601
const val ERROR_LOGIN_PODLASIE_API_DEVICE_LIMIT = 602
const val ERROR_PODLASIE_API_NO_TOKEN = 630
const val ERROR_PODLASIE_API_OTHER = 631
const val ERROR_PODLASIE_API_DATA_MISSING = 632
const val ERROR_TEMPLATE_WEB_OTHER = 801
const val EXCEPTION_API_TASK = 900
@ -222,5 +228,6 @@ const val EXCEPTION_EDUDZIENNIK_FILE_REQUEST = 921
const val ERROR_ONEDRIVE_DOWNLOAD = 930
const val EXCEPTION_VULCAN_WEB_LOGIN = 931
const val EXCEPTION_VULCAN_WEB_REQUEST = 932
const val EXCEPTION_PODLASIE_API_REQUEST = 940
const val LOGIN_NO_ARGUMENTS = 1201

View File

@ -143,6 +143,7 @@ val edudziennikLoginMethods = listOf(
)
const val LOGIN_TYPE_PODLASIE = 6
const val LOGIN_MODE_PODLASIE_API = 0
const val LOGIN_METHOD_PODLASIE_API = 100
val podlasieLoginMethods = listOf(
LoginMethod(LOGIN_TYPE_PODLASIE, LOGIN_METHOD_PODLASIE_API, PodlasieLoginApi::class.java)

View File

@ -12,6 +12,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.Edudziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.Idziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.Librus
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.Mobidziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.Podlasie
import pl.szczodrzynski.edziennik.data.api.edziennik.template.Template
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.Vulcan
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
@ -74,6 +75,7 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
LOGIN_TYPE_VULCAN -> Vulcan(app, profile, loginStore, taskCallback)
LOGIN_TYPE_IDZIENNIK -> Idziennik(app, profile, loginStore, taskCallback)
LOGIN_TYPE_EDUDZIENNIK -> Edudziennik(app, profile, loginStore, taskCallback)
LOGIN_TYPE_PODLASIE -> Podlasie(app, profile, loginStore, taskCallback)
LOGIN_TYPE_TEMPLATE -> Template(app, profile, loginStore, taskCallback)
else -> null
}

View File

@ -1,7 +1,12 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-5-12
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_TEMPLATE_API
import pl.szczodrzynski.edziennik.crc32
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_PODLASIE_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
@ -14,12 +19,10 @@ class DataPodlasie(app: App, profile: Profile?, loginStore: LoginStore) : Data(a
override fun satisfyLoginMethods() {
loginMethods.clear()
if (isApiLoginValid())
loginMethods += LOGIN_METHOD_TEMPLATE_API
loginMethods += LOGIN_METHOD_PODLASIE_API
}
override fun generateUserCode(): String {
TODO("Not yet implemented")
}
override fun generateUserCode(): String = "$schoolShortName:$loginShort:${studentId?.crc32()}"
/* _
/\ (_)
@ -33,4 +36,51 @@ class DataPodlasie(app: App, profile: Profile?, loginStore: LoginStore) : Data(a
var apiToken: String?
get() { mApiToken = mApiToken ?: loginStore.getLoginData("apiToken", null); return mApiToken }
set(value) { loginStore.putLoginData("apiToken", value); mApiToken = value }
private var mApiUrl: String? = null
var apiUrl: String?
get() { mApiUrl = mApiUrl ?: profile?.getStudentData("apiUrl", null); return mApiUrl }
set(value) { profile?.putStudentData("apiUrl", value) ?: return; mApiUrl = value }
/* ____ _ _
/ __ \| | | |
| | | | |_| |__ ___ _ __
| | | | __| '_ \ / _ \ '__|
| |__| | |_| | | | __/ |
\____/ \__|_| |_|\___|*/
private var mStudentId: String? = null
var studentId: String?
get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", null); return mStudentId }
set(value) { profile?.putStudentData("studentId", value) ?: return; mStudentId = value }
private var mStudentLogin: String? = null
var studentLogin: String?
get() { mStudentLogin = mStudentLogin ?: profile?.getStudentData("studentLogin", null); return mStudentLogin }
set(value) { profile?.putStudentData("studentLogin", value) ?: return; mStudentLogin = value }
private var mSchoolName: String? = null
var schoolName: String?
get() { mSchoolName = mSchoolName ?: profile?.getStudentData("schoolName", null); return mSchoolName }
set(value) { profile?.putStudentData("schoolName", value) ?: return; mSchoolName = value }
private var mClassName: String? = null
var className: String?
get() { mClassName = mClassName ?: profile?.getStudentData("className", null); return mClassName }
set(value) { profile?.putStudentData("className", value) ?: return; mClassName = value }
private var mSchoolYear: String? = null
var schoolYear: String?
get() { mSchoolYear = mSchoolYear ?: profile?.getStudentData("schoolYear", null); return mSchoolYear }
set(value) { profile?.putStudentData("schoolYear", value) ?: return; mSchoolYear = value }
private var mCurrentSemester: Int? = null
var currentSemester: Int
get() { mCurrentSemester = mCurrentSemester ?: profile?.getStudentData("currentSemester", 0); return mCurrentSemester ?: 0 }
set(value) { profile?.putStudentData("currentSemester", value) ?: return; mCurrentSemester = value }
val schoolShortName: String?
get() = studentLogin?.replace(".podlaskie.pl", "")
val loginShort: String?
get() = studentLogin?.split('@')?.get(0)
}

View File

@ -1,14 +1,19 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-5-12
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.App
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.template.TemplateFeatures
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.login.PodlasieLogin
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.podlasieLoginMethods
import pl.szczodrzynski.edziennik.data.api.prepare
import pl.szczodrzynski.edziennik.data.api.templateLoginMethods
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
@ -47,10 +52,14 @@ class Podlasie(val app: App, val profile: Profile?, val loginStore: LoginStore,
|__*/
override fun sync(featureIds: List<Int>, viewId: Int?, onlyEndpoints: List<Int>?, arguments: JsonObject?) {
data.arguments = arguments
data.prepare(templateLoginMethods, TemplateFeatures, featureIds, viewId, onlyEndpoints)
data.prepare(podlasieLoginMethods, PodlasieFeatures, featureIds, viewId, onlyEndpoints)
Utils.d(TAG, "LoginMethod IDs: ${data.targetLoginMethodIds}")
Utils.d(TAG, "Endpoint IDs: ${data.targetEndpointIds}")
PodlasieLogin(data) {
PodlasieData(data) {
completed()
}
}
}
override fun getMessage(message: MessageFull) {

View File

@ -0,0 +1,9 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-5-12
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie
import pl.szczodrzynski.edziennik.data.api.models.Feature
val PodlasieFeatures = listOf<Feature>()

View File

@ -0,0 +1,107 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-5-12
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data
import com.google.gson.JsonObject
import im.wangchao.mhttp.Request
import im.wangchao.mhttp.RequestParams
import im.wangchao.mhttp.Response
import im.wangchao.mhttp.callback.JsonCallbackHandler
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.getInt
import pl.szczodrzynski.edziennik.getJsonObject
import pl.szczodrzynski.edziennik.toHexString
import pl.szczodrzynski.edziennik.utils.Utils
import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.util.*
open class PodlasieApi(open val data: DataPodlasie, open val lastSync: Long?) {
companion object {
const val TAG = "PodlasieApi"
}
val profileId
get() = data.profile?.id ?: -1
val profile
get() = data.profile
fun apiGet(tag: String, endpoint: String, onSuccess: (json: JsonObject) -> Unit) {
val url = PODLASIE_API_URL + endpoint
Utils.d(tag, "Request: Podlasie/Api - $url")
if (data.apiToken == null) {
data.error(tag, ERROR_PODLASIE_API_NO_TOKEN)
return
}
val callback = object : JsonCallbackHandler() {
override fun onSuccess(json: JsonObject?, response: Response?) {
if (json == null || response == null) {
data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY)
.withResponse(response))
return
}
val error = json.getJsonObject("system_message")?.getInt("code")
error?.let { code ->
when (code) {
0 -> ERROR_PODLASIE_API_DATA_MISSING
4 -> ERROR_LOGIN_PODLASIE_API_DEVICE_LIMIT
5 -> ERROR_LOGIN_PODLASIE_API_INVALID_TOKEN
200 -> null // Not an error
else -> ERROR_PODLASIE_API_OTHER
}?.let { errorCode ->
data.error(ApiError(tag, errorCode)
.withApiResponse(json)
.withResponse(response))
return@onSuccess
}
}
try {
onSuccess(json)
} catch (e: Exception) {
data.error(ApiError(tag, EXCEPTION_PODLASIE_API_REQUEST)
.withResponse(response)
.withThrowable(e)
.withApiResponse(json))
}
}
override fun onFailure(response: Response?, throwable: Throwable?) {
data.error(ApiError(tag, ERROR_REQUEST_FAILURE)
.withResponse(response)
.withThrowable(throwable))
}
}
Request.builder()
.url(url)
.userAgent(SYSTEM_USER_AGENT)
.requestParams(RequestParams(mapOf(
"token" to data.apiToken,
"securityToken" to getSecurityToken(),
"mobileId" to data.app.deviceId,
"ver" to PODLASIE_API_VERSION
)))
.callback(callback)
.build()
.enqueue()
}
private fun getSecurityToken(): String {
val format = SimpleDateFormat("yyyy-MM-dd HH", Locale.ENGLISH).format(Calendar.getInstance().time.time)
val instance = MessageDigest.getInstance("SHA-256")
val digest = instance.digest("-EYlwYu8u16miVd8tT?oO7cvoUVQrQN0vr!$format".toByteArray()).toHexString()
val digest2 = instance.digest(data.apiToken!!.toByteArray()).toHexString()
return instance.digest("$digest$digest2".toByteArray()).toHexString()
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-5-12
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie
import pl.szczodrzynski.edziennik.utils.Utils
class PodlasieData(val data: DataPodlasie, val onSuccess: () -> Unit) {
companion object {
const val TAG = "PodlasieData"
}
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) {
data.progress(data.progressStep)
nextEndpoint(onSuccess)
}
}
private fun useEndpoint(endpointId: Int, lastSync: Long?, onSuccess: (endpointId: Int) -> Unit) {
Utils.d(TAG, "Using endpoint $endpointId. Last sync time = $lastSync")
when (endpointId) {
else -> onSuccess(endpointId)
}
}
}

View File

@ -1,13 +1,70 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-5-12
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.firstlogin
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_PODLASIE
import pl.szczodrzynski.edziennik.data.api.PODLASIE_API_USER_ENDPOINT
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data.PodlasieApi
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.login.PodlasieLoginApi
import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent
import pl.szczodrzynski.edziennik.data.db.entity.Profile
class PodlasieFirstLogin(val data: DataPodlasie, val onSuccess: () -> Unit) {
companion object {
const val TAG = "PodlasieFirstLogin"
}
private val api = PodlasieApi(data, null)
init {
// TODO
val loginStoreId = data.loginStore.id
val loginStoreType = LOGIN_TYPE_PODLASIE
PodlasieLoginApi(data) {
api.apiGet(TAG, PODLASIE_API_USER_ENDPOINT) { json ->
val uuid = json.getString("Uuid")
val login = json.getString("Login")
val firstName = json.getString("FirstName")
val lastName = json.getString("LastName")
val studentNameLong = "$firstName $lastName".fixName()
val studentNameShort = studentNameLong.getShortName()
val schoolName = json.getString("SchoolName")
val className = json.getString("SchoolClass")
val schoolYear = json.getString("ActualSchoolYear")?.replace(' ', '/')
val semester = json.getString("ActualTermShortcut")?.length
val apiUrl = json.getString("URL")
val profile = Profile(
loginStoreId,
loginStoreId,
loginStoreType,
studentNameLong,
login,
studentNameLong,
studentNameShort,
null
).apply {
studentData["studentId"] = uuid
studentData["studentLogin"] = login
studentData["schoolName"] = schoolName
studentData["className"] = className
studentData["schoolYear"] = schoolYear
studentData["currentSemester"] = semester ?: 1
studentData["apiUrl"] = apiUrl
schoolYear?.split('/')?.get(0)?.toInt()?.let {
studentSchoolYearStart = it
}
}
EventBus.getDefault().postSticky(FirstLoginFinishedEvent(listOf(profile), data.loginStore))
onSuccess()
}
}
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-5-12
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.login
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_PODLASIE_API
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie
import pl.szczodrzynski.edziennik.utils.Utils
class PodlasieLogin(val data: DataPodlasie, val onSuccess: () -> Unit) {
companion object {
const val TAG = "PodlasieLogin"
}
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
}
Utils.d(TAG, "Using login method $loginMethodId")
when (loginMethodId) {
LOGIN_METHOD_PODLASIE_API -> {
data.startProgress(R.string.edziennik_progress_login_podlasie_api)
PodlasieLoginApi(data) { onSuccess(loginMethodId) }
}
}
}
}

View File

@ -1,7 +1,10 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-5-12
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.login
import pl.szczodrzynski.edziennik.data.api.ERROR_LOGIN_DATA_MISSING
import pl.szczodrzynski.edziennik.data.api.ERROR_PROFILE_MISSING
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie
import pl.szczodrzynski.edziennik.data.api.models.ApiError
@ -11,11 +14,6 @@ class PodlasieLoginApi(val data: DataPodlasie, val onSuccess: () -> Unit) {
}
init { run {
if (data.profile == null) {
data.error(ApiError(TAG, ERROR_PROFILE_MISSING))
return@run
}
if (data.isApiLoginValid()) {
onSuccess()
} else {

View File

@ -333,6 +333,34 @@ object LoginInfo {
)
)
)
),
Register(
loginType = LOGIN_TYPE_PODLASIE,
internalName = "podlasie",
registerName = R.string.login_type_podlasie,
registerLogo = R.drawable.login_logo_podlasie,
loginModes = listOf(
Mode(
loginMode = LOGIN_MODE_PODLASIE_API,
name = R.string.login_mode_podlasie_api,
icon = R.drawable.login_mode_podlasie_api,
guideText = R.string.login_mode_podlasie_api_guide,
credentials = listOf(
Credential(
keyName = "apiToken",
name = R.string.login_hint_token,
icon = CommunityMaterial.Icon2.cmd_lock_outline,
emptyText = R.string.login_error_no_token,
invalidText = R.string.login_error_incorrect_token,
errorCodes = mapOf(),
isRequired = true,
validationRegex = "[a-zA-Z0-9]{10}",
caseMode = Credential.CaseMode.UNCHANGED
)
),
errorCodes = mapOf()
)
)
)
) }

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -160,6 +160,12 @@
<string name="error_522" translatable="false">ERROR_EDUDZIENNIK_WEB_SESSION_EXPIRED</string>
<string name="error_530" translatable="false">ERROR_EDUDZIENNIK_WEB_TEAM_MISSING</string>
<string name="error_601" translatable="false">ERROR_LOGIN_PODLASIE_API_INVALID_TOKEN</string>
<string name="error_602" translatable="false">ERROR_LOGIN_PODLASIE_API_DEVICE_LIMIT</string>
<string name="error_630" translatable="false">ERROR_PODLASIE_API_NO_TOKEN</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_801" translatable="false">ERROR_TEMPLATE_WEB_OTHER</string>
<string name="error_900" translatable="false">EXCEPTION_API_TASK</string>
@ -180,6 +186,9 @@
<string name="error_920" translatable="false">EXCEPTION_EDUDZIENNIK_WEB_REQUEST</string>
<string name="error_921" translatable="false">EXCEPTION_EDUDZIENNIK_FILE_REQUEST</string>
<string name="error_930" translatable="false">ERROR_ONEDRIVE_DOWNLOAD</string>
<string name="error_931" translatable="false">EXCEPTION_VULCAN_WEB_LOGIN</string>
<string name="error_932" translatable="false">EXCEPTION_VULCAN_WEB_REQUEST</string>
<string name="error_940" translatable="false">EXCEPTION_PODLASIE_API_REQUEST</string>
<string name="error_1201" translatable="false">LOGIN_NO_ARGUMENTS</string>
@ -339,6 +348,12 @@
<string name="error_522_reason">Sesja wygasła</string>
<string name="error_530_reason">Nie można pobrać informacji o klasie i szkole</string>
<string name="error_601_reason">Nieprawidłowy token</string>
<string name="error_602_reason">Przekroczono maksymalną liczbę urządzeń (5)</string>
<string name="error_630_reason">ERROR_PODLASIE_API_NO_TOKEN</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_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>
@ -359,6 +374,9 @@
<string name="error_920_reason">Wystąpił błąd</string>
<string name="error_921_reason">Wystąpił błąd podczas pobierania pliku</string>
<string name="error_930_reason">Nie udało się pobrać pliku z OneDrive</string>
<string name="error_931_reason">EXCEPTION_VULCAN_WEB_LOGIN</string>
<string name="error_932_reason">EXCEPTION_VULCAN_WEB_REQUEST</string>
<string name="error_940_reason">Zgłoś błąd: wyjątek w API PPE</string>
<string name="error_1201_reason">Nie podano parametrów</string>
</resources>

View File

@ -1352,4 +1352,8 @@
<string name="login_mode_edudziennik_web">Zaloguj używając e-maila i hasła</string>
<string name="login_mode_edudziennik_web_hint">Użyj danych, które podajesz na stronie internetowej e-dziennika</string>
<string name="login_mode_edudziennik_web_guide">Podaj adres e-mail i hasło, których używasz do logowania w przeglądarce na stronie EduDziennika.</string>
<string name="login_type_podlasie">Podlaska Platforma Edukacyjna</string>
<string name="login_mode_podlasie_api">Zaloguj używając tokenu</string>
<string name="login_mode_podlasie_api_guide">Podaj token aplikacji mobilnej.</string>
<string name="edziennik_progress_login_podlasie_api">Logowanie do PPE…</string>
</resources>