Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions app/src/main/java/one/mixin/android/extension/UrlExtension.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -204,24 +205,24 @@ 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()
}
}
} else {
if (isMixinUrl() || isExternalScheme(context) || isExternalTransferUrl()) {
LinkBottomSheetDialogFragment.newInstance(this)
LinkBottomSheetDialogFragment.newInstance(this, closeSourceOnOpenPage = closeSourceOnOpenPage)
.showNow(supportFragmentManager, LinkBottomSheetDialogFragment.TAG)
} else {
extraAction()
Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -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"
Expand All @@ -289,6 +292,9 @@ fun String.checkUserOrApp(
}
AnalyticsTracker.trackOpenBotHomePage(AnalyticsTracker.BotSource.SCHEME, app.appNumber)
WebActivity.show(context, url, null, app)
if (closeSourceOnOpenPage) {
closeSourceWebActivityIfNeeded(context)
}
Comment on lines +295 to +297
if (context is UrlInterpreterActivity) {
context.finish()
}
Expand All @@ -303,6 +309,7 @@ fun String.checkUserOrApp(
fun String.checkConversation(
context: Context,
scope: CoroutineScope,
closeSourceOnOpenPage: Boolean = false,
elseAction: () -> Unit,
) {
val uri = Uri.parse(this)
Expand All @@ -325,6 +332,9 @@ fun String.checkConversation(
val conversation = conversationDao.getConversationByIdSuspend(conversationId)
if (conversation != null) {
ConversationActivity.show(context, conversation.conversationId, startParam = startParam)
if (closeSourceOnOpenPage) {
closeSourceWebActivityIfNeeded(context)
}
return@launch
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
}

Expand All @@ -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) }

Expand Down Expand Up @@ -255,6 +259,7 @@ class LinkBottomSheetDialogFragment : SchemeBottomSheet() {
}
AnalyticsTracker.trackOpenBotHomePage(AnalyticsTracker.BotSource.SCHEME, app.appNumber)
WebActivity.show(requireActivity(), url, null, app)
closeSourceWebActivityOnOpenPage()
} else {
showUserBottom(parentFragmentManager, user, botEntrySource = AnalyticsTracker.BotSource.SCHEME)
}
Expand Down Expand Up @@ -498,6 +503,7 @@ class LinkBottomSheetDialogFragment : SchemeBottomSheet() {
if (hash != null) {
jobManager.addJobInBackground(SyncOutputJob())
InscriptionActivity.show(requireContext(), hash)
closeSourceWebActivityOnOpenPage()
dismiss()
} else {
showError()
Expand All @@ -523,7 +529,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)
closeSourceWebActivityOnOpenPage()
}
dismiss()
} else {
val avatarUserIds = mutableListOf<String>()
Expand Down Expand Up @@ -738,6 +747,7 @@ class LinkBottomSheetDialogFragment : SchemeBottomSheet() {
Destination.Market,
AnalyticsTracker.MarketSource.SCHEMA,
)
closeSourceWebActivityOnOpenPage()
dismiss()
}
}
Expand Down Expand Up @@ -880,13 +890,15 @@ class LinkBottomSheetDialogFragment : SchemeBottomSheet() {
}
else -> {
ConversationActivity.show(requireContext(), conversationId, userId, startParam = startParam)
closeSourceWebActivityOnOpenPage()
dismiss()
}
}
} else {
val conversation = oldLinkViewModel.getAndSyncConversation(conversationId)
if (conversation != null) {
ConversationActivity.show(requireContext(), conversation.conversationId, startParam = startParam)
closeSourceWebActivityOnOpenPage()
dismiss()
} else {
showError(R.string.Conversation_not_found)
Expand Down Expand Up @@ -923,6 +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)
closeSourceWebActivityOnOpenPage()
dismiss()
} else if (url.startsWith(Scheme.TIP, true)) {
val uri = Uri.parse(url)
Expand Down Expand Up @@ -1175,6 +1188,12 @@ class LinkBottomSheetDialogFragment : SchemeBottomSheet() {
dismiss()
}

private fun closeSourceWebActivityOnOpenPage() {
if (closeSourceOnOpenPage) {
closeSourceWebActivityIfNeeded()
}
}

private fun closeSourceWebActivityIfNeeded() {
(activity as? WebActivity)?.finish()
}
Expand Down
40 changes: 40 additions & 0 deletions app/src/main/java/one/mixin/android/ui/web/WebFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -1903,6 +1942,7 @@ class WebFragment : BaseFragment() {
scope,
host = host,
currentConversation = conversationId,
closeSourceOnOpenPage = request.isForMainFrame && url.shouldCloseWebContainerForMixinRedirect(),
) {}
return true
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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())
}
}
}