Skip to content

Commit 31e8119

Browse files
5192: Closes mozilla-mobile#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: Roger Yang <[email protected]>
2 parents 1fed233 + 1610b18 commit 31e8119

File tree

3 files changed

+52
-7
lines changed

3 files changed

+52
-7
lines changed

components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksUseCases.kt

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class AppLinksUseCases(
7777
* @param includeInstallAppFallback If {true} then offer an app-link to the installed market app
7878
* if no web fallback is available.
7979
*/
80+
@Suppress("ComplexMethod")
8081
inner class GetAppLinkRedirect internal constructor(
8182
private val includeHttpAppLinks: Boolean = false,
8283
private val ignoreDefaultBrowser: Boolean = false,
@@ -90,6 +91,7 @@ class AppLinksUseCases(
9091
redirectData.resolveInfo == null -> null
9192
includeHttpAppLinks && (ignoreDefaultBrowser ||
9293
(redirectData.appIntent != null && isDefaultBrowser(redirectData.appIntent))) -> null
94+
includeHttpAppLinks -> redirectData.appIntent
9395
!launchInApp() && isAppIntentHttpOrHttps -> null
9496
else -> redirectData.appIntent
9597
}
@@ -139,15 +141,23 @@ class AppLinksUseCases(
139141
}
140142
}
141143

144+
val resolveInfoList = intent?.let {
145+
getNonBrowserActivities(it)
146+
}
147+
val resolveInfo = resolveInfoList?.firstOrNull()
148+
149+
// only target intent for specific app if only one non browser app is found
150+
if (resolveInfoList?.count() == 1) {
151+
resolveInfo?.let {
152+
intent.`package` = it.activityInfo?.packageName
153+
}
154+
}
155+
142156
val appIntent = when (intent.data) {
143157
null -> null
144158
else -> intent
145159
}
146160

147-
val resolveInfo = appIntent?.let {
148-
getNonBrowserActivities(it).firstOrNull()
149-
}
150-
151161
return RedirectData(appIntent, fallbackIntent, marketplaceIntent, resolveInfo)
152162
}
153163
}

components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksUseCasesTest.kt

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ class AppLinksUseCasesTest {
2929
private val appUrl = "https://example.com"
3030
private val appPackage = "com.example.app"
3131
private val browserPackage = "com.browser"
32+
private val testBrowserPackage = "com.current.browser"
3233

33-
private fun createContext(vararg urlToPackages: Pair<String, String>): Context {
34+
private fun createContext(vararg urlToPackages: Pair<String, String>, default: Boolean = false): Context {
3435
val pm = testContext.packageManager
3536
val packageManager = shadowOf(pm)
3637

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

5354
val context = mock<Context>()
5455
`when`(context.packageManager).thenReturn(pm)
56+
if (!default) {
57+
`when`(context.packageName).thenReturn(testBrowserPackage)
58+
}
5559

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

8690
val menuRedirect = subject.appLinkRedirect(appUrl)
87-
assertFalse(menuRedirect.isRedirect())
91+
assertTrue(menuRedirect.isRedirect())
8892
}
8993

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

9599
val redirect = subject.interceptedAppLinkRedirect(appUrl)
96100
assertFalse(redirect.isRedirect())
101+
102+
val menuRedirect = subject.appLinkRedirect(appUrl)
103+
assertFalse(menuRedirect.hasExternalApp())
97104
}
98105

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

119+
@Test
120+
fun `A URL that also matches default activity is not an app link`() {
121+
val context = createContext(appUrl to appPackage, appUrl to browserPackage, default = true)
122+
val subject = AppLinksUseCases(context, { true }, setOf(browserPackage))
123+
124+
val menuRedirect = subject.appLinkRedirect(appUrl)
125+
assertFalse(menuRedirect.hasExternalApp())
126+
}
127+
112128
@Test
113129
fun `A list of browser package names can be generated if not supplied`() {
114130
val unguessable = "https://unguessable-test-url.com"
@@ -138,7 +154,7 @@ class AppLinksUseCasesTest {
138154
val context = createContext(uri to appPackage, appUrl to browserPackage)
139155
val subject = AppLinksUseCases(context, { true }, setOf(browserPackage))
140156

141-
val redirect = subject.appLinkRedirect.invoke(uri)
157+
val redirect = subject.interceptedAppLinkRedirect.invoke(uri)
142158

143159
assertTrue(redirect.hasExternalApp())
144160
assertTrue(redirect.isInstallable())

samples/browser/src/main/java/org/mozilla/samples/browser/DefaultComponents.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import mozilla.components.concept.engine.DefaultSettings
3434
import mozilla.components.concept.engine.Engine
3535
import mozilla.components.concept.fetch.Client
3636
import mozilla.components.feature.addons.amo.AddOnCollectionProvider
37+
import mozilla.components.feature.app.links.AppLinksUseCases
3738
import mozilla.components.feature.contextmenu.ContextMenuUseCases
3839
import mozilla.components.feature.customtabs.CustomTabIntentProcessor
3940
import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
@@ -143,6 +144,7 @@ open class DefaultComponents(private val applicationContext: Context) {
143144

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

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

213+
items.add(
214+
SimpleBrowserMenuItem("Open in App") {
215+
val getRedirect = appLinksUseCases.appLinkRedirect
216+
sessionManager.selectedSession?.let {
217+
val redirect = getRedirect.invoke(it.url)
218+
redirect.appIntent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK
219+
appLinksUseCases.openAppLink.invoke(redirect)
220+
}
221+
}.apply {
222+
visible = {
223+
sessionManager.selectedSession?.let {
224+
appLinksUseCases.appLinkRedirect(it.url).hasExternalApp()
225+
} ?: false
226+
}
227+
}
228+
)
229+
211230
items.add(
212231
SimpleBrowserMenuItem("Clear Data") {
213232
sessionUseCases.clearData()

0 commit comments

Comments
 (0)