From 2ff784066efc716c82b2843293eb6ecbe9ac86b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Fri, 14 Oct 2022 21:44:58 +0200 Subject: [PATCH] [API/Usos] Implement OAuth authorization flow. --- .../edziennik/data/api/Constants.kt | 9 +++ .../data/api/edziennik/EdziennikTask.kt | 2 + .../data/api/edziennik/usos/DataUsos.kt | 10 +++ .../edziennik/data/api/edziennik/usos/Usos.kt | 5 +- .../data/api/edziennik/usos/data/UsosApi.kt | 12 ++-- .../data/api/edziennik/usos/data/UsosData.kt | 2 +- .../usos/firstlogin/UsosFirstLogin.kt | 23 +++++++ .../api/edziennik/usos/login/UsosLoginApi.kt | 53 +++++++++++++-- .../edziennik/ext/TextExtensions.kt | 7 ++ .../edziennik/ui/dialogs/OAuthLoginDialog.kt | 67 +++++++++++++++++++ .../edziennik/ui/login/LoginActivity.kt | 5 +- .../edziennik/ui/login/LoginFormFragment.kt | 12 +++- .../utils/managers/UserActionManager.kt | 43 +++++++++--- app/src/main/res/values/strings.xml | 1 + 14 files changed, 225 insertions(+), 26 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/firstlogin/UsosFirstLogin.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/OAuthLoginDialog.kt diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt index 3df74621..c0318251 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt @@ -99,3 +99,12 @@ const val PODLASIE_API_VERSION = "1.0.62" const val PODLASIE_API_URL = "https://cpdklaser.zeto.bialystok.pl/api" const val PODLASIE_API_USER_ENDPOINT = "/pobierzDaneUcznia" const val PODLASIE_API_LOGOUT_DEVICES_ENDPOINT = "/wyczyscUrzadzenia" + +const val USOS_API_OAUTH_REDIRECT_URL = "szkolny://redirect/usos" + +val USOS_API_SCOPES by lazy { listOf( + "offline_access", + "studies", + "grades", + "events", +) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt index 0b49e38b..097ed29e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt @@ -12,6 +12,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.Librus import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.Mobidziennik import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.Podlasie import pl.szczodrzynski.edziennik.data.api.edziennik.template.Template +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.Usos import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.Vulcan import pl.szczodrzynski.edziennik.data.api.events.RegisterAvailabilityEvent import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback @@ -113,6 +114,7 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa LOGIN_TYPE_VULCAN -> Vulcan(app, profile, loginStore, taskCallback) LOGIN_TYPE_PODLASIE -> Podlasie(app, profile, loginStore, taskCallback) LOGIN_TYPE_TEMPLATE -> Template(app, profile, loginStore, taskCallback) + LOGIN_TYPE_USOS -> Usos(app, profile, loginStore, taskCallback) else -> null } if (edziennikInterface == null) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt index 1dee14ba..f374db94 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt @@ -27,11 +27,21 @@ class DataUsos( override fun generateUserCode() = "USOS:TEST" + var schoolId: String? + get() { mSchoolId = mSchoolId ?: loginStore.getLoginData("schoolId", null); return mSchoolId } + set(value) { loginStore.putLoginData("schoolId", value); mSchoolId = value } + private var mSchoolId: String? = null + var instanceUrl: String? get() { mInstanceUrl = mInstanceUrl ?: loginStore.getLoginData("instanceUrl", null); return mInstanceUrl } set(value) { loginStore.putLoginData("instanceUrl", value); mInstanceUrl = value } private var mInstanceUrl: String? = null + var oauthLoginResponse: String? + get() { mOauthLoginResponse = mOauthLoginResponse ?: loginStore.getLoginData("oauthLoginResponse", null); return mOauthLoginResponse } + set(value) { loginStore.putLoginData("oauthLoginResponse", value); mOauthLoginResponse = value } + private var mOauthLoginResponse: String? = null + var oauthConsumerKey: String? get() { mOauthConsumerKey = mOauthConsumerKey ?: loginStore.getLoginData("oauthConsumerKey", null); return mOauthConsumerKey } set(value) { loginStore.putLoginData("oauthConsumerKey", value); mOauthConsumerKey = value } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/Usos.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/Usos.kt index c97e3695..4ff9d04c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/Usos.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/Usos.kt @@ -6,6 +6,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.usos import com.google.gson.JsonObject import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.firstlogin.UsosFirstLogin import pl.szczodrzynski.edziennik.data.api.edziennik.usos.login.UsosLogin import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface @@ -71,9 +72,9 @@ class Usos( override fun getEvent(eventFull: EventFull) {} override fun firstLogin() { - /*UsosFirstLogin(data) { + UsosFirstLogin(data) { completed() - }*/ + } } override fun cancel() { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt index 6ffcd46a..ac73e29a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt @@ -78,7 +78,7 @@ open class UsosApi(open val data: DataUsos, open val lastSync: Long?) { responseType: ResponseType, onSuccess: (data: T) -> Unit, ) { - val url = "${data.instanceUrl}/services/$service" + val url = "${data.instanceUrl}services/$service" d(tag, "Request: Usos/Api - $url") val formData = params.mapValues { valueToString(it.value) @@ -92,7 +92,11 @@ open class UsosApi(open val data: DataUsos, open val lastSync: Long?) { "oauth_token" to (data.oauthTokenKey ?: ""), "oauth_version" to "1.0", ) - val signature = buildSignature("POST", url, formData + auth) + val signature = buildSignature( + method = "POST", + url = url, + params = formData + auth.filterKeys { it.startsWith("oauth_") }, + ) auth["oauth_signature"] = signature val authString = auth.map { @@ -152,10 +156,10 @@ open class UsosApi(open val data: DataUsos, open val lastSync: Long?) { private fun processResponse( response: Response?, - data: T?, + data: T, onSuccess: (data: T) -> Unit, ) { - + onSuccess(data) } private fun processError( diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt index bea52ea3..5ad199ff 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt @@ -41,7 +41,7 @@ class UsosData(val data: DataUsos, val onSuccess: () -> Unit) { when (endpointId) { ENDPOINT_USOS_API_USER -> { data.startProgress(R.string.edziennik_progress_endpoint_student_info) - TemplateWebSample(data, lastSync, onSuccess) +// TemplateWebSample(data, lastSync, onSuccess) } else -> onSuccess(endpointId) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/firstlogin/UsosFirstLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/firstlogin/UsosFirstLogin.kt new file mode 100644 index 00000000..3094980c --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/firstlogin/UsosFirstLogin.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-14. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos.firstlogin + +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.login.UsosLoginApi + +class UsosFirstLogin(val data: DataUsos, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "UsosFirstLogin" + } + + private val api = UsosApi(data, null) + + init { + UsosLoginApi(data) { + + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt index bdf771c3..3027c334 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt @@ -4,10 +4,17 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.usos.login -import pl.szczodrzynski.edziennik.data.api.ERROR_PROFILE_MISSING import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_OAUTH_LOGIN_REQUEST +import pl.szczodrzynski.edziennik.data.api.USOS_API_OAUTH_REDIRECT_URL +import pl.szczodrzynski.edziennik.data.api.USOS_API_SCOPES import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.ext.Bundle +import pl.szczodrzynski.edziennik.ext.fromQueryString +import pl.szczodrzynski.edziennik.ext.toBundle +import pl.szczodrzynski.edziennik.ext.toQueryString +import pl.szczodrzynski.edziennik.utils.Utils.d class UsosLoginApi(val data: DataUsos, val onSuccess: () -> Unit) { companion object { @@ -15,15 +22,47 @@ class UsosLoginApi(val data: DataUsos, val onSuccess: () -> Unit) { } init { run { - if (data.profile == null) { - data.error(ApiError(TAG, ERROR_PROFILE_MISSING)) - return@run - } - if (data.isApiLoginValid()) { onSuccess() + } else if (data.oauthLoginResponse != null) { + login() } else { - data.error(ApiError(TAG, ERROR_USOS_OAUTH_LOGIN_REQUEST)) + authorize() } }} + + private fun authorize() { + val api = UsosApi(data, null) + + api.apiRequest( + tag = TAG, + service = "oauth/request_token", + params = mapOf( + "oauth_callback" to USOS_API_OAUTH_REDIRECT_URL, + "scopes" to USOS_API_SCOPES, + ), + responseType = UsosApi.ResponseType.PLAIN, + ) { + val response = it.fromQueryString() + data.oauthTokenKey = response["oauth_token"] + data.oauthTokenSecret = response["oauth_token_secret"] + + val authUrl = "${data.instanceUrl}services/oauth/authorize" + val authParams = mapOf( + "interactivity" to "confirm_user", + "oauth_token" to (data.oauthTokenKey ?: ""), + ) + val params = Bundle( + "authorizeUrl" to "$authUrl?${authParams.toQueryString()}", + "redirectUrl" to USOS_API_OAUTH_REDIRECT_URL, + "responseStoreKey" to "oauthLoginResponse", + "extras" to data.loginStore.data.toBundle(), + ) + data.error(ApiError(TAG, ERROR_USOS_OAUTH_LOGIN_REQUEST).withParams(params)) + } + } + + private fun login() { + d(TAG, "Login to ${data.schoolId} with ${data.oauthLoginResponse} (${data.oauthTokenSecret})") + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt index 84fe6631..d7717c7b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt @@ -18,6 +18,7 @@ import android.text.style.StyleSpan import androidx.annotation.PluralsRes import androidx.annotation.StringRes import com.mikepenz.materialdrawer.holder.StringHolder +import java.net.URLDecoder import java.net.URLEncoder fun CharSequence?.isNotNullNorEmpty(): Boolean { @@ -346,8 +347,14 @@ fun CharSequence.toStringHolder() = StringHolder(this) fun @receiver:StringRes Int.resolveString(context: Context) = context.getString(this) fun String.urlEncode(): String = URLEncoder.encode(this, "UTF-8").replace("+", "%20") +fun String.urlDecode(): String = URLDecoder.decode(this, "UTF-8") fun Map.toQueryString() = this .map { it.key.urlEncode() to it.value.urlEncode() } .sortedBy { it.first } .joinToString("&") { "${it.first}=${it.second}" } + +fun String.fromQueryString() = this + .split("&") + .map { it.split("=") } + .associate { it[0].urlDecode() to it[1].urlDecode() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/OAuthLoginDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/OAuthLoginDialog.kt new file mode 100644 index 00000000..b777377e --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/OAuthLoginDialog.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-14. + */ + +package pl.szczodrzynski.edziennik.ui.dialogs + +import android.annotation.SuppressLint +import android.graphics.Bitmap +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.FrameLayout +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.updateLayoutParams +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.ext.dp +import pl.szczodrzynski.edziennik.ui.dialogs.base.ViewDialog +import pl.szczodrzynski.edziennik.utils.Utils.d + +class OAuthLoginDialog( + activity: AppCompatActivity, + private val authorizeUrl: String, + private val redirectUrl: String, + private val onSuccess: (responseUrl: String) -> Unit, + private val onFailure: (() -> Unit)?, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : ViewDialog(activity, onShowListener, onDismissListener) { + + override val TAG = "OAuthLoginDialog" + + override fun getTitleRes() = R.string.oauth_dialog_title + override fun getPositiveButtonText() = R.string.close + + private var isSuccessful = false + + @SuppressLint("SetJavaScriptEnabled") + override fun getRootView(): WebView { + val webView = WebView(activity) + webView.webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { + d(TAG, "Navigating to $url") + if (url.startsWith(redirectUrl)) { + isSuccessful = true + onSuccess(url) + dismiss() + } + } + } + webView.settings.javaScriptEnabled = true + return webView + } + + override suspend fun onShow() { + dialog.window?.setLayout(MATCH_PARENT, MATCH_PARENT) + root.minimumHeight = activity.windowManager.defaultDisplay?.height?.div(2) ?: 300.dp + root.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + root.loadUrl(authorizeUrl) + } + + override fun onDismiss() { + root.stopLoading() + if (!isSuccessful) + onFailure?.invoke() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginActivity.kt index 61aa4520..08d4917d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginActivity.kt @@ -31,6 +31,7 @@ class LoginActivity : AppCompatActivity(), CoroutineScope { private val app: App by lazy { applicationContext as App } private lateinit var b: LoginActivityBinding lateinit var navOptions: NavOptions + lateinit var navOptionsBuilder: NavOptions.Builder val nav by lazy { Navigation.findNavController(this, R.id.nav_host_fragment) } val errorSnackbar: ErrorSnackbar by lazy { ErrorSnackbar(this) } val swipeRefreshLayout: SwipeRefreshLayoutNoTouch by lazy { b.swipeRefreshLayout } @@ -87,12 +88,12 @@ class LoginActivity : AppCompatActivity(), CoroutineScope { super.onCreate(savedInstanceState) setTheme(R.style.AppTheme_Light) - navOptions = NavOptions.Builder() + navOptionsBuilder = NavOptions.Builder() .setEnterAnim(R.anim.slide_in_right) .setExitAnim(R.anim.slide_out_left) .setPopEnterAnim(R.anim.slide_in_left) .setPopExitAnim(R.anim.slide_out_right) - .build() + navOptions = navOptionsBuilder.build() b = LoginActivityBinding.inflate(layoutInflater) setContentView(b.root) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginFormFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginFormFragment.kt index b403568b..12daadcd 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginFormFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginFormFragment.kt @@ -14,6 +14,8 @@ import android.widget.Toast import androidx.core.view.isVisible import androidx.core.widget.addTextChangedListener import androidx.fragment.app.Fragment +import androidx.navigation.NavOptions +import androidx.navigation.navOptions import androidx.viewbinding.ViewBinding import com.google.android.material.textfield.TextInputLayout import com.mikepenz.iconics.IconicsDrawable @@ -304,6 +306,14 @@ class LoginFormFragment : Fragment(), CoroutineScope { if (hasErrors) return - nav.navigate(R.id.loginProgressFragment, payload, activity.navOptions) + val navOptions = + if (credentials.isEmpty()) + activity.navOptionsBuilder + .setPopUpTo(R.id.loginPlatformListFragment, inclusive = false) + .build() + else + activity.navOptions + + nav.navigate(R.id.loginProgressFragment, payload, navOptions) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt index bbdb8602..c848c47a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt @@ -21,6 +21,7 @@ import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.ui.captcha.LibrusCaptchaDialog +import pl.szczodrzynski.edziennik.ui.dialogs.OAuthLoginDialog import pl.szczodrzynski.edziennik.utils.Utils.d class UserActionManager(val app: App) { @@ -89,9 +90,13 @@ class UserActionManager(val app: App) { onFailure: (() -> Unit)? = null ) { d(TAG, "Running user action ($type) with params: ${params?.toJsonObject()}") - when (type) { + val isSuccessful = when (type) { UserActionRequiredEvent.CAPTCHA_LIBRUS -> executeLibrus(activity, profileId, params, onSuccess, onFailure) UserActionRequiredEvent.OAUTH_USOS -> executeOauth(activity, profileId, params, onSuccess, onFailure) + else -> false + } + if (!isSuccessful) { + onFailure?.invoke() } } @@ -101,9 +106,9 @@ class UserActionManager(val app: App) { params: Bundle?, onSuccess: ((params: Bundle) -> Unit)?, onFailure: (() -> Unit)?, - ) { + ): Boolean { if (profileId == null) - return + return false val extras = params?.getBundle("extras") // show captcha dialog // use passed onSuccess listener, else sync profile @@ -117,14 +122,14 @@ class UserActionManager(val app: App) { if (extras != null) args.putAll(extras) - if (onSuccess != null) { + if (onSuccess != null) onSuccess(args) - } else { + else EdziennikTask.syncProfile(profileId, arguments = args.toJsonObject()).enqueue(activity) - } }, - onFailure = onFailure + onFailure = onFailure, ).show() + return true } private fun executeOauth( @@ -133,10 +138,30 @@ class UserActionManager(val app: App) { params: Bundle?, onSuccess: ((params: Bundle) -> Unit)?, onFailure: (() -> Unit)?, - ) { + ): Boolean { if (profileId == null || params == null) - return + return false val extras = params.getBundle("extras") + val storeKey = params.getString("responseStoreKey") ?: return false + OAuthLoginDialog( + activity = activity, + authorizeUrl = params.getString("authorizeUrl") ?: return false, + redirectUrl = params.getString("redirectUrl") ?: return false, + onSuccess = { responseUrl -> + val args = Bundle( + storeKey to responseUrl, + ) + if (extras != null) + args.putAll(extras) + + if (onSuccess != null) + onSuccess(args) + else + EdziennikTask.syncProfile(profileId, arguments = args.toJsonObject()).enqueue(activity) + }, + onFailure = onFailure, + ).show() + return true } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 393248c9..7aae860c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1542,4 +1542,5 @@ Logowanie z użyciem przeglądarki TODO USOS - wymagane logowanie z użyciem przeglądarki + Zaloguj się