commit 8c2a1ee1f707dd80aeb1f6c12e56c84263f6b9bd parent 5798f0f05fae4e218f6dd8ad5ca1259a36169301 Author: Roger Yang <royang@mozilla.com> Date: Mon, 17 Nov 2025 19:42:54 +0000 Bug 1979576 - If URL is updated then dismiss AppLinks redirect prompt. r=android-reviewers,tthibaud Differential Revision: https://phabricator.services.mozilla.com/D262998 Diffstat:
4 files changed, 44 insertions(+), 9 deletions(-)
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksFeature.kt b/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksFeature.kt @@ -80,19 +80,34 @@ class AppLinksFeature( override fun start() { scope = store.flowScoped { flow -> flow.mapNotNull { state -> state.findTabOrCustomTabOrSelectedTab(sessionId) } - .distinctUntilChangedBy { - it.content.appIntent + // monitor either appIntent OR url changes + .distinctUntilChangedBy { session -> + val content = session.content + content.appIntent to content.url } .collect { sessionState -> - sessionState.content.appIntent?.let { + val content = sessionState.content + val intent = content.appIntent + val url = content.url + + if (intent != null) { handleAppIntent( sessionState = sessionState, - url = it.url, - appIntent = it.appIntent, - fallbackUrl = it.fallbackUrl, - appName = it.appName, + url = intent.url, + appIntent = intent.appIntent, + fallbackUrl = intent.fallbackUrl, + appName = intent.appName, ) + + // Clear the consumed app intent so we don't re-handle it store.dispatch(ContentAction.ConsumeAppIntentAction(sessionState.id)) + } else { + // No appIntent present, but url changed, remove the external application prompt + findPreviousDialogFragment()?.let { + if (it.triggerUrl != url) { + fragmentManager?.beginTransaction()?.remove(it)?.commit() + } + } } } } @@ -198,7 +213,13 @@ class AppLinksFeature( return } - getOrCreateDialog(isPrivate, isWallet, url, appName).apply { + getOrCreateDialog( + isPrivate = isPrivate, + isWallet = isWallet, + triggerUrl = sessionState.content.url, + url = url, + targetAppName = appName, + ).apply { onConfirmRedirect = { isCheckboxTicked -> if (isCheckboxTicked) { alwaysOpenCheckboxAction?.invoke() @@ -215,6 +236,7 @@ class AppLinksFeature( internal fun getOrCreateDialog( isPrivate: Boolean, isWallet: Boolean, + triggerUrl: String?, url: String, targetAppName: String?, ): RedirectDialogFragment { @@ -254,6 +276,7 @@ class AppLinksFeature( dialogMessageString = dialogMessage, showCheckbox = if (isPrivate || isWallet) false else alwaysOpenCheckboxAction != null, maxSuccessiveDialogMillisLimit = MAX_SUCCESSIVE_DIALOG_MILLIS_LIMIT, + triggerUrl = triggerUrl, ) } diff --git a/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/RedirectDialogFragment.kt b/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/RedirectDialogFragment.kt @@ -13,6 +13,10 @@ import androidx.fragment.app.DialogFragment * Be mindful to call [onConfirmRedirect] when you want to open the linked app. */ abstract class RedirectDialogFragment : DialogFragment() { + /** + * The URL that triggered the dialog. + */ + open val triggerUrl: String? = null /** * A callback to trigger a redirect action. Call it when you are ready to open the linked app. For instance, diff --git a/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/SimpleRedirectDialogFragment.kt b/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/SimpleRedirectDialogFragment.kt @@ -38,6 +38,9 @@ class SimpleRedirectDialogFragment( @VisibleForTesting internal var testingContext: Context? = null + override val triggerUrl: String? + get() = arguments?.getString(KEY_TRIGGER_URL) + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { fun getBuilder(themeID: Int): MaterialAlertDialogBuilder { val context = testingContext ?: requireContext() @@ -124,6 +127,7 @@ class SimpleRedirectDialogFragment( cancelable: Boolean = false, showCheckbox: Boolean = false, maxSuccessiveDialogMillisLimit: Int = TIME_SHOWN_OFFSET_MILLIS, + triggerUrl: String? = null, ): RedirectDialogFragment { val fragment = SimpleRedirectDialogFragment(maxSuccessiveDialogMillisLimit) val arguments = fragment.arguments ?: Bundle() @@ -146,6 +150,8 @@ class SimpleRedirectDialogFragment( putBoolean(KEY_CANCELABLE, cancelable) putBoolean(KEY_CHECKBOX, showCheckbox) + + putString(KEY_TRIGGER_URL, triggerUrl) } fragment.arguments = arguments @@ -170,6 +176,8 @@ class SimpleRedirectDialogFragment( private const val KEY_CHECKBOX = "KEY_CHECKBOX" + private const val KEY_TRIGGER_URL = "KEY_TRIGGER_URL" + private const val TIME_SHOWN_OFFSET_MILLIS = 1000 internal const val VIEW_ID = 111 diff --git a/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksFeatureTest.kt b/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksFeatureTest.kt @@ -380,7 +380,7 @@ class AppLinksFeatureTest { verify(mockDialog).showNow(eq(mockFragmentManager), anyString()) - doReturn(mockDialog).`when`(feature).getOrCreateDialog(false, false, "", null) + doReturn(mockDialog).`when`(feature).getOrCreateDialog(false, false, webUrl, "", null) doReturn(mockDialog).`when`(mockFragmentManager).findFragmentByTag(RedirectDialogFragment.FRAGMENT_TAG) feature.handleAppIntent(tab, intentUrl, mock(), null, null) verify(mockDialog, times(1)).showNow(mockFragmentManager, RedirectDialogFragment.FRAGMENT_TAG)