From d79108ca450cf53ec07339c2ab860105741e1660 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Thu, 2 Jul 2026 20:10:55 +0800 Subject: [PATCH 1/2] fix(web): close webview for page-opening mixin schemes --- .../java/one/mixin/android/extension/UrlExtension.kt | 2 ++ .../link/LinkBottomSheetDialogFragment.kt | 11 ++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/one/mixin/android/extension/UrlExtension.kt b/app/src/main/java/one/mixin/android/extension/UrlExtension.kt index 915a4a1ed2..01332f6767 100644 --- a/app/src/main/java/one/mixin/android/extension/UrlExtension.kt +++ b/app/src/main/java/one/mixin/android/extension/UrlExtension.kt @@ -289,6 +289,7 @@ fun String.checkUserOrApp( } AnalyticsTracker.trackOpenBotHomePage(AnalyticsTracker.BotSource.SCHEME, app.appNumber) WebActivity.show(context, url, null, app) + closeSourceWebActivityIfNeeded(context) if (context is UrlInterpreterActivity) { context.finish() } @@ -325,6 +326,7 @@ fun String.checkConversation( val conversation = conversationDao.getConversationByIdSuspend(conversationId) if (conversation != null) { ConversationActivity.show(context, conversation.conversationId, startParam = startParam) + closeSourceWebActivityIfNeeded(context) return@launch } } diff --git a/app/src/main/java/one/mixin/android/ui/conversation/link/LinkBottomSheetDialogFragment.kt b/app/src/main/java/one/mixin/android/ui/conversation/link/LinkBottomSheetDialogFragment.kt index 304cd26836..bb4a8411dc 100644 --- a/app/src/main/java/one/mixin/android/ui/conversation/link/LinkBottomSheetDialogFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/conversation/link/LinkBottomSheetDialogFragment.kt @@ -255,6 +255,7 @@ class LinkBottomSheetDialogFragment : SchemeBottomSheet() { } AnalyticsTracker.trackOpenBotHomePage(AnalyticsTracker.BotSource.SCHEME, app.appNumber) WebActivity.show(requireActivity(), url, null, app) + closeSourceWebActivityIfNeeded() } else { showUserBottom(parentFragmentManager, user, botEntrySource = AnalyticsTracker.BotSource.SCHEME) } @@ -498,6 +499,7 @@ class LinkBottomSheetDialogFragment : SchemeBottomSheet() { if (hash != null) { jobManager.addJobInBackground(SyncOutputJob()) InscriptionActivity.show(requireContext(), hash) + closeSourceWebActivityIfNeeded() dismiss() } else { showError() @@ -523,7 +525,10 @@ class LinkBottomSheetDialogFragment : SchemeBottomSheet() { if (found != null) { oldLinkViewModel.refreshConversation(response.conversationId) toast(R.string.group_already_in) - context?.let { ConversationActivity.show(it, response.conversationId) } + context?.let { + ConversationActivity.show(it, response.conversationId) + closeSourceWebActivityIfNeeded() + } dismiss() } else { val avatarUserIds = mutableListOf() @@ -738,6 +743,7 @@ class LinkBottomSheetDialogFragment : SchemeBottomSheet() { Destination.Market, AnalyticsTracker.MarketSource.SCHEMA, ) + closeSourceWebActivityIfNeeded() dismiss() } } @@ -880,6 +886,7 @@ class LinkBottomSheetDialogFragment : SchemeBottomSheet() { } else -> { ConversationActivity.show(requireContext(), conversationId, userId, startParam = startParam) + closeSourceWebActivityIfNeeded() dismiss() } } @@ -887,6 +894,7 @@ class LinkBottomSheetDialogFragment : SchemeBottomSheet() { val conversation = oldLinkViewModel.getAndSyncConversation(conversationId) if (conversation != null) { ConversationActivity.show(requireContext(), conversation.conversationId, startParam = startParam) + closeSourceWebActivityIfNeeded() dismiss() } else { showError(R.string.Conversation_not_found) @@ -923,6 +931,7 @@ class LinkBottomSheetDialogFragment : SchemeBottomSheet() { } } else if (url.startsWith(Scheme.HTTPS_BUY, true) || url.startsWith(Scheme.MIXIN_BUY, true) || url.startsWith(Scheme.BUY, true)) { MainActivity.showWallet(requireContext(), buy = true) + closeSourceWebActivityIfNeeded() dismiss() } else if (url.startsWith(Scheme.TIP, true)) { val uri = Uri.parse(url) From b6e0c6114d9da47cf6f16c6fec5ad29ad36d682c Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Thu, 2 Jul 2026 20:23:45 +0800 Subject: [PATCH 2/2] fix(web): route mixin close handling through redirects --- .../mixin/android/extension/UrlExtension.kt | 26 ++++++--- .../link/LinkBottomSheetDialogFragment.kt | 24 +++++--- .../one/mixin/android/ui/web/WebFragment.kt | 40 +++++++++++++ .../android/ui/web/WebFragmentRedirectTest.kt | 58 +++++++++++++++++++ 4 files changed, 132 insertions(+), 16 deletions(-) create mode 100644 app/src/test/java/one/mixin/android/ui/web/WebFragmentRedirectTest.kt diff --git a/app/src/main/java/one/mixin/android/extension/UrlExtension.kt b/app/src/main/java/one/mixin/android/extension/UrlExtension.kt index 01332f6767..6315f0fdb6 100644 --- a/app/src/main/java/one/mixin/android/extension/UrlExtension.kt +++ b/app/src/main/java/one/mixin/android/extension/UrlExtension.kt @@ -151,6 +151,7 @@ fun String.openAsUrl( currentConversation: String? = null, app: App? = null, host: String? = null, + closeSourceOnOpenPage: Boolean = false, extraAction: () -> Unit, ) { if (startsWith(Constants.Scheme.SEND, true) || startsWith(Constants.Scheme.MIXIN_SEND, true) || startsWith(Constants.Scheme.HTTPS_SEND, true)) { @@ -204,16 +205,16 @@ BTC Address: ${Web3Signer.btcAddress} } else if (startsWith(Constants.Scheme.DEVICE, true)) { ConfirmBottomFragment.show(MixinApplication.appContext, supportFragmentManager, this) } else if (isUserScheme() || isAppScheme()) { - checkUserOrApp(context, supportFragmentManager, scope) + checkUserOrApp(context, supportFragmentManager, scope, closeSourceOnOpenPage) } else if (isTradeScheme()) { checkTradeUrl(context, supportFragmentManager, scope) } else if (isMembershipScheme()) { MixinMemberUpgradeBottomSheetDialogFragment.newInstance(Session.getAccount()?.membership?.plan).showNow(supportFragmentManager, MixinMemberUpgradeBottomSheetDialogFragment.TAG) } else if (isConversationScheme()) { - checkConversation(context, scope) { + checkConversation(context, scope, closeSourceOnOpenPage) { if (isMixinUrl() || isExternalScheme(context) || isExternalTransferUrl() || isLightningUrl()) { - LinkBottomSheetDialogFragment.newInstance(this) + LinkBottomSheetDialogFragment.newInstance(this, closeSourceOnOpenPage = closeSourceOnOpenPage) .showNow(supportFragmentManager, LinkBottomSheetDialogFragment.TAG) } else { extraAction() @@ -221,7 +222,7 @@ BTC Address: ${Web3Signer.btcAddress} } } else { if (isMixinUrl() || isExternalScheme(context) || isExternalTransferUrl()) { - LinkBottomSheetDialogFragment.newInstance(this) + LinkBottomSheetDialogFragment.newInstance(this, closeSourceOnOpenPage = closeSourceOnOpenPage) .showNow(supportFragmentManager, LinkBottomSheetDialogFragment.TAG) } else { extraAction() @@ -238,12 +239,14 @@ fun Uri.checkUserOrApp( context: Context, supportFragmentManager: FragmentManager, scope: CoroutineScope, -) = this.toString().checkUserOrApp(context, supportFragmentManager, scope) + closeSourceOnOpenPage: Boolean = false, +) = this.toString().checkUserOrApp(context, supportFragmentManager, scope, closeSourceOnOpenPage) fun String.checkUserOrApp( context: Context, supportFragmentManager: FragmentManager, scope: CoroutineScope, + closeSourceOnOpenPage: Boolean = false, ) { val isAppScheme = isAppScheme() val ctx = MixinApplication.appContext @@ -264,7 +267,7 @@ fun String.checkUserOrApp( val identityNumber = Session.getAccount()?.identityNumber if (identityNumber.isNullOrBlank()) { - val bottomSheet = LinkBottomSheetDialogFragment.newInstance(uri.toString()) + val bottomSheet = LinkBottomSheetDialogFragment.newInstance(uri.toString(), closeSourceOnOpenPage = closeSourceOnOpenPage) bottomSheet.showNow(supportFragmentManager, LinkBottomSheetDialogFragment.TAG) return } @@ -274,7 +277,7 @@ fun String.checkUserOrApp( scope.launch { val user = userDao.suspendFindUserById(userId) if (user == null) { - val bottomSheet = LinkBottomSheetDialogFragment.newInstance(uri.toString()) + val bottomSheet = LinkBottomSheetDialogFragment.newInstance(uri.toString(), closeSourceOnOpenPage = closeSourceOnOpenPage) bottomSheet.showNow(supportFragmentManager, LinkBottomSheetDialogFragment.TAG) } else { val isOpenApp = isAppScheme && uri.getQueryParameter("action") == "open" @@ -289,7 +292,9 @@ fun String.checkUserOrApp( } AnalyticsTracker.trackOpenBotHomePage(AnalyticsTracker.BotSource.SCHEME, app.appNumber) WebActivity.show(context, url, null, app) - closeSourceWebActivityIfNeeded(context) + if (closeSourceOnOpenPage) { + closeSourceWebActivityIfNeeded(context) + } if (context is UrlInterpreterActivity) { context.finish() } @@ -304,6 +309,7 @@ fun String.checkUserOrApp( fun String.checkConversation( context: Context, scope: CoroutineScope, + closeSourceOnOpenPage: Boolean = false, elseAction: () -> Unit, ) { val uri = Uri.parse(this) @@ -326,7 +332,9 @@ fun String.checkConversation( val conversation = conversationDao.getConversationByIdSuspend(conversationId) if (conversation != null) { ConversationActivity.show(context, conversation.conversationId, startParam = startParam) - closeSourceWebActivityIfNeeded(context) + if (closeSourceOnOpenPage) { + closeSourceWebActivityIfNeeded(context) + } return@launch } } diff --git a/app/src/main/java/one/mixin/android/ui/conversation/link/LinkBottomSheetDialogFragment.kt b/app/src/main/java/one/mixin/android/ui/conversation/link/LinkBottomSheetDialogFragment.kt index bb4a8411dc..352167a9f4 100644 --- a/app/src/main/java/one/mixin/android/ui/conversation/link/LinkBottomSheetDialogFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/conversation/link/LinkBottomSheetDialogFragment.kt @@ -140,6 +140,7 @@ class LinkBottomSheetDialogFragment : SchemeBottomSheet() { const val TAG = "LinkBottomSheetDialogFragment" const val CODE = "code" const val FROM = "from" + const val CLOSE_SOURCE_ON_OPEN_PAGE = "close_source_on_open_page" const val FROM_EXTERNAL = 0 const val FROM_INTERNAL = 1 @@ -148,10 +149,12 @@ class LinkBottomSheetDialogFragment : SchemeBottomSheet() { fun newInstance( code: String, from: Int = FROM_INTERNAL, + closeSourceOnOpenPage: Boolean = false, ) = LinkBottomSheetDialogFragment().withArgs { putString(CODE, code) putInt(FROM, from) + putBoolean(CLOSE_SOURCE_ON_OPEN_PAGE, closeSourceOnOpenPage) } } @@ -176,6 +179,7 @@ class LinkBottomSheetDialogFragment : SchemeBottomSheet() { private lateinit var url: String private val from: Int by lazy { requireArguments().getInt(FROM, FROM_EXTERNAL) } + private val closeSourceOnOpenPage: Boolean by lazy { requireArguments().getBoolean(CLOSE_SOURCE_ON_OPEN_PAGE, false) } private val newSchemeParser: NewSchemeParser by lazy { NewSchemeParser(this, linkViewModel) } @@ -255,7 +259,7 @@ class LinkBottomSheetDialogFragment : SchemeBottomSheet() { } AnalyticsTracker.trackOpenBotHomePage(AnalyticsTracker.BotSource.SCHEME, app.appNumber) WebActivity.show(requireActivity(), url, null, app) - closeSourceWebActivityIfNeeded() + closeSourceWebActivityOnOpenPage() } else { showUserBottom(parentFragmentManager, user, botEntrySource = AnalyticsTracker.BotSource.SCHEME) } @@ -499,7 +503,7 @@ class LinkBottomSheetDialogFragment : SchemeBottomSheet() { if (hash != null) { jobManager.addJobInBackground(SyncOutputJob()) InscriptionActivity.show(requireContext(), hash) - closeSourceWebActivityIfNeeded() + closeSourceWebActivityOnOpenPage() dismiss() } else { showError() @@ -527,7 +531,7 @@ class LinkBottomSheetDialogFragment : SchemeBottomSheet() { toast(R.string.group_already_in) context?.let { ConversationActivity.show(it, response.conversationId) - closeSourceWebActivityIfNeeded() + closeSourceWebActivityOnOpenPage() } dismiss() } else { @@ -743,7 +747,7 @@ class LinkBottomSheetDialogFragment : SchemeBottomSheet() { Destination.Market, AnalyticsTracker.MarketSource.SCHEMA, ) - closeSourceWebActivityIfNeeded() + closeSourceWebActivityOnOpenPage() dismiss() } } @@ -886,7 +890,7 @@ class LinkBottomSheetDialogFragment : SchemeBottomSheet() { } else -> { ConversationActivity.show(requireContext(), conversationId, userId, startParam = startParam) - closeSourceWebActivityIfNeeded() + closeSourceWebActivityOnOpenPage() dismiss() } } @@ -894,7 +898,7 @@ class LinkBottomSheetDialogFragment : SchemeBottomSheet() { val conversation = oldLinkViewModel.getAndSyncConversation(conversationId) if (conversation != null) { ConversationActivity.show(requireContext(), conversation.conversationId, startParam = startParam) - closeSourceWebActivityIfNeeded() + closeSourceWebActivityOnOpenPage() dismiss() } else { showError(R.string.Conversation_not_found) @@ -931,7 +935,7 @@ class LinkBottomSheetDialogFragment : SchemeBottomSheet() { } } else if (url.startsWith(Scheme.HTTPS_BUY, true) || url.startsWith(Scheme.MIXIN_BUY, true) || url.startsWith(Scheme.BUY, true)) { MainActivity.showWallet(requireContext(), buy = true) - closeSourceWebActivityIfNeeded() + closeSourceWebActivityOnOpenPage() dismiss() } else if (url.startsWith(Scheme.TIP, true)) { val uri = Uri.parse(url) @@ -1184,6 +1188,12 @@ class LinkBottomSheetDialogFragment : SchemeBottomSheet() { dismiss() } + private fun closeSourceWebActivityOnOpenPage() { + if (closeSourceOnOpenPage) { + closeSourceWebActivityIfNeeded() + } + } + private fun closeSourceWebActivityIfNeeded() { (activity as? WebActivity)?.finish() } diff --git a/app/src/main/java/one/mixin/android/ui/web/WebFragment.kt b/app/src/main/java/one/mixin/android/ui/web/WebFragment.kt index 3ac4c5b1f7..ded4702e24 100644 --- a/app/src/main/java/one/mixin/android/ui/web/WebFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/web/WebFragment.kt @@ -193,9 +193,48 @@ import timber.log.Timber import java.io.ByteArrayInputStream import java.net.URI import java.net.URISyntaxException +import java.net.URLDecoder +import java.nio.charset.StandardCharsets import java.util.Locale import javax.inject.Inject +internal fun String.shouldCloseWebContainerForMixinRedirect(): Boolean = + when { + startsWith(Constants.Scheme.MIXIN_MARKET, true) || + startsWith(Constants.Scheme.HTTPS_MARKET, true) || + startsWith(Constants.Scheme.MIXIN_SWAP, true) || + startsWith(Constants.Scheme.HTTPS_SWAP, true) || + startsWith(Constants.Scheme.MIXIN_TRADE, true) || + startsWith(Constants.Scheme.HTTPS_TRADE, true) || + startsWith(Constants.Scheme.CONVERSATIONS, true) || + startsWith(Constants.Scheme.MIXIN_BUY, true) || + startsWith(Constants.Scheme.HTTPS_BUY, true) || + startsWith(Constants.Scheme.BUY, true) || + startsWith(Constants.Scheme.HTTPS_INSCRIPTION, true) || + startsWith(Constants.Scheme.CODES, true) || + startsWith(Constants.Scheme.HTTPS_CODES, true) || + startsWith(Constants.Scheme.MIXIN_SCHEME, true) || + startsWith(Constants.Scheme.HTTPS_SCHEME, true) -> true + startsWith(Constants.Scheme.APPS, true) || + startsWith(Constants.Scheme.HTTPS_APPS, true) -> queryParameter("action") == "open" + else -> false + } + +private fun String.queryParameter(name: String): String? = + runCatching { URI(this).rawQuery } + .getOrNull() + ?.split("&") + ?.firstNotNullOfOrNull { parameter -> + val parts = parameter.split("=", limit = 2) + val key = parts.getOrNull(0)?.urlDecode() + val value = parts.getOrNull(1)?.urlDecode() + if (key == name) value else null + } + +private fun String.urlDecode(): String = + runCatching { URLDecoder.decode(this, StandardCharsets.UTF_8.name()) } + .getOrDefault(this) + @AndroidEntryPoint class WebFragment : BaseFragment() { companion object { @@ -1903,6 +1942,7 @@ class WebFragment : BaseFragment() { scope, host = host, currentConversation = conversationId, + closeSourceOnOpenPage = request.isForMainFrame && url.shouldCloseWebContainerForMixinRedirect(), ) {} return true } diff --git a/app/src/test/java/one/mixin/android/ui/web/WebFragmentRedirectTest.kt b/app/src/test/java/one/mixin/android/ui/web/WebFragmentRedirectTest.kt new file mode 100644 index 0000000000..8113f1f13c --- /dev/null +++ b/app/src/test/java/one/mixin/android/ui/web/WebFragmentRedirectTest.kt @@ -0,0 +1,58 @@ +package one.mixin.android.ui.web + +import one.mixin.android.Constants +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class WebFragmentRedirectTest { + @Test + fun closesWebContainerForPageOpeningMixinRedirects() { + listOf( + "${Constants.Scheme.MIXIN_MARKET}/btc", + "${Constants.Scheme.HTTPS_MARKET}/btc", + "${Constants.Scheme.MIXIN_TRADE}?input=btc", + "${Constants.Scheme.HTTPS_TRADE}?input=btc", + "${Constants.Scheme.MIXIN_SWAP}?input=btc", + "${Constants.Scheme.HTTPS_SWAP}?input=btc", + "${Constants.Scheme.CONVERSATIONS}/a465ffdb-4441-4cb9-8b45-00cf79dfbc46", + "${Constants.Scheme.MIXIN_BUY}", + "${Constants.Scheme.HTTPS_BUY}", + "${Constants.Scheme.HTTPS_INSCRIPTION}/hash", + "${Constants.Scheme.APPS}/a465ffdb-4441-4cb9-8b45-00cf79dfbc46?action=open", + "${Constants.Scheme.HTTPS_APPS}/a465ffdb-4441-4cb9-8b45-00cf79dfbc46?action=open", + ).forEach { url -> + assertTrue(url, url.shouldCloseWebContainerForMixinRedirect()) + } + } + + @Test + fun keepsWebContainerForBottomSheetMixinRedirects() { + listOf( + "${Constants.Scheme.USERS}/a465ffdb-4441-4cb9-8b45-00cf79dfbc46", + "${Constants.Scheme.HTTPS_USERS}/a465ffdb-4441-4cb9-8b45-00cf79dfbc46", + "${Constants.Scheme.PAY}/a465ffdb-4441-4cb9-8b45-00cf79dfbc46", + "${Constants.Scheme.MIXIN_PAY}a465ffdb-4441-4cb9-8b45-00cf79dfbc46", + "${Constants.Scheme.HTTPS_PAY}/a465ffdb-4441-4cb9-8b45-00cf79dfbc46", + "${Constants.Scheme.ADDRESS}/a465ffdb-4441-4cb9-8b45-00cf79dfbc46", + "${Constants.Scheme.HTTPS_ADDRESS}/a465ffdb-4441-4cb9-8b45-00cf79dfbc46", + "${Constants.Scheme.MIXIN_REFERRALS}/abc", + "${Constants.Scheme.HTTPS_REFERRALS}/abc", + Constants.Scheme.MIXIN_WC, + "${Constants.Scheme.WALLET_CONNECT_PREFIX}topic@2", + ).forEach { url -> + assertFalse(url, url.shouldCloseWebContainerForMixinRedirect()) + } + } + + @Test + fun keepsWebContainerForAppLinksWithoutOpenAction() { + listOf( + "${Constants.Scheme.APPS}/a465ffdb-4441-4cb9-8b45-00cf79dfbc46", + "${Constants.Scheme.HTTPS_APPS}/a465ffdb-4441-4cb9-8b45-00cf79dfbc46", + "${Constants.Scheme.APPS}/a465ffdb-4441-4cb9-8b45-00cf79dfbc46?action=auth", + ).forEach { url -> + assertFalse(url, url.shouldCloseWebContainerForMixinRedirect()) + } + } +}