Skip to content
Draft
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
70 changes: 63 additions & 7 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2229,13 +2229,25 @@ class BrowserTabFragment :
}

is Command.HandleNonHttpAppLink -> {
openExternalDialog(
intent = it.nonHttpAppLink.intent,
fallbackUrl = it.nonHttpAppLink.fallbackUrl,
fallbackIntent = it.nonHttpAppLink.fallbackIntent,
useFirstActivityFound = false,
headers = it.headers,
)
if (isActiveCustomTab() && isLaunchedFromExternalApp) {
// For external Custom Tabs, non-HTTP(s) schemes are frequently used as auth callbacks
// back into the originating app. Prompting here can break the flow and cause the app
// to restart/loop the login process.
launchNonHttpAppLinkFromExternalCustomTab(
intent = it.nonHttpAppLink.intent,
fallbackUrl = it.nonHttpAppLink.fallbackUrl,
fallbackIntent = it.nonHttpAppLink.fallbackIntent,
headers = it.headers,
)
} else {
openExternalDialog(
intent = it.nonHttpAppLink.intent,
fallbackUrl = it.nonHttpAppLink.fallbackUrl,
fallbackIntent = it.nonHttpAppLink.fallbackIntent,
useFirstActivityFound = false,
headers = it.headers,
)
}
}

is Command.ExtractUrlFromCloakedAmpLink -> {
Expand Down Expand Up @@ -2751,6 +2763,50 @@ class BrowserTabFragment :
}
}

private fun launchNonHttpAppLinkFromExternalCustomTab(
intent: Intent,
fallbackUrl: String? = null,
fallbackIntent: Intent? = null,
headers: Map<String, String> = emptyMap(),
) {
// Only act if the fragment is still active; avoids launching from background tabs.
if (!isActiveCustomTab()) return

context?.let { context ->
val pm = context.packageManager
val activities = pm.queryIntentActivities(intent, 0)

when {
activities.isEmpty() -> {
when {
fallbackIntent != null -> {
runCatching { context.startActivity(fallbackIntent) }
.onFailure { showToast(R.string.unableToOpenLink) }
}

fallbackUrl != null -> {
webView?.loadUrl(fallbackUrl, headers)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Missing deferred URL load for new tab scenario

When loading fallbackUrl in launchNonHttpAppLinkFromExternalCustomTab, the code directly calls webView?.loadUrl(fallbackUrl, headers). The original openExternalDialog function checks viewModel.linkOpenedInNewTab() and uses webView.post to defer the load when true, ensuring the WebView is properly attached. If a new tab is opened within an external Custom Tab (via window.open or target="_blank"), isLinkOpenedInNewTab would be true, but this new function skips the deferred loading pattern. This could cause a race condition where the URL load is attempted before the WebView is ready.


Please tell me if this was useful or not with a 👍 or 👎.

Fix in Cursor Fix in Web

}

else -> showToast(R.string.unableToOpenLink)
}
}

activities.size == 1 -> {
runCatching { context.startActivity(intent) }
.onFailure { showToast(R.string.unableToOpenLink) }
}

else -> {
val title = getString(R.string.openExternalApp)
val chooser = Intent.createChooser(intent, title)
runCatching { context.startActivity(chooser) }
.onFailure { showToast(R.string.unableToOpenLink) }
}
}
}
}

private fun launchDialogForIntent(
context: Context,
pm: PackageManager,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,19 @@ class CustomTabActivity : DuckDuckGoActivity() {
}

companion object {
fun intent(context: Context, flags: Int, text: String?, toolbarColor: Int, isExternal: Boolean): Intent {
return Intent(context, CustomTabActivity::class.java).apply {
fun intent(
context: Context,
originalIntent: Intent?,
flags: Int,
text: String?,
toolbarColor: Int,
isExternal: Boolean,
): Intent {
// Preserve Custom Tabs extras from the original caller intent (e.g., session binder),
// while still routing to our CustomTabActivity implementation.
val base = originalIntent?.let { Intent(it) } ?: Intent()
return base.apply {
setClass(context, CustomTabActivity::class.java)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Copied intent preserves original flags causing unexpected behavior

When creating the Custom Tab intent, Intent(it) copies all fields from the original intent including its flags, and then addFlags(flags) ORs the new flags with the existing ones. The comment says "preserve Custom Tabs extras" but this approach also preserves the original intent's flags. If the calling app set any conflicting flags (like FLAG_ACTIVITY_SINGLE_TOP, FLAG_ACTIVITY_BROUGHT_TO_FRONT, etc.), they would combine with FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK | FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS and could cause unexpected activity launch behavior. If only extras need to be preserved, the flags portion should be explicitly set rather than added.


Please tell me if this was useful or not with a 👍 or 👎.

Fix in Cursor Fix in Web

addFlags(flags)
putExtra(CustomTabsIntent.EXTRA_TOOLBAR_COLOR, toolbarColor)
putExtra(Intent.EXTRA_TEXT, text)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ class IntentDispatcherActivity : DuckDuckGoActivity() {
startActivity(
CustomTabActivity.intent(
context = this,
flags = Intent.FLAG_ACTIVITY_NEW_TASK and Intent.FLAG_ACTIVITY_CLEAR_TASK and Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS,
originalIntent = intent,
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS,
text = intentText,
toolbarColor = toolbarColor,
isExternal = isExternal,
Expand Down
Loading