tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit af3d9ccb7baf7c153ccd117d8566ef015f49237f
parent 9e946915e46ae82fe0d609c763e117d9e40d0d0d
Author: Roger Yang <royang@mozilla.com>
Date:   Wed,  5 Nov 2025 19:39:54 +0000

Bug 1991601 - Use fallback url to app links only if original URL is not supported by the engine. r=android-reviewers,tthibaud

Differential Revision: https://phabricator.services.mozilla.com/D267498

Diffstat:
Mmobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksUseCases.kt | 28+++++++++++++++++++---------
Mmobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksUseCasesTest.kt | 29+++++++++++++++++++++--------
Mmobile/android/fenix/app/src/androidTest/assets/pages/appLinksLinks.html | 26++++++++++++++++++++++++++
Mmobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/AppLinksTest.kt | 167++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
4 files changed, 228 insertions(+), 22 deletions(-)

diff --git a/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksUseCases.kt b/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksUseCases.kt @@ -112,17 +112,11 @@ class AppLinksUseCases( getAppNameFromResolveInfo(context, resolveInfo) } ?: "" - // Only set fallback URL if url is not a Google PlayStore URL - // The reason here is we already handled that case with the market place URL - val fallbackUrl: String? = redirectData.fallbackUrl?.takeIf { url -> - !isPlayStoreURL(url) || redirectData.resolveInfo == null - } - val appIntent = when { redirectData.resolveInfo == null -> null isBrowserRedirect && isEngineSupportedScheme -> null includeHttpAppLinks && isAppIntentHttpOrHttps -> redirectData.appIntent - !launchInApp() && (isEngineSupportedScheme || fallbackUrl != null) -> null + !launchInApp() && (isEngineSupportedScheme || redirectData.fallbackUrl != null) -> null else -> redirectData.appIntent } @@ -130,7 +124,7 @@ class AppLinksUseCases( val appLinkRedirect = AppLinkRedirect( appIntent = appIntent, appName = appName, - fallbackUrl = fallbackUrl, + fallbackUrl = redirectData.fallbackUrl, marketplaceIntent = redirectData.marketplaceIntent, ) @@ -198,9 +192,25 @@ class AppLinksUseCases( } } + /** + * Determines the fallback URL to use when attempting to redirect to an external app. + * + * The fallback URL is taken from the intent's `EXTRA_BROWSER_FALLBACK_URL` only if: + * - The original URL scheme is not supported by the engine, AND + * - The provided fallback URL is not a Google Play Store URL OR application is not + * installed. (Handled by marketplace intent) + */ + val fallbackUrl = appIntent?.getStringExtra(EXTRA_BROWSER_FALLBACK_URL)?.takeIf { + val schemeEngineSupported = url.toUri().scheme in ENGINE_SUPPORTED_SCHEMES + val appInstalled = resolveInfo != null + + val isPlayStoreUrlForInstalledApp = isPlayStoreURL(it) && appInstalled + !schemeEngineSupported && !isPlayStoreUrlForInstalledApp + } + return RedirectData( appIntent = appIntent, - fallbackUrl = null, + fallbackUrl = fallbackUrl, marketplaceIntent = marketplaceIntent, resolveInfo = resolveInfo, ) diff --git a/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksUseCasesTest.kt b/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksUseCasesTest.kt @@ -58,9 +58,11 @@ class AppLinksUseCasesTest { private val appIntentWithPackageAndPlayStoreFallback = "intent://com.example.app#Intent;package=com.example.com;S.browser_fallback_url=https://play.google.com/store/abc;end" private val urlWithAndroidFallbackLink = - "https://example.com/?afl=https://example.com" + "https://mozilla.org/?afl=https://example.com" private val urlWithFallbackLink = - "https://example.com/?link=https://example.com" + "https://mozilla.org/?link=https://example.com" + private val urlWithBrowserFallbackLink = + "https://mozilla.org/?S.browser_fallback_url=https://example.com" @Before fun setup() { @@ -154,7 +156,7 @@ class AppLinksUseCasesTest { val redirect = subject.interceptedAppLinkRedirect(appIntentWithPackageAndFallback) assertFalse(redirect.hasMarketplaceIntent()) - assertFalse(redirect.hasFallback()) + assertTrue(redirect.hasFallback()) } @Test @@ -165,7 +167,7 @@ class AppLinksUseCasesTest { val redirect = subject.interceptedAppLinkRedirect(appIntentWithPackageAndFallback) assertFalse(redirect.hasExternalApp()) assertTrue(redirect.hasMarketplaceIntent()) - assertFalse(redirect.hasFallback()) + assertTrue(redirect.hasFallback()) } @Test @@ -348,7 +350,7 @@ class AppLinksUseCasesTest { val redirect = subject.interceptedAppLinkRedirect(uri) assertFalse(redirect.hasExternalApp()) - assertFalse(redirect.hasFallback()) + assertTrue(redirect.hasFallback()) } @Test @@ -672,11 +674,12 @@ class AppLinksUseCasesTest { val subject = AppLinksUseCases(context, { false }) val redirect = subject.interceptedAppLinkRedirect(appIntentWithPackageAndPlayStoreFallback) assertFalse(redirect.hasExternalApp()) - assertFalse(redirect.hasFallback()) + assertTrue(redirect.hasFallback()) + assertEquals("https://play.google.com/store/abc", redirect.fallbackUrl) } @Test - fun `WHEN A intent WITH android fallback link THEN fallback should NOT be used`() { + fun `GIVEN an url with android fallback link WHEN scheme is supported by the engine THEN fallback should not be used`() { val context = createContext() val subject = AppLinksUseCases(context, { true }) @@ -686,7 +689,7 @@ class AppLinksUseCasesTest { } @Test - fun `WHEN A intent WITH fallback link THEN fallback should NOT be used`() { + fun `GIVEN an url with fallback link WHEN scheme is supported by the engine THEN fallback should not be used`() { val context = createContext() val subject = AppLinksUseCases(context, { true }) @@ -694,4 +697,14 @@ class AppLinksUseCasesTest { assertNull(redirect.fallbackUrl) assertFalse(redirect.hasFallback()) } + + @Test + fun `GIVEN an url with browser fallback link WHEN scheme is supported by the engine THEN fallback should not be used`() { + val context = createContext() + val subject = AppLinksUseCases(context, { true }) + + val redirect = subject.interceptedAppLinkRedirect(urlWithBrowserFallbackLink) + assertNull(redirect.fallbackUrl) + assertFalse(redirect.hasFallback()) + } } diff --git a/mobile/android/fenix/app/src/androidTest/assets/pages/appLinksLinks.html b/mobile/android/fenix/app/src/androidTest/assets/pages/appLinksLinks.html @@ -34,4 +34,30 @@ Telephone post navigation link </a> </section> + + <section> + <a + href="intent://1234567890#Intent;scheme=tel;S.browser_fallback_url=https://www.mozilla.org;end;" + > + Telephone with fallback URL + </a> + </section> + + <section> + <a href="https://mozilla.org/?afl=https://youtube.com"> + Link with android fallback link + </a> + </section> + + <section> + <a href="https://mozilla.org/?link=https://youtube.com"> + Link with fallback link + </a> + </section> + + <section> + <a href="https://mozilla.org/?S.browser_fallback_url=https://youtube.com"> + Link with browser fallback link + </a> + </section> </html> diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/AppLinksTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/AppLinksTest.kt @@ -36,7 +36,10 @@ class AppLinksTest : TestSetup() { private val phoneUrlLink = itemContainingText("Telephone link") private val formRedirectLink = itemContainingText("Telephone post navigation link") private val intentSchemeWithExampleAppLink = itemContainingText("Example app link") - + private val phoneWithFallbackLink = itemContainingText("Telephone with fallback URL") + private val linkWithAndroidFallbackLink = itemContainingText("Link with android fallback link") + private val linkWithFallbackLink = itemContainingText("Link with fallback link") + private val linkWithBrowserFallbackLink = itemContainingText("Link with browser fallback link") private val phoneSchemaLink = "tel://1234567890" @get:Rule @@ -56,6 +59,12 @@ class AppLinksTest : TestSetup() { externalLinksPage = mockWebServer.appLinksRedirectAsset } + /** + * User setting: Ask + * Tests that when opening a youtube:// scheme link under “Ask”, the app prompt appears. + * After tapping “Cancel”, the browser stays on the same page (no external app opened). + * vnd.youtube://@Mozilla + */ @Test fun askBeforeOpeningLinkInAppYoutubeSchemeCancelTest() { navigationToolbar { @@ -68,6 +77,12 @@ class AppLinksTest : TestSetup() { } } + /** + * User setting: Ask + * Clicking an intent:// link without corresponding app should not trigger the + * external-app prompt. The user stays on the same page. + * intent://com.example.app + */ @Test fun askBeforeOpeningLinkWithIntentSchemeTest() { navigationToolbar { @@ -79,6 +94,12 @@ class AppLinksTest : TestSetup() { } } + /** + * User setting: Ask + * After canceling once for youtube://, tapping the same link again in the same tab + * should not show the prompt again. The browser remains on the test page. + * vnd.youtube://@Mozilla + */ @Test fun askBeforeOpeningLinkInAppYoutubeSchemeCancelMultiTapTest() { navigationToolbar { @@ -98,6 +119,12 @@ class AppLinksTest : TestSetup() { } } + /** + * User setting: Ask + * Canceling a youtube:// link prompt affects only the current tab. + * In a new tab, the same link still shows the prompt. + * vnd.youtube://@Mozilla + */ @Test fun askBeforeOpeningLinkInAppYoutubeSchemeCancelOnlyAffectCurrentTabTest() { navigationToolbar { @@ -118,6 +145,12 @@ class AppLinksTest : TestSetup() { } } + /** + * User setting: Never + * For an https YouTube link, no external-app prompt is shown. + * The page loads directly in-browser (verify “youtube.com”). + * https://m.youtube.com/user/mozilla + */ @Test fun neverOpeningLinkInAppYoutubeTest() { composeTestRule.activityRule.applySettingsExceptions { @@ -133,6 +166,12 @@ class AppLinksTest : TestSetup() { } } + /** + * User setting: Never + * For a youtube:// scheme link, the app prompt still appears. + * After “Cancel”, the browser stays on the same page. + * vnd.youtube://@Mozilla + */ @Test fun neverOpeningLinkInAppYoutubeSchemeCancelTest() { composeTestRule.activityRule.applySettingsExceptions { @@ -149,6 +188,12 @@ class AppLinksTest : TestSetup() { } } + /** + * User setting: Ask + * When opening a normal YouTube link, the app prompt appears. + * Tapping “Cancel” opens the YouTube website in-browser. + * https://m.youtube.com/user/mozilla + */ @Test fun askBeforeOpeningLinkInAppYoutubeCancelTest() { navigationToolbar { @@ -161,27 +206,44 @@ class AppLinksTest : TestSetup() { } } + /** + * User setting: Ask + * Verifies that the “Open in Phone” prompt appears when tapping a tel: link. + * tel://1234567890 + */ @Test fun appLinksRedirectPhoneLinkPromptTest() { navigationToolbar { }.enterURLAndEnterToBrowser(externalLinksPage.url) { clickPageObject(phoneUrlLink) verifyOpenLinkInAnotherAppPrompt(appName = "Phone") - clickPageObject(itemWithResIdAndText("android:id/button2", "Cancel")) - mDevice.waitForIdle() - verifyUrl(externalLinksPage.url.toString()) } } + /** + * User setting: Ask + * Clicking a tel: link triggers the Phone prompt. + * Tapping “Cancel” keeps the user on the same page. + * tel://1234567890 + */ @Test fun askBeforeOpeningLinkInAppPhoneCancelTest() { navigationToolbar { }.enterURLAndEnterToBrowser(externalLinksPage.url) { clickPageObject(phoneUrlLink) verifyOpenLinkInAnotherAppPrompt(appName = "Phone") + clickPageObject(itemWithResIdAndText("android:id/button2", "Cancel")) + mDevice.waitForIdle() + verifyUrl(externalLinksPage.url.toString()) } } + /** + * User setting: Always + * For tel: links, no prompt is shown. + * The native Phone app opens automatically with the correct URI. + * tel://1234567890 + */ @Test fun alwaysOpenPhoneLinkInAppTest() { composeTestRule.activityRule.applySettingsExceptions { @@ -196,6 +258,12 @@ class AppLinksTest : TestSetup() { } } + /** + * User setting: Ask + * When prompted for a tel: link and user taps “Open”, + * the Phone app launches, then control returns to the same browser page. + * tel://1234567890 + */ @Test fun askBeforeOpeningPhoneLinkInAcceptTest() { navigationToolbar { @@ -210,6 +278,11 @@ class AppLinksTest : TestSetup() { } } + /** + * User setting: Ask + * Form redirect leading to a tel: link should trigger the Phone app prompt. + * <form action="tel://1234567890" method="POST"></form> + */ @Test fun appLinksNewTabRedirectAskTest() { navigationToolbar { @@ -219,6 +292,11 @@ class AppLinksTest : TestSetup() { } } + /** + * User setting: Always + * Form redirect leading to a tel: Phone app launches directly with no prompt. + * <form action="tel://1234567890" method="POST"></form> + */ @Test fun appLinksNewTabRedirectAlwaysTest() { composeTestRule.activityRule.applySettingsExceptions { @@ -233,6 +311,11 @@ class AppLinksTest : TestSetup() { } } + /** + * User setting: Never + * Form redirect leading to a tel: prompt is still shown for the tel: link. + * <form action="tel://1234567890" method="POST"></form> + */ @Test fun appLinksNewTabRedirectNeverTest() { composeTestRule.activityRule.applySettingsExceptions { @@ -246,11 +329,22 @@ class AppLinksTest : TestSetup() { } } + /** + * User setting: Ask + * When prompted for a external application not installed: user taps “Open”, + * a marketing intent should be used. + * intent://com.example.app#Intent;package=com.example.app;end + */ @Test fun marketingIntentWhenOpeningLinkWithoutApp() { // Use ACTION_DIAL as a non-ACTION_VIEW intent to verify that the marketing flow always // launches with ACTION_VIEW instead of reusing the original intent action. - intending(hasAction(Intent.ACTION_DIAL)).respondWith(Instrumentation.ActivityResult(0, null)) + intending(hasAction(Intent.ACTION_DIAL)).respondWith( + Instrumentation.ActivityResult( + 0, + null, + ), + ) navigationToolbar { }.enterURLAndEnterToBrowser(externalLinksPage.url) { @@ -261,4 +355,67 @@ class AppLinksTest : TestSetup() { intended(hasDataString(equalTo("market://details?id=com.example.app"))) } } + + /** + * User setting: Ask + * For a tel: link with a browser fallback, tapping “Cancel” navigates + * to the fallback URL (mozilla.org). + * intent://1234567890#Intent;scheme=tel;S.browser_fallback_url=https://www.mozilla.org;end; + */ + @Test + fun appLinksBrowserFallbackURLTest() { + navigationToolbar { + }.enterURLAndEnterToBrowser(externalLinksPage.url) { + clickPageObject(phoneWithFallbackLink) + verifyOpenLinkInAnotherAppPrompt(appName = "Phone") + clickPageObject(itemWithResIdAndText("android:id/button2", "Cancel")) + mDevice.waitForIdle() + verifyUrl("mozilla.org") + } + } + + /** + * User setting: Ask + * Link with supported scheme will never load the "afl" fallback URL + * https://mozilla.org/?afl=https://youtube.com + */ + @Test + fun linkWithAndroidFallbackLinkTest() { + navigationToolbar { + }.enterURLAndEnterToBrowser(externalLinksPage.url) { + clickPageObject(linkWithAndroidFallbackLink) + mDevice.waitForIdle() + verifyUrl("mozilla.org") + } + } + + /** + * User setting: Ask + * Link with supported scheme will never load the "link" fallback URL + * https://mozilla.org/?link=https://youtube.com + */ + @Test + fun linkWithFallbackLinkTest() { + navigationToolbar { + }.enterURLAndEnterToBrowser(externalLinksPage.url) { + clickPageObject(linkWithFallbackLink) + mDevice.waitForIdle() + verifyUrl("mozilla.org") + } + } + + /** + * User setting: Ask + * Link with supported scheme will never load the "S.browser_fallback_url" fallback URL + * https://mozilla.org/?S.browser_fallback_url=https://youtube.com + */ + @Test + fun linkWithBrowserFallbackLinkTest() { + navigationToolbar { + }.enterURLAndEnterToBrowser(externalLinksPage.url) { + clickPageObject(linkWithBrowserFallbackLink) + mDevice.waitForIdle() + verifyUrl("mozilla.org") + } + } }