From d54f3b4e849f62591d0bedb5b8f25fc85047e42f Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Mon, 15 Jun 2026 12:29:14 +0800 Subject: [PATCH 1/2] fix(captcha): show challenge in centered dialog --- app/src/main/assets/captcha.html | 16 +- .../ui/landing/MnemonicPhraseFragment.kt | 11 +- .../android/ui/landing/MobileFragment.kt | 13 +- .../ui/landing/VerificationFragment.kt | 15 +- .../setting/delete/DeleteAccountFragment.kt | 16 +- .../one/mixin/android/widget/CaptchaView.kt | 327 ++++++++++++++++-- 6 files changed, 334 insertions(+), 64 deletions(-) diff --git a/app/src/main/assets/captcha.html b/app/src/main/assets/captcha.html index d2e4eab910..8c7dbcb252 100644 --- a/app/src/main/assets/captcha.html +++ b/app/src/main/assets/captcha.html @@ -2,6 +2,20 @@ + #gt - \ No newline at end of file + diff --git a/app/src/main/java/one/mixin/android/ui/landing/MnemonicPhraseFragment.kt b/app/src/main/java/one/mixin/android/ui/landing/MnemonicPhraseFragment.kt index f72666aa20..5ff925a49d 100644 --- a/app/src/main/java/one/mixin/android/ui/landing/MnemonicPhraseFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/landing/MnemonicPhraseFragment.kt @@ -2,8 +2,6 @@ package one.mixin.android.ui.landing import android.os.Bundle import android.view.View -import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams.MATCH_PARENT import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -225,6 +223,13 @@ class MnemonicPhraseFragment : BaseFragment(R.layout.fragment_compose) { } private var captchaView: CaptchaView? = null + + override fun onDestroyView() { + captchaView?.release() + captchaView = null + super.onDestroyView() + } + private fun initAndLoadCaptcha(sessionKey: EdKeyPair, edKey: EdKeyPair, errorDescription: String) = lifecycleScope.launch { errorInfo = null @@ -237,6 +242,7 @@ class MnemonicPhraseFragment : BaseFragment(R.layout.fragment_compose) { override fun onStop() { if (viewDestroyed()) return binding.mobileCover.isVisible = false + landingViewModel.updateMnemonicPhraseState(MnemonicPhraseState.Failure) } override fun onPostToken(value: Pair) { @@ -245,7 +251,6 @@ class MnemonicPhraseFragment : BaseFragment(R.layout.fragment_compose) { } }, ) - (view as ViewGroup).addView(captchaView?.webView, MATCH_PARENT, MATCH_PARENT) } captchaView?.loadCaptcha( if (errorDescription.containsIgnoreCase(gtCAPTCHA)) CaptchaView.CaptchaType.GTCaptcha diff --git a/app/src/main/java/one/mixin/android/ui/landing/MobileFragment.kt b/app/src/main/java/one/mixin/android/ui/landing/MobileFragment.kt index a28d847c41..6e8a513af3 100644 --- a/app/src/main/java/one/mixin/android/ui/landing/MobileFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/landing/MobileFragment.kt @@ -8,8 +8,6 @@ import android.text.Selection import android.text.TextWatcher import android.view.View import android.view.View.AUTOFILL_HINT_PHONE -import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.WindowManager import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat @@ -261,6 +259,12 @@ class MobileFragment: BaseFragment(R.layout.fragment_mobile) { setupFocusListeners() } + override fun onDestroyView() { + captchaView?.release() + captchaView = null + super.onDestroyView() + } + private fun applySafeTopPadding(rootView: View) { val originalPaddingTop: Int = rootView.paddingTop ViewCompat.setOnApplyWindowInsetsListener(rootView) { v: View, insets: WindowInsetsCompat -> @@ -304,10 +308,6 @@ class MobileFragment: BaseFragment(R.layout.fragment_mobile) { } override fun onBackPressed(): Boolean { - if (captchaView?.isVisible() == true) { - hideLoading() - return true - } if (binding.keyboard.translationY == 0f) { binding.mobileEt.clearFocus() binding.countryCodeEt.clearFocus() @@ -466,7 +466,6 @@ class MobileFragment: BaseFragment(R.layout.fragment_mobile) { } }, ) - (view as ViewGroup).addView(captchaView?.webView, MATCH_PARENT, MATCH_PARENT) } val captchaType = if (errorDescription.containsIgnoreCase(gtCAPTCHA)) { CaptchaView.CaptchaType.GTCaptcha diff --git a/app/src/main/java/one/mixin/android/ui/landing/VerificationFragment.kt b/app/src/main/java/one/mixin/android/ui/landing/VerificationFragment.kt index bac7807f9f..8c675b621f 100644 --- a/app/src/main/java/one/mixin/android/ui/landing/VerificationFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/landing/VerificationFragment.kt @@ -5,8 +5,6 @@ import android.os.Bundle import android.os.CountDownTimer import android.view.View import android.view.View.GONE -import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams.MATCH_PARENT import androidx.appcompat.view.ContextThemeWrapper import androidx.core.content.ContextCompat import androidx.core.os.bundleOf @@ -180,15 +178,13 @@ class VerificationFragment : PinCodeFragment(R.layout.fragment_verification) { } override fun onDestroyView() { - super.onDestroyView() + captchaView?.release() + captchaView = null mCountDownTimer?.cancel() + super.onDestroyView() } override fun onBackPressed(): Boolean { - if (captchaView?.isVisible() == true) { - hideLoading() - return true - } return false } @@ -395,7 +391,7 @@ class VerificationFragment : PinCodeFragment(R.layout.fragment_verification) { override fun hideLoading() { super.hideLoading() - captchaView?.webView?.visibility = GONE + captchaView?.hide() } private fun sendVerification(captchaResponse: Pair? = null) { @@ -443,7 +439,7 @@ class VerificationFragment : PinCodeFragment(R.layout.fragment_verification) { { t: Throwable -> handleError(t) binding.verificationNextFab.visibility = GONE - captchaView?.webView?.visibility = GONE + captchaView?.hide() }, ) } @@ -464,7 +460,6 @@ class VerificationFragment : PinCodeFragment(R.layout.fragment_verification) { } }, ) - (view as ViewGroup).addView(captchaView?.webView, MATCH_PARENT, MATCH_PARENT) } val captchaType = if (errorDescription.containsIgnoreCase(gtCAPTCHA)) { CaptchaView.CaptchaType.GTCaptcha diff --git a/app/src/main/java/one/mixin/android/ui/setting/delete/DeleteAccountFragment.kt b/app/src/main/java/one/mixin/android/ui/setting/delete/DeleteAccountFragment.kt index 428f2527ba..d7236a6449 100644 --- a/app/src/main/java/one/mixin/android/ui/setting/delete/DeleteAccountFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/setting/delete/DeleteAccountFragment.kt @@ -2,7 +2,6 @@ package one.mixin.android.ui.setting.delete import android.os.Bundle import android.view.View -import android.view.ViewGroup import androidx.core.view.isVisible import androidx.fragment.app.DialogFragment import androidx.fragment.app.viewModels @@ -77,11 +76,13 @@ class DeleteAccountFragment : BaseFragment(R.layout.fragment_delete_account) { } } + override fun onDestroyView() { + captchaView?.release() + captchaView = null + super.onDestroyView() + } + override fun onBackPressed(): Boolean { - if (captchaView?.isVisible() == true) { - captchaView?.hide() - return true - } return false } @@ -253,11 +254,6 @@ class DeleteAccountFragment : BaseFragment(R.layout.fragment_delete_account) { } }, ) - (view as ViewGroup).addView( - captchaView?.webView, - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT, - ) } captchaView?.loadCaptcha( if (errorDescription.containsIgnoreCase(gtCAPTCHA)) CaptchaView.CaptchaType.GTCaptcha diff --git a/app/src/main/java/one/mixin/android/widget/CaptchaView.kt b/app/src/main/java/one/mixin/android/widget/CaptchaView.kt index 967cd79466..6c215830b6 100644 --- a/app/src/main/java/one/mixin/android/widget/CaptchaView.kt +++ b/app/src/main/java/one/mixin/android/widget/CaptchaView.kt @@ -1,35 +1,72 @@ package one.mixin.android.widget import android.annotation.SuppressLint +import android.app.Dialog import android.content.Context import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.GradientDrawable import android.net.http.SslError +import android.view.Gravity +import android.view.ViewGroup +import android.view.Window +import android.view.WindowManager +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.ProgressBar import android.webkit.JavascriptInterface import android.webkit.SslErrorHandler +import android.webkit.WebChromeClient import android.webkit.WebResourceError import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebView import android.webkit.WebViewClient +import androidx.annotation.DrawableRes +import androidx.appcompat.view.ContextThemeWrapper import okio.buffer import okio.source import one.mixin.android.BuildConfig import one.mixin.android.Constants import one.mixin.android.R import one.mixin.android.extension.cancelRunOnUiThread +import one.mixin.android.extension.dp import one.mixin.android.extension.runOnUiThread import one.mixin.android.extension.screenHeight +import one.mixin.android.extension.screenWidth import one.mixin.android.extension.toast import one.mixin.android.extension.translationY import one.mixin.android.util.reportException import timber.log.Timber import java.nio.charset.Charset +internal data class CaptchaDialogBarStyle( + val heightDp: Int, + @DrawableRes val closeIconResId: Int, + val closeIconGravity: Int, + val cornerRadiusDp: Int, + val progressBelowBar: Boolean, +) + +internal fun captchaDialogBarStyle() = + CaptchaDialogBarStyle( + heightDp = 48, + closeIconResId = R.drawable.ic_circle_close, + closeIconGravity = Gravity.END or Gravity.CENTER_VERTICAL, + cornerRadiusDp = 12, + progressBelowBar = true, + ) + @SuppressLint("JavascriptInterface", "SetJavaScriptEnabled") class CaptchaView(private val context: Context, private val callback: Callback) { companion object { private const val WEB_VIEW_TIME_OUT = 35000L + private const val DIALOG_HORIZONTAL_MARGIN_DP = 20 + private const val CAPTCHA_CONTENT_MAX_HEIGHT_DP = 560 + private const val CAPTCHA_DIALOG_DIM_AMOUNT = 0.6f + private const val TAG = "CaptchaView" const val reCAPTCHA = "reCAPTCHA" @@ -37,7 +74,97 @@ class CaptchaView(private val context: Context, private val callback: Callback) const val gtCAPTCHA = "GeeTest" } - val webView: WebView by lazy { + private var captchaDialog: Dialog? = null + private var released = false + private val timedOutCaptchaTypes = mutableSetOf() + + private val captchaContentHeight by lazy { + val barHeight = captchaDialogBarStyle().heightDp.dp + minOf(CAPTCHA_CONTENT_MAX_HEIGHT_DP.dp, (context.screenHeight() * 0.85f).toInt() - barHeight).coerceAtLeast(320.dp) + } + + private val captchaContainer: LinearLayout by lazy { + val barStyle = captchaDialogBarStyle() + LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + background = GradientDrawable().apply { + setColor(Color.WHITE) + cornerRadius = barStyle.cornerRadiusDp.dp.toFloat() + } + clipToOutline = true + addView( + captchaBar, + LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + barStyle.heightDp.dp, + ), + ) + addView( + captchaContent, + LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + captchaContentHeight, + ), + ) + } + } + + private val captchaBar: FrameLayout by lazy { + val barStyle = captchaDialogBarStyle() + FrameLayout(context).apply { + setBackgroundColor(Color.TRANSPARENT) + addView( + closeButton, + FrameLayout.LayoutParams( + 48.dp, + ViewGroup.LayoutParams.MATCH_PARENT, + barStyle.closeIconGravity, + ), + ) + } + } + + private val closeButton: ImageView by lazy { + val barStyle = captchaDialogBarStyle() + ImageView(context).apply { + setImageResource(barStyle.closeIconResId) + setBackgroundResource(R.drawable.mixin_ripple) + setPadding(12.dp, 12.dp, 12.dp, 12.dp) + contentDescription = context.getString(R.string.Cancel) + setOnClickListener { cancelCaptcha() } + } + } + + private val captchaContent: FrameLayout by lazy { + FrameLayout(context).apply { + setBackgroundColor(Color.WHITE) + addView( + webView, + FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ), + ) + addView( + progressBar, + FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + 2.dp, + Gravity.TOP, + ), + ) + } + } + + private val progressBar: ProgressBar by lazy { + ProgressBar(ContextThemeWrapper(context, R.style.ProgressTheme), null, android.R.attr.progressBarStyleHorizontal).apply { + max = 100 + progress = 0 + visibility = ProgressBar.GONE + } + } + + private val webViewLazy = lazy { WebView(context).apply { settings.apply { defaultTextEncodingName = "utf-8" @@ -45,44 +172,48 @@ class CaptchaView(private val context: Context, private val callback: Callback) settings.javaScriptEnabled = true settings.domStorageEnabled = true addJavascriptInterface(this@CaptchaView, "MixinContext") - setBackgroundColor(Color.WHITE) translationY = context.screenHeight().toFloat() } } - private val stopWebViewRunnable = - Runnable { - if (captchaType.isG()) { - Timber.e("load GCaptcha timeout") - reportException(CaptchaException("$TAG load $captchaType timeout")) - loadCaptcha(CaptchaType.HCaptcha) - } else if (captchaType.isGT()) { - Timber.e("load GTCaptcha timeout") - reportException(CaptchaException("$TAG load $captchaType timeout")) - loadCaptcha(CaptchaType.GCaptcha) - } else if (captchaType.isH()) { - Timber.e("load HCaptcha timeout") - reportException(CaptchaException("$TAG load $captchaType timeout")) - loadCaptcha(CaptchaType.GTCaptcha) - } else { - webView.loadUrl("about:blank") - hide() - webView.webViewClient = object : WebViewClient() {} - toast(R.string.Recaptcha_timeout) - callback.onStop() - reportException(CaptchaException("$TAG load $captchaType timeout")) - } - } + val webView: WebView by webViewLazy + + private val stopWebViewRunnable = Runnable { handleCaptchaTimeout() } private var captchaType = CaptchaType.GCaptcha + private var fallbackEnabled = true + + fun loadCaptcha(captchaType: CaptchaType) = loadCaptcha(captchaType, true, true) + + fun loadCaptchaWithoutFallback(captchaType: CaptchaType) = loadCaptcha(captchaType, true, false) - fun loadCaptcha(captchaType: CaptchaType) { + private fun loadCaptcha( + captchaType: CaptchaType, + resetTimeoutFallbacks: Boolean, + fallbackEnabled: Boolean, + ) { + if (released) return this.captchaType = captchaType + this.fallbackEnabled = fallbackEnabled + if (resetTimeoutFallbacks) { + timedOutCaptchaTypes.clear() + } + show() val isG = captchaType.isG() val isH = captchaType.isH() val isGT = captchaType.isGT() - Timber.e("load $captchaType") if (isG || isH || isGT) { + updateProgress(0) + webView.webChromeClient = + object : WebChromeClient() { + override fun onProgressChanged( + view: WebView?, + newProgress: Int, + ) { + super.onProgressChanged(view, newProgress) + updateProgress(newProgress) + } + } webView.webViewClient = object : WebViewClient() { override fun onPageFinished( @@ -93,6 +224,7 @@ class CaptchaView(private val context: Context, private val callback: Callback) if (isGT) view?.evaluateJavascript("initGTCaptcha()") {} cancelRunOnUiThread(stopWebViewRunnable) view?.translationY(0f) + updateProgress(100) } override fun onReceivedHttpError( @@ -101,7 +233,9 @@ class CaptchaView(private val context: Context, private val callback: Callback) errorResponse: WebResourceResponse?, ) { super.onReceivedHttpError(view, request, errorResponse) - reportException(CaptchaException("$TAG load $captchaType onReceivedHttpError ${errorResponse?.statusCode} ${errorResponse?.reasonPhrase}")) + val message = "$TAG load $captchaType onReceivedHttpError ${errorResponse?.statusCode} ${errorResponse?.reasonPhrase}" + Timber.e(message) + reportException(CaptchaException(message)) } override fun onReceivedSslError( @@ -110,7 +244,9 @@ class CaptchaView(private val context: Context, private val callback: Callback) error: SslError?, ) { super.onReceivedSslError(view, handler, error) - reportException(CaptchaException("$TAG load $captchaType onReceivedSslError ${error?.toString()}")) + val message = "$TAG load $captchaType onReceivedSslError ${error?.toString()}" + Timber.e(message) + reportException(CaptchaException(message)) } override fun onReceivedError( @@ -119,7 +255,9 @@ class CaptchaView(private val context: Context, private val callback: Callback) error: WebResourceError?, ) { super.onReceivedError(view, request, error) - reportException(CaptchaException("$TAG load $captchaType onReceivedError ${error?.errorCode} ${error?.description}")) + val message = "$TAG load $captchaType onReceivedError ${error?.errorCode} ${error?.description}" + Timber.e(message) + reportException(CaptchaException(message)) } override fun shouldInterceptRequest( @@ -131,6 +269,7 @@ class CaptchaView(private val context: Context, private val callback: Callback) val inputStream = context.assets.open("gt4.js") return WebResourceResponse("application/javascript", "UTF-8", inputStream) } catch (e: Exception) { + Timber.e(e, "$TAG load $captchaType intercept local gt4.js failed") reportException(e) } } @@ -175,10 +314,123 @@ class CaptchaView(private val context: Context, private val callback: Callback) } } - fun isVisible() = webView.translationY == 0f + private fun handleCaptchaTimeout() { + if (released) return + val message = "$TAG load $captchaType timeout" + Timber.e(message) + reportException(CaptchaException(message)) + if (!fallbackEnabled) { + updateProgress(100) + callback.onStop() + return + } + timedOutCaptchaTypes.add(captchaType) + val fallbackCaptchaType = captchaType.fallback() + if (fallbackCaptchaType !in timedOutCaptchaTypes) { + loadCaptcha(fallbackCaptchaType, false, true) + } else { + hide() + toast(R.string.Recaptcha_timeout) + callback.onStop() + } + } + + private fun show() { + val dialog = captchaDialog ?: createDialog().also { + captchaDialog = it + } + webView.translationY = context.screenHeight().toFloat() + if (!dialog.isShowing) { + try { + dialog.show() + } catch (e: Exception) { + Timber.e(e, "$TAG show dialog failed captchaType=$captchaType") + reportException(e) + } + } + updateDialogWindow(dialog) + } + + private fun createDialog() = + Dialog(context).apply { + requestWindowFeature(Window.FEATURE_NO_TITLE) + setCanceledOnTouchOutside(false) + (captchaContainer.parent as? ViewGroup)?.removeView(captchaContainer) + setContentView(captchaContainer) + window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + window?.setGravity(Gravity.CENTER) + setOnShowListener { + updateDialogWindow(this) + } + setOnCancelListener { + stopCaptcha() + captchaDialog = null + callback.onStop() + } + setOnDismissListener { + if (captchaDialog === this) { + captchaDialog = null + } + } + } + + private fun updateDialogWindow(dialog: Dialog) { + dialog.window?.apply { + setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + setGravity(Gravity.CENTER) + addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + setDimAmount(CAPTCHA_DIALOG_DIM_AMOUNT) + setLayout( + context.screenWidth() - DIALOG_HORIZONTAL_MARGIN_DP.dp * 2, + ViewGroup.LayoutParams.WRAP_CONTENT, + ) + } + } + + fun isVisible() = captchaDialog?.isShowing == true + + private fun updateProgress(progress: Int) { + progressBar.progress = progress + progressBar.visibility = if (progress in 0..99) ProgressBar.VISIBLE else ProgressBar.GONE + } fun hide() { + if (released) return + stopCaptcha() + val dialog = captchaDialog + captchaDialog = null + dialog?.dismiss() + } + + private fun cancelCaptcha() { + if (released) return + hide() + callback.onStop() + } + + fun release() { + if (released) return + released = true + cancelRunOnUiThread(stopWebViewRunnable) + val dialog = captchaDialog + captchaDialog = null + dialog?.dismiss() + if (!webViewLazy.isInitialized()) return + webView.webChromeClient = object : WebChromeClient() {} + webView.loadUrl("about:blank") + webView.webViewClient = object : WebViewClient() {} + (webView.parent as? ViewGroup)?.removeView(webView) + webView.destroy() + } + + private fun stopCaptcha() { + cancelRunOnUiThread(stopWebViewRunnable) + updateProgress(100) + if (!webViewLazy.isInitialized()) return webView.translationY(context.screenHeight().toFloat()) + webView.webChromeClient = object : WebChromeClient() {} + webView.loadUrl("about:blank") + webView.webViewClient = object : WebViewClient() {} } @Suppress("unused") @@ -186,7 +438,9 @@ class CaptchaView(private val context: Context, private val callback: Callback) fun postMessage( @Suppress("UNUSED_PARAMETER") value: String, ) { + if (released) return if (value.isBlank()) return + Timber.e("$TAG postMessage captchaType=$captchaType value=$value") cancelRunOnUiThread(stopWebViewRunnable) runOnUiThread(stopWebViewRunnable) } @@ -194,11 +448,11 @@ class CaptchaView(private val context: Context, private val callback: Callback) @Suppress("unused") @JavascriptInterface fun postToken(value: String) { + if (released) return cancelRunOnUiThread(stopWebViewRunnable) webView.post { + if (released) return@post hide() - webView.loadUrl("about:blank") - webView.webViewClient = object : WebViewClient() {} callback.onPostToken(Pair(captchaType, value)) } } @@ -212,6 +466,13 @@ class CaptchaView(private val context: Context, private val callback: Callback) fun isG() = this == GCaptcha fun isH() = this == HCaptcha fun isGT() = this == GTCaptcha + + fun fallback() = + when (this) { + GCaptcha -> HCaptcha + HCaptcha -> GTCaptcha + GTCaptcha -> GCaptcha + } } interface Callback { From b4d3bf80332da8df18874d3d987ae25a1412d64d Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Mon, 15 Jun 2026 12:29:26 +0800 Subject: [PATCH 2/2] chore(debug): add captcha preview controls --- .../android/ui/setting/LogAndDebugFragment.kt | 63 +++++++++++++++++++ .../main/res/layout/fragment_log_debug.xml | 12 ++++ .../layout/view_captcha_preview_bottom.xml | 43 +++++++++++++ app/src/main/res/values-zh-rCN/strings.xml | 5 ++ app/src/main/res/values/strings.xml | 5 ++ 5 files changed, 128 insertions(+) create mode 100644 app/src/main/res/layout/view_captcha_preview_bottom.xml diff --git a/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugFragment.kt b/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugFragment.kt index 8b0b5db00d..da95d95322 100644 --- a/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugFragment.kt @@ -1,10 +1,12 @@ package one.mixin.android.ui.setting +import android.annotation.SuppressLint import android.content.ActivityNotFoundException import android.content.Intent import android.content.pm.PackageManager import android.os.Bundle import android.view.View +import androidx.appcompat.view.ContextThemeWrapper import androidx.core.net.toUri import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope @@ -15,6 +17,7 @@ import kotlinx.coroutines.withContext import one.mixin.android.Constants import one.mixin.android.R import one.mixin.android.databinding.FragmentLogDebugBinding +import one.mixin.android.databinding.ViewCaptchaPreviewBottomBinding import one.mixin.android.db.DatabaseMonitor import one.mixin.android.db.property.PropertyHelper.findValueByKey import one.mixin.android.db.property.PropertyHelper.updateKeyValue @@ -33,6 +36,8 @@ import one.mixin.android.ui.home.reminder.VerifyMobileReminderBottomSheetDialogF import one.mixin.android.ui.setting.diagnosis.DiagnosisFragment import one.mixin.android.util.debug.FileLogTree import one.mixin.android.util.viewBinding +import one.mixin.android.widget.BottomSheet +import one.mixin.android.widget.CaptchaView import javax.inject.Inject @AndroidEntryPoint @@ -45,6 +50,7 @@ class LogAndDebugFragment : BaseFragment(R.layout.fragment_log_debug) { private val binding by viewBinding(FragmentLogDebugBinding::bind) private val viewModel by viewModels() + private var captchaView: CaptchaView? = null @Inject lateinit var jobManager: MixinJobManager @@ -124,6 +130,10 @@ class LogAndDebugFragment : BaseFragment(R.layout.fragment_log_debug) { toast(R.string.New_Update_Reminder_Will_Show_Once) } + previewCaptcha.setOnClickListener { + showCaptchaPreviewBottom() + } + resetTpslGuide.setOnClickListener { defaultSharedPreferences.edit() .remove(one.mixin.android.ui.home.web3.trade.perps.PREF_HIDE_TP_GUIDE_UNTIL) @@ -172,6 +182,59 @@ class LogAndDebugFragment : BaseFragment(R.layout.fragment_log_debug) { } } + override fun onDestroyView() { + captchaView?.release() + captchaView = null + super.onDestroyView() + } + + @SuppressLint("InflateParams") + private fun showCaptchaPreviewBottom() { + val builder = BottomSheet.Builder(requireActivity()) + val bottomBinding = ViewCaptchaPreviewBottomBinding.bind( + View.inflate( + ContextThemeWrapper(requireActivity(), R.style.Custom), + R.layout.view_captcha_preview_bottom, + null, + ), + ) + builder.setCustomView(bottomBinding.root) + val bottomSheet = builder.create() + bottomBinding.apply { + gCaptcha.setOnClickListener { + bottomSheet.dismiss() + previewCaptcha(CaptchaView.CaptchaType.GCaptcha) + } + hCaptcha.setOnClickListener { + bottomSheet.dismiss() + previewCaptcha(CaptchaView.CaptchaType.HCaptcha) + } + gtCaptcha.setOnClickListener { + bottomSheet.dismiss() + previewCaptcha(CaptchaView.CaptchaType.GTCaptcha) + } + } + bottomSheet.show() + } + + private fun previewCaptcha(captchaType: CaptchaView.CaptchaType) { + val view = + captchaView ?: CaptchaView( + requireContext(), + object : CaptchaView.Callback { + override fun onStop() { + } + + override fun onPostToken(value: Pair) { + toast(getString(R.string.Captcha_Preview_Token_Received, value.first.name)) + } + }, + ).also { + captchaView = it + } + view.loadCaptchaWithoutFallback(captchaType) + } + private fun shareLogsFile() { val dialog = indeterminateProgressDialog(message = R.string.Please_wait_a_bit).apply { diff --git a/app/src/main/res/layout/fragment_log_debug.xml b/app/src/main/res/layout/fragment_log_debug.xml index caac93bf6b..75bbc02edc 100644 --- a/app/src/main/res/layout/fragment_log_debug.xml +++ b/app/src/main/res/layout/fragment_log_debug.xml @@ -165,6 +165,18 @@ android:foreground="?android:attr/selectableItemBackground" android:textSize="16sp" /> + + + + + + + + + + + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index d6eedd136b..e14fb95b72 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -2000,6 +2000,11 @@ Recovery Kit 提醒弹窗将显示一次 预览新版本提醒 新版本提醒弹窗将显示一次 + 预览验证码 + 预览 GCaptcha + 预览 HCaptcha + 预览 GeeTest + 验证码预览已收到 token:%1$s Web3 交易数据已删除,将重新同步数据 删除失败: %1$s 什么是隐私钱包? diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d01cdde6cc..a350f2e023 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2045,6 +2045,11 @@ Recovery reminder will show once Preview New Update Reminder New update reminder will show once + Preview Captcha + Preview GCaptcha + Preview HCaptcha + Preview GeeTest Captcha + Captcha preview token received: %1$s Web3 transaction data has been deleted and will be re-synchronized Delete failed: %1$s What\'s the Privacy Wallet?