[UI/Login] Fallback reCAPTCHA to WebView activity.

This commit is contained in:
Kuba Szczodrzyński 2023-03-24 21:25:54 +01:00
parent 07ab1b984f
commit db00566ebf
No known key found for this signature in database
GPG Key ID: 43037AC62A600562
7 changed files with 194 additions and 3 deletions

View File

@ -160,7 +160,11 @@
<activity android:name=".ui.login.oauth.OAuthLoginActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false"
android:theme="@style/AppTheme.Light" />
android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar" />
<activity android:name=".ui.login.recaptcha.RecaptchaActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false"
android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar" />
<activity android:name=".ui.base.BuildInvalidActivity" android:exported="false" />
<activity android:name=".ui.settings.contributors.ContributorsActivity" android:exported="false" />

View File

@ -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<RecaptchaDialogBinding>(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()

View File

@ -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<RecaptchaViewBinding>(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()
}
}

View File

@ -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,
)
)
}
}

View File

@ -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?,
)

View File

@ -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,

View File

@ -1543,6 +1543,7 @@
<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>
<string name="recaptcha_dialog_title">reCAPTCHA</string>
<string name="app_cannot_load_data">Nie można załadować danych aplikacji</string>
<string name="legend_event_shared_received">{cmd-share-variant} udostępnione w klasie</string>
<string name="legend_event_shared_sent">{cmd-share-variant} udostępnione przez Ciebie</string>