Commit 31e8119c authored by MozLando's avatar MozLando
Browse files

Merge #5192



5192: Closes #5177: Support App Links Redirect that Opens URL in Non Browser App If Available r=jonalmeida a=rocketsroger

This will allow browsers to detect and launch on the first matched non browser app if its available.
Added to sample browser for future testing. 

Tested with both sample browser and Fenix to confirm feature working as expected.



Co-authored-by: default avatarRoger Yang <royang@mozilla.com>
parents 1fed2331 1610b18c
Loading
Loading
Loading
Loading
+14 −4
Original line number Diff line number Diff line
@@ -77,6 +77,7 @@ class AppLinksUseCases(
     * @param includeInstallAppFallback If {true} then offer an app-link to the installed market app
     * if no web fallback is available.
     */
    @Suppress("ComplexMethod")
    inner class GetAppLinkRedirect internal constructor(
        private val includeHttpAppLinks: Boolean = false,
        private val ignoreDefaultBrowser: Boolean = false,
@@ -90,6 +91,7 @@ class AppLinksUseCases(
                redirectData.resolveInfo == null -> null
                includeHttpAppLinks && (ignoreDefaultBrowser ||
                    (redirectData.appIntent != null && isDefaultBrowser(redirectData.appIntent))) -> null
                includeHttpAppLinks -> redirectData.appIntent
                !launchInApp() && isAppIntentHttpOrHttps -> null
                else -> redirectData.appIntent
            }
@@ -139,15 +141,23 @@ class AppLinksUseCases(
                }
            }

            val resolveInfoList = intent?.let {
                getNonBrowserActivities(it)
            }
            val resolveInfo = resolveInfoList?.firstOrNull()

            // only target intent for specific app if only one non browser app is found
            if (resolveInfoList?.count() == 1) {
                resolveInfo?.let {
                    intent.`package` = it.activityInfo?.packageName
                }
            }

            val appIntent = when (intent.data) {
                null -> null
                else -> intent
            }

            val resolveInfo = appIntent?.let {
                getNonBrowserActivities(it).firstOrNull()
            }

            return RedirectData(appIntent, fallbackIntent, marketplaceIntent, resolveInfo)
        }
    }
+19 −3
Original line number Diff line number Diff line
@@ -29,8 +29,9 @@ class AppLinksUseCasesTest {
    private val appUrl = "https://example.com"
    private val appPackage = "com.example.app"
    private val browserPackage = "com.browser"
    private val testBrowserPackage = "com.current.browser"

    private fun createContext(vararg urlToPackages: Pair<String, String>): Context {
    private fun createContext(vararg urlToPackages: Pair<String, String>, default: Boolean = false): Context {
        val pm = testContext.packageManager
        val packageManager = shadowOf(pm)

@@ -52,6 +53,9 @@ class AppLinksUseCasesTest {

        val context = mock<Context>()
        `when`(context.packageManager).thenReturn(pm)
        if (!default) {
            `when`(context.packageName).thenReturn(testBrowserPackage)
        }

        return context
    }
@@ -84,7 +88,7 @@ class AppLinksUseCasesTest {
        assertFalse(redirect.isRedirect())

        val menuRedirect = subject.appLinkRedirect(appUrl)
        assertFalse(menuRedirect.isRedirect())
        assertTrue(menuRedirect.isRedirect())
    }

    @Test
@@ -94,6 +98,9 @@ class AppLinksUseCasesTest {

        val redirect = subject.interceptedAppLinkRedirect(appUrl)
        assertFalse(redirect.isRedirect())

        val menuRedirect = subject.appLinkRedirect(appUrl)
        assertFalse(menuRedirect.hasExternalApp())
    }

    @Test
@@ -109,6 +116,15 @@ class AppLinksUseCasesTest {
        assertTrue(menuRedirect.isRedirect())
    }

    @Test
    fun `A URL that also matches default activity is not an app link`() {
        val context = createContext(appUrl to appPackage, appUrl to browserPackage, default = true)
        val subject = AppLinksUseCases(context, { true }, setOf(browserPackage))

        val menuRedirect = subject.appLinkRedirect(appUrl)
        assertFalse(menuRedirect.hasExternalApp())
    }

    @Test
    fun `A list of browser package names can be generated if not supplied`() {
        val unguessable = "https://unguessable-test-url.com"
@@ -138,7 +154,7 @@ class AppLinksUseCasesTest {
        val context = createContext(uri to appPackage, appUrl to browserPackage)
        val subject = AppLinksUseCases(context, { true }, setOf(browserPackage))

        val redirect = subject.appLinkRedirect.invoke(uri)
        val redirect = subject.interceptedAppLinkRedirect.invoke(uri)

        assertTrue(redirect.hasExternalApp())
        assertTrue(redirect.isInstallable())
+19 −0
Original line number Diff line number Diff line
@@ -34,6 +34,7 @@ import mozilla.components.concept.engine.DefaultSettings
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.fetch.Client
import mozilla.components.feature.addons.amo.AddOnCollectionProvider
import mozilla.components.feature.app.links.AppLinksUseCases
import mozilla.components.feature.contextmenu.ContextMenuUseCases
import mozilla.components.feature.customtabs.CustomTabIntentProcessor
import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
@@ -143,6 +144,7 @@ open class DefaultComponents(private val applicationContext: Context) {

    val searchUseCases by lazy { SearchUseCases(applicationContext, searchEngineManager, sessionManager) }
    val defaultSearchUseCase by lazy { { searchTerms: String -> searchUseCases.defaultSearch.invoke(searchTerms) } }
    val appLinksUseCases by lazy { AppLinksUseCases(applicationContext) }

    val webAppManifestStorage by lazy { ManifestStorage(applicationContext) }
    val webAppShortcutManager by lazy { WebAppShortcutManager(applicationContext, client, webAppManifestStorage) }
@@ -208,6 +210,23 @@ open class DefaultComponents(private val applicationContext: Context) {
            }
        )

        items.add(
            SimpleBrowserMenuItem("Open in App") {
                val getRedirect = appLinksUseCases.appLinkRedirect
                sessionManager.selectedSession?.let {
                    val redirect = getRedirect.invoke(it.url)
                    redirect.appIntent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK
                    appLinksUseCases.openAppLink.invoke(redirect)
                }
            }.apply {
                visible = {
                    sessionManager.selectedSession?.let {
                        appLinksUseCases.appLinkRedirect(it.url).hasExternalApp()
                    } ?: false
                }
            }
        )

        items.add(
            SimpleBrowserMenuItem("Clear Data") {
                sessionUseCases.clearData()