From db00566ebf58482352bb8f873aecec5210d029c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Fri, 24 Mar 2023 21:25:54 +0100 Subject: [PATCH] [UI/Login] Fallback reCAPTCHA to WebView activity. --- app/src/main/AndroidManifest.xml | 6 +- .../edziennik/ui/captcha/RecaptchaDialog.kt | 7 +- .../ui/captcha/RecaptchaPromptDialog.kt | 4 +- .../ui/login/recaptcha/RecaptchaActivity.kt | 132 ++++++++++++++++++ .../ui/login/recaptcha/RecaptchaResult.kt | 10 ++ .../utils/managers/UserActionManager.kt | 37 +++++ app/src/main/res/values/strings.xml | 1 + 7 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaActivity.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaResult.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2c24b426..da531480 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -160,7 +160,11 @@ + android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar" /> + diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaDialog.kt index cfecde91..56d3976a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaDialog.kt @@ -25,6 +25,7 @@ class RecaptchaDialog( private val autoRetry: Boolean = true, private val onSuccess: (recaptchaCode: String) -> Unit, private val onFailure: (() -> Unit)? = null, + private val onServerError: (() -> Unit)? = null, onShowListener: ((tag: String) -> Unit)? = null, onDismissListener: ((tag: String) -> Unit)? = null, ) : BindingDialog(activity, onShowListener, onDismissListener) { @@ -44,7 +45,11 @@ class RecaptchaDialog( override suspend fun onBeforeShow(): Boolean { val (title, text, bitmap) = withContext(Dispatchers.Default) { - val html = loadCaptchaHtml() ?: return@withContext null + val html = loadCaptchaHtml() + if (html == null) { + onServerError?.invoke() + return@withContext null + } return@withContext loadCaptchaData(html) } ?: run { onFailure?.invoke() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaPromptDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaPromptDialog.kt index a927347d..ca876ddb 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaPromptDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaPromptDialog.kt @@ -19,6 +19,7 @@ class RecaptchaPromptDialog( private val referer: String, private val onSuccess: (recaptchaCode: String) -> Unit, private val onCancel: (() -> Unit)?, + private val onServerError: (() -> Unit)? = null, onShowListener: ((tag: String) -> Unit)? = null, onDismissListener: ((tag: String) -> Unit)? = null, ) : BindingDialog(activity, onShowListener, onDismissListener) { @@ -62,7 +63,8 @@ class RecaptchaPromptDialog( b.checkbox.background = checkboxBackground b.checkbox.foreground = checkboxForeground b.progress.visibility = View.GONE - } + }, + onServerError = onServerError, ).show() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaActivity.kt new file mode 100644 index 00000000..2e58da07 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaActivity.kt @@ -0,0 +1,132 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2023-3-24. + */ + +package pl.szczodrzynski.edziennik.ui.login.recaptcha + +import android.annotation.SuppressLint +import android.graphics.Color +import android.os.Build +import android.os.Bundle +import android.util.Base64 +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.webkit.JavascriptInterface +import android.webkit.WebView +import android.widget.FrameLayout +import androidx.appcompat.app.AppCompatActivity +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.SYSTEM_USER_AGENT +import pl.szczodrzynski.edziennik.utils.Themes +import java.nio.charset.Charset + +class RecaptchaActivity : AppCompatActivity() { + companion object { + private const val TAG = "RecaptchaActivity" + + private const val CODE = """ + PCFET0NUWVBFIGh0bWw+PGh0bWw+PGhlYWQ+PHNjcmlwdCBzcmM9Imh0dHBzOi8vd3d3Lmdvb2ds + ZS5jb20vcmVjYXB0Y2hhL2FwaS5qcz9vbmxvYWQ9cmVhZHkmcmVuZGVyPWV4cGxpY2l0Ij48L3Nj + cmlwdD48L2hlYWQ+PGJvZHk+PGJyPjxkaXYgaWQ9ImdyIiBzdHlsZT0icG9zaXRpb246YWJzb2x1 + dGU7dG9wOjUwJTt0cmFuc2Zvcm06dHJhbnNsYXRlKDAsLTUwJSk7Ij48L2Rpdj48YnI+PHNjcmlw + dD5mdW5jdGlvbiByZWFkeSgpe2dyZWNhcHRjaGEucmVuZGVyKCJnciIse3NpdGVrZXk6IlNJVEVL + RVkiLHRoZW1lOiJUSEVNRSIsY2FsbGJhY2s6ZnVuY3Rpb24oZSl7d2luZG93LmlmLmNhbGxiYWNr + KGUpO30sImV4cGlyZWQtY2FsbGJhY2siOndpbmRvdy5pZi5leHBpcmVkQ2FsbGJhY2ssImVycm9y + LWNhbGxiYWNrIjp3aW5kb3cuaWYuZXJyb3JDYWxsYmFja30pO308L3NjcmlwdD48L2JvZHk+PC9o + dG1sPg== + """ + } + + private var isSuccessful = false + private lateinit var jsInterface: CaptchaCallbackInterface + + interface CaptchaCallbackInterface { + @JavascriptInterface + fun callback(recaptchaResponse: String) + + @JavascriptInterface + fun expiredCallback() + + @JavascriptInterface + fun errorCallback() + } + + @SuppressLint("AddJavascriptInterface", "SetJavaScriptEnabled") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setTitle(R.string.recaptcha_dialog_title) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + WebView.setWebContentsDebuggingEnabled(true) + } + + val siteKey = intent.getStringExtra("siteKey") ?: return + val referer = intent.getStringExtra("referer") ?: return + val userAgent = intent.getStringExtra("userAgent") ?: SYSTEM_USER_AGENT + + val htmlContent = Base64.decode(CODE, Base64.DEFAULT) + .toString(Charset.defaultCharset()) + .replace("THEME", if (Themes.isDark) "dark" else "light") + .replace("SITEKEY", siteKey) + + jsInterface = object : CaptchaCallbackInterface { + @JavascriptInterface + override fun callback(recaptchaResponse: String) { + isSuccessful = true + EventBus.getDefault().post( + RecaptchaResult( + isError = false, + code = recaptchaResponse, + ) + ) + finish() + } + + @JavascriptInterface + override fun expiredCallback() { + isSuccessful = false + } + + @JavascriptInterface + override fun errorCallback() { + isSuccessful = false + EventBus.getDefault().post( + RecaptchaResult( + isError = true, + code = null, + ) + ) + finish() + } + } + + val webView = WebView(this).apply { + setBackgroundColor(Color.TRANSPARENT) + settings.javaScriptEnabled = true + settings.userAgentString = userAgent + addJavascriptInterface(jsInterface, "if") + loadDataWithBaseURL( + referer, + htmlContent, + "text/html", + "UTF-8", + null, + ) + // setLayerType(WebView.LAYER_TYPE_SOFTWARE, null) + layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + } + setContentView(webView) + } + + override fun onDestroy() { + super.onDestroy() + if (!isSuccessful) + EventBus.getDefault().post( + RecaptchaResult( + isError = false, + code = null, + ) + ) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaResult.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaResult.kt new file mode 100644 index 00000000..ae5fae19 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaResult.kt @@ -0,0 +1,10 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2023-3-24. + */ + +package pl.szczodrzynski.edziennik.ui.login.recaptcha + +data class RecaptchaResult( + val isError: Boolean, + val code: String?, +) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt index 3e8761c8..9ca4c233 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 @@ -22,6 +22,8 @@ import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.ui.captcha.RecaptchaPromptDialog import pl.szczodrzynski.edziennik.ui.login.oauth.OAuthLoginActivity import pl.szczodrzynski.edziennik.ui.login.oauth.OAuthLoginResult +import pl.szczodrzynski.edziennik.ui.login.recaptcha.RecaptchaActivity +import pl.szczodrzynski.edziennik.ui.login.recaptcha.RecaptchaResult import pl.szczodrzynski.edziennik.utils.Utils.d class UserActionManager(val app: App) { @@ -107,10 +109,45 @@ class UserActionManager(val app: App) { )) }, onCancel = callback.onCancel, + onServerError = { + executeRecaptchaActivity(activity, event, callback) + }, ).show() return true } + private fun executeRecaptchaActivity( + activity: AppCompatActivity, + event: UserActionRequiredEvent, + callback: UserActionCallback, + ): Boolean { + event.params.getString("siteKey") ?: return false + event.params.getString("referer") ?: return false + + var listener: Any? = null + listener = object { + @Subscribe(threadMode = ThreadMode.MAIN) + fun onRecaptchaResult(result: RecaptchaResult) { + EventBus.getDefault().unregister(listener) + when { + result.isError -> callback.onFailure?.invoke() + result.code != null -> { + finishAction(activity, event, callback, Bundle( + "recaptchaCode" to result.code, + "recaptchaTime" to System.currentTimeMillis(), + )) + } + else -> callback.onCancel?.invoke() + } + } + } + EventBus.getDefault().register(listener) + + val intent = Intent(activity, RecaptchaActivity::class.java).putExtras(event.params) + activity.startActivity(intent) + return true + } + private fun executeOauth( activity: AppCompatActivity, event: UserActionRequiredEvent, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e8e44d0..4620f9b2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1543,6 +1543,7 @@ TODO USOS - wymagane logowanie z użyciem przeglądarki Zaloguj się + reCAPTCHA Nie można załadować danych aplikacji {cmd-share-variant} udostępnione w klasie {cmd-share-variant} udostępnione przez Ciebie