Commit ff0389f6 authored by MozLando's avatar MozLando
Browse files

Merge #7345 #7346



7345: Change WebAppHideToolbarFeature to use callback r=grigoryk a=NotWoods

One more tweak for the PWA toolbar system. This makes the Fenix integration easier.

7346: Closes #7341: Use component in App Links app intent r=Amejia481 a=rocketsroger
Co-authored-by: default avatarTiger Oakes <toakes@mozilla.com>
Co-authored-by: default avatarRoger Yang <royang@mozilla.com>
......@@ -5,6 +5,7 @@
package mozilla.components.feature.app.links
import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
......@@ -192,7 +193,7 @@ class AppLinksUseCases(
// only target intent for specific app if only one non browser app is found
if (resolveInfoList?.count() == 1) {
resolveInfo?.let {
appIntent.`package` = it.activityInfo?.packageName
appIntent.component = ComponentName(it.activityInfo.packageName, it.activityInfo.name)
}
}
......
......@@ -50,6 +50,9 @@ class AppLinksUseCasesTest {
private val aboutUrl = "about:config"
private val javascriptUrl = "javascript:'hello, world'"
private val fileType = "audio/mpeg"
private val layerUrl = "https://exmaple.com"
private val layerPackage = "com.example.app"
private val layerActivity = "com.example2.app.intentActivity"
@Before
fun setup() {
......@@ -57,16 +60,17 @@ class AppLinksUseCasesTest {
AppLinksUseCases.browserNamesCache = null
}
private fun createContext(vararg urlToPackages: Pair<String, String>, default: Boolean = false): Context {
private fun createContext(vararg urlToPackages: Triple<String, String, String>, default: Boolean = false): Context {
val pm = testContext.packageManager
val packageManager = shadowOf(pm)
urlToPackages.forEach { (urlString, pkgName) ->
urlToPackages.forEach { (urlString, pkgName, className) ->
val intent = Intent.parseUri(urlString, 0).addCategory(Intent.CATEGORY_BROWSABLE)
val info = ActivityInfo().apply {
packageName = pkgName
name = className
icon = android.R.drawable.btn_default
}
......@@ -95,6 +99,17 @@ class AppLinksUseCasesTest {
assertFalse(redirect.isRedirect())
}
@Test
fun `A URL that matches app with activity is an app link with correct component`() {
val context = createContext(Triple(layerUrl, layerPackage, layerActivity))
val subject = AppLinksUseCases(context, { true })
val redirect = subject.interceptedAppLinkRedirect(layerUrl)
assertTrue(redirect.isRedirect())
assertEquals(redirect.appIntent?.component?.packageName, layerPackage)
assertEquals(redirect.appIntent?.component?.className, layerActivity)
}
@Test
fun `A URL that matches zero apps is not an app link`() {
val context = createContext()
......@@ -106,7 +121,7 @@ class AppLinksUseCasesTest {
@Test
fun `A web URL that matches more than zero apps is an app link`() {
val context = createContext(appUrl to appPackage)
val context = createContext(Triple(appUrl, appPackage, ""))
val subject = AppLinksUseCases(context, { true })
// We will redirect to it if browser option set to true.
......@@ -116,7 +131,7 @@ class AppLinksUseCasesTest {
@Test
fun `A file is not an app link`() {
val context = createContext(filePath to appPackage)
val context = createContext(Triple(filePath, appPackage, ""))
val subject = AppLinksUseCases(context, { true })
// We will redirect to it if browser option set to true.
......@@ -126,7 +141,7 @@ class AppLinksUseCasesTest {
@Test
fun `A data url is not an app link`() {
val context = createContext(dataUrl to appPackage)
val context = createContext(Triple(dataUrl, appPackage, ""))
val subject = AppLinksUseCases(context, { true })
val redirect = subject.interceptedAppLinkRedirect(dataUrl)
......@@ -135,7 +150,7 @@ class AppLinksUseCasesTest {
@Test
fun `A javascript url is not an app link`() {
val context = createContext(javascriptUrl to appPackage)
val context = createContext(Triple(javascriptUrl, appPackage, ""))
val subject = AppLinksUseCases(context, { true })
val redirect = subject.interceptedAppLinkRedirect(javascriptUrl)
......@@ -144,7 +159,7 @@ class AppLinksUseCasesTest {
@Test
fun `An about url is not an app link`() {
val context = createContext(aboutUrl to appPackage)
val context = createContext(Triple(aboutUrl, appPackage, ""))
val subject = AppLinksUseCases(context, { true })
val redirect = subject.interceptedAppLinkRedirect(aboutUrl)
......@@ -153,7 +168,7 @@ class AppLinksUseCasesTest {
@Test
fun `Will not redirect app link if browser option set to false and scheme is supported`() {
val context = createContext(appUrl to appPackage)
val context = createContext(Triple(appUrl, appPackage, ""))
val subject = AppLinksUseCases(context, { false })
val redirect = subject.interceptedAppLinkRedirect(appUrl)
......@@ -165,7 +180,7 @@ class AppLinksUseCasesTest {
@Test
fun `Will redirect app link if browser option set to false and scheme is not supported`() {
val context = createContext(appIntent to appPackage)
val context = createContext(Triple(appIntent, appPackage, ""))
val subject = AppLinksUseCases(context, { false })
val redirect = subject.interceptedAppLinkRedirect(appIntent)
......@@ -177,7 +192,7 @@ class AppLinksUseCasesTest {
@Test
fun `A URL that matches only excluded packages is not an app link`() {
val context = createContext(appUrl to browserPackage, browserUrl to browserPackage)
val context = createContext(Triple(appUrl, browserPackage, ""), Triple(browserUrl, browserPackage, ""))
val subject = AppLinksUseCases(context, { true }, unguessableWebUrl = browserUrl)
val redirect = subject.interceptedAppLinkRedirect(appUrl)
......@@ -189,7 +204,7 @@ class AppLinksUseCasesTest {
@Test
fun `A URL that also matches excluded packages is an app link`() {
val context = createContext(appUrl to appPackage, appUrl to browserPackage, browserUrl to browserPackage)
val context = createContext(Triple(appUrl, appPackage, ""), Triple(appUrl, browserPackage, ""), Triple(browserUrl, browserPackage, ""))
val subject = AppLinksUseCases(context, { true }, unguessableWebUrl = browserUrl)
val redirect = subject.interceptedAppLinkRedirect(appUrl)
......@@ -202,7 +217,7 @@ class AppLinksUseCasesTest {
@Test
fun `A URL that only matches default activity is not an app link`() {
val context = createContext(appUrl to appPackage, default = true)
val context = createContext(Triple(appUrl, appPackage, ""), default = true)
val subject = AppLinksUseCases(context, { true })
val menuRedirect = subject.appLinkRedirect(appUrl)
......@@ -212,7 +227,7 @@ class AppLinksUseCasesTest {
@Test
fun `A list of browser package names can be generated if not supplied`() {
val unguessable = "https://unguessable-test-url.com"
val context = createContext(unguessable to browserPackage)
val context = createContext(Triple(unguessable, browserPackage, ""))
val subject = AppLinksUseCases(context, unguessableWebUrl = unguessable)
subject.appLinkRedirect(unguessable)
......@@ -222,7 +237,7 @@ class AppLinksUseCasesTest {
@Test
fun `A intent scheme uri with an installed app is an app link`() {
val uri = "intent://scan/#Intent;scheme=zxing;package=com.google.zxing.client.android;end"
val context = createContext(uri to appPackage)
val context = createContext(Triple(uri, appPackage, ""))
val subject = AppLinksUseCases(context, { true })
val redirect = subject.interceptedAppLinkRedirect(uri)
......@@ -236,7 +251,7 @@ class AppLinksUseCasesTest {
@Test
fun `A market scheme uri with no installed app is an install link`() {
val uri = "intent://details/#Intent;scheme=market;package=com.google.play;end"
val context = createContext(uri to appPackage)
val context = createContext(Triple(uri, appPackage, ""))
val subject = AppLinksUseCases(context, { true })
val redirect = subject.interceptedAppLinkRedirect.invoke(uri)
......@@ -276,7 +291,7 @@ class AppLinksUseCasesTest {
@Test
fun `A intent scheme denied should return no app intent`() {
val uri = "intent://details/#Intent"
val context = createContext(uri to appPackage)
val context = createContext(Triple(uri, appPackage, ""))
val subject = AppLinksUseCases(context, { true }, alwaysDeniedSchemes = setOf("intent"))
val redirect = subject.interceptedAppLinkRedirect.invoke(uri)
......@@ -317,7 +332,7 @@ class AppLinksUseCasesTest {
fun `AppLinksUsecases uses cache`() {
val testDispatcher = TestCoroutineDispatcher()
TestCoroutineScope(testDispatcher).launch {
val context = createContext(appUrl to appPackage)
val context = createContext(Triple(appUrl, appPackage, ""))
var subject = AppLinksUseCases(context, { true })
var redirect = subject.interceptedAppLinkRedirect(appUrl)
......@@ -342,7 +357,7 @@ class AppLinksUseCasesTest {
fun `AppLinksUsecases uses browser names cache`() {
val testDispatcher = TestCoroutineDispatcher()
TestCoroutineScope(testDispatcher).launch {
val context = createContext(appUrl to appPackage)
val context = createContext(Triple(appUrl, appPackage, ""))
var subject = AppLinksUseCases(context, { true })
whenever(subject.findExcludedPackages(any())).thenReturn(emptySet())
......@@ -439,7 +454,7 @@ class AppLinksUseCasesTest {
@Test
fun `A app scheme uri will redirect regardless user preference`() {
val context = createContext(appSchemeIntent to appPackage)
val context = createContext(Triple(appSchemeIntent, appPackage, ""))
var subject = AppLinksUseCases(context, { false })
var redirect = subject.interceptedAppLinkRedirect(appSchemeIntent)
......@@ -460,7 +475,7 @@ class AppLinksUseCasesTest {
@Test
fun `A app intent uri will redirect regardless user preference`() {
val context = createContext(appIntent to appPackage)
val context = createContext(Triple(appIntent, appPackage, ""))
var subject = AppLinksUseCases(context, { false })
var redirect = subject.interceptedAppLinkRedirect(appIntent)
......
......@@ -4,9 +4,7 @@
package mozilla.components.feature.pwa.feature
import android.view.View
import androidx.core.net.toUri
import androidx.core.view.isVisible
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.MainScope
......@@ -41,19 +39,19 @@ import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
* In standard custom tabs, no scopes are trusted.
* As a result the URL has no impact on toolbar visibility.
*
* @param toolbar Toolbar to show or hide.
* @param store Reference to the browser store where tab state is located.
* @param customTabsStore Reference to the store that communicates with the custom tabs service.
* @param tabId ID of the tab session, or null if the selected session should be used.
* @param manifest Reference to the cached [WebAppManifest] for the current PWA.
* Null if this feature is not used in a PWA context.
* @param setToolbarVisibility Callback to show or hide the toolbar.
*/
class WebAppHideToolbarFeature(
private val toolbar: View,
private val store: BrowserStore,
private val customTabsStore: CustomTabsServiceStore,
private val tabId: String? = null,
manifest: WebAppManifest? = null
manifest: WebAppManifest? = null,
private val setToolbarVisibility: (Boolean) -> Unit
) : LifecycleAwareFeature {
private val manifestScope = listOfNotNull(manifest?.getTrustedScope())
......@@ -63,7 +61,7 @@ class WebAppHideToolbarFeature(
// Hide the toolbar by default to prevent a flash.
val tab = store.state.findTabOrCustomTabOrSelectedTab(tabId)
val customTabState = customTabsStore.state.getCustomTabStateForTab(tab)
toolbar.isVisible = shouldToolbarBeVisible(tab, customTabState)
setToolbarVisibility(shouldToolbarBeVisible(tab, customTabState))
}
@ExperimentalCoroutinesApi
......@@ -84,7 +82,7 @@ class WebAppHideToolbarFeature(
.map { (tab, customTabState) -> shouldToolbarBeVisible(tab, customTabState) }
.ifChanged()
.collect { toolbarVisible ->
toolbar.isVisible = toolbarVisible
setToolbarVisibility(toolbarVisible)
}
}
}
......
......@@ -4,7 +4,6 @@
package mozilla.components.feature.pwa.feature
import android.view.View
import androidx.browser.customtabs.CustomTabsService.RELATION_HANDLE_ALL_URLS
import androidx.browser.customtabs.CustomTabsSessionToken
import androidx.core.net.toUri
......@@ -29,9 +28,10 @@ import mozilla.components.feature.customtabs.store.ValidateRelationshipAction
import mozilla.components.feature.customtabs.store.VerificationStatus
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
......@@ -42,11 +42,16 @@ class WebAppHideToolbarFeatureTest {
private val customTabId = "custom-id"
private val testDispatcher = TestCoroutineDispatcher()
private val toolbar = View(testContext)
private var toolbarVisible = false
@get:Rule
val coroutinesTestRule = MainCoroutineRule(testDispatcher)
@Before
fun setup() {
toolbarVisible = false
}
@Test
fun `hides toolbar immediately based on PWA manifest`() {
val tab = CustomTabSessionState(
......@@ -57,14 +62,15 @@ class WebAppHideToolbarFeatureTest {
val store = BrowserStore(BrowserState(customTabs = listOf(tab)))
val feature = WebAppHideToolbarFeature(
toolbar,
store,
CustomTabsServiceStore(),
tabId = tab.id,
manifest = mockManifest("https://mozilla.org")
)
) {
toolbarVisible = it
}
feature.start()
assertEquals(View.GONE, toolbar.visibility)
assertFalse(toolbarVisible)
}
@Test
......@@ -81,13 +87,14 @@ class WebAppHideToolbarFeatureTest {
))
val feature = WebAppHideToolbarFeature(
toolbar,
store,
customTabsStore,
tabId = tab.id
)
) {
toolbarVisible = it
}
feature.start()
assertEquals(View.GONE, toolbar.visibility)
assertFalse(toolbarVisible)
}
@Test
......@@ -95,18 +102,22 @@ class WebAppHideToolbarFeatureTest {
val tab = createTab("https://mozilla.org")
val store = BrowserStore(BrowserState(tabs = listOf(tab)))
val feature = WebAppHideToolbarFeature(toolbar, store, CustomTabsServiceStore(), tabId = tab.id)
val feature = WebAppHideToolbarFeature(store, CustomTabsServiceStore(), tabId = tab.id) {
toolbarVisible = it
}
feature.start()
assertEquals(View.VISIBLE, toolbar.visibility)
assertTrue(toolbarVisible)
}
@Test
fun `does not hide toolbar for an invalid tab`() {
val store = BrowserStore()
val feature = WebAppHideToolbarFeature(toolbar, store, CustomTabsServiceStore())
val feature = WebAppHideToolbarFeature(store, CustomTabsServiceStore()) {
toolbarVisible = it
}
feature.start()
assertEquals(View.VISIBLE, toolbar.visibility)
assertTrue(toolbarVisible)
}
@Test
......@@ -119,9 +130,11 @@ class WebAppHideToolbarFeatureTest {
)
val store = BrowserStore(BrowserState(tabs = listOf(tab)))
val feature = WebAppHideToolbarFeature(toolbar, store, CustomTabsServiceStore(), tabId = tab.id)
val feature = WebAppHideToolbarFeature(store, CustomTabsServiceStore(), tabId = tab.id) {
toolbarVisible = it
}
feature.start()
assertEquals(View.GONE, toolbar.visibility)
assertFalse(toolbarVisible)
}
@Test
......@@ -134,9 +147,11 @@ class WebAppHideToolbarFeatureTest {
)
val store = BrowserStore(BrowserState(tabs = listOf(tab)))
val feature = WebAppHideToolbarFeature(toolbar, store, CustomTabsServiceStore(), tabId = tab.id)
val feature = WebAppHideToolbarFeature(store, CustomTabsServiceStore(), tabId = tab.id) {
toolbarVisible = it
}
feature.start()
assertEquals(View.GONE, toolbar.visibility)
assertFalse(toolbarVisible)
}
@Test
......@@ -153,13 +168,14 @@ class WebAppHideToolbarFeatureTest {
))
val feature = WebAppHideToolbarFeature(
toolbar,
store,
customTabsStore,
tabId = tab.id
)
) {
toolbarVisible = it
}
feature.start()
assertEquals(View.VISIBLE, toolbar.visibility)
assertTrue(toolbarVisible)
}
@Test
......@@ -175,32 +191,33 @@ class WebAppHideToolbarFeatureTest {
tabs = mapOf(token to mockCustomTabState("https://mozilla.com", "https://m.mozilla.com"))
))
val feature = WebAppHideToolbarFeature(
toolbar,
store,
customTabsStore,
tabId = customTabId
)
) {
toolbarVisible = it
}
feature.start()
store.dispatch(
ContentAction.UpdateUrlAction(customTabId, "https://mozilla.com/example-page")
).joinBlocking()
assertEquals(View.GONE, toolbar.visibility)
assertFalse(toolbarVisible)
store.dispatch(
ContentAction.UpdateUrlAction(customTabId, "https://firefox.com/out-of-scope")
).joinBlocking()
assertEquals(View.VISIBLE, toolbar.visibility)
assertTrue(toolbarVisible)
store.dispatch(
ContentAction.UpdateUrlAction(customTabId, "https://mozilla.com/back-in-scope")
).joinBlocking()
assertEquals(View.GONE, toolbar.visibility)
assertFalse(toolbarVisible)
store.dispatch(
ContentAction.UpdateUrlAction(customTabId, "https://m.mozilla.com/second-origin")
).joinBlocking()
assertEquals(View.GONE, toolbar.visibility)
assertFalse(toolbarVisible)
}
@Test
......@@ -208,33 +225,34 @@ class WebAppHideToolbarFeatureTest {
val tab = createCustomTab(id = customTabId, url = "https://mozilla.org")
val store = BrowserStore(BrowserState(customTabs = listOf(tab)))
val feature = WebAppHideToolbarFeature(
toolbar,
store,
CustomTabsServiceStore(),
tabId = customTabId,
manifest = mockManifest("https://mozilla.github.io/my-app/")
)
) {
toolbarVisible = it
}
feature.start()
store.dispatch(
ContentAction.UpdateUrlAction(customTabId, "https://mozilla.github.io/my-app/")
).joinBlocking()
assertEquals(View.GONE, toolbar.visibility)
assertFalse(toolbarVisible)
store.dispatch(
ContentAction.UpdateUrlAction(customTabId, "https://firefox.com/out-of-scope")
).joinBlocking()
assertEquals(View.VISIBLE, toolbar.visibility)
assertTrue(toolbarVisible)
store.dispatch(
ContentAction.UpdateUrlAction(customTabId, "https://mozilla.github.io/my-app-almost-in-scope")
).joinBlocking()
assertEquals(View.VISIBLE, toolbar.visibility)
assertTrue(toolbarVisible)
store.dispatch(
ContentAction.UpdateUrlAction(customTabId, "https://mozilla.github.io/my-app/sub-page")
).joinBlocking()
assertEquals(View.GONE, toolbar.visibility)
assertFalse(toolbarVisible)
}
@Test
......@@ -242,23 +260,24 @@ class WebAppHideToolbarFeatureTest {
val tab = createCustomTab(id = customTabId, url = "https://mozilla.org")
val store = BrowserStore(BrowserState(customTabs = listOf(tab)))
val feature = WebAppHideToolbarFeature(
toolbar,
store,
CustomTabsServiceStore(),
tabId = customTabId,
manifest = mockManifest("https://mozilla.github.io/prefix")
)
) {
toolbarVisible = it
}
feature.start()
store.dispatch(
ContentAction.UpdateUrlAction(customTabId, "https://mozilla.github.io/prefix/")
).joinBlocking()
assertEquals(View.GONE, toolbar.visibility)
assertFalse(toolbarVisible)
store.dispatch(
ContentAction.UpdateUrlAction(customTabId, "https://mozilla.github.io/prefix-of/resource.html")
).joinBlocking()
assertEquals(View.GONE, toolbar.visibility)
assertFalse(toolbarVisible)
}
@Test
......@@ -274,11 +293,12 @@ class WebAppHideToolbarFeatureTest {
tabs = mapOf(token to mockCustomTabState())
))
val feature = WebAppHideToolbarFeature(
toolbar,
store,
customTabsStore,
tabId = customTabId
)
) {
toolbarVisible = it
}
feature.start()
customTabsStore.dispatch(ValidateRelationshipAction(
......@@ -287,7 +307,7 @@ class WebAppHideToolbarFeatureTest {
"https://m.mozilla.com".toUri(),
VerificationStatus.PENDING
)).joinBlocking()
assertEquals(View.VISIBLE, toolbar.visibility)
assertTrue(toolbarVisible)
customTabsStore.dispatch(ValidateRelationshipAction(
token,
......@@ -295,7 +315,7 @@ class WebAppHideToolbarFeatureTest {
"https://mozilla.com".toUri(),
VerificationStatus.PENDING
)).joinBlocking()
assertEquals(View.GONE, toolbar.visibility)
assertFalse(toolbarVisible)
}
private fun mockCustomTabState(vararg origins: String) = CustomTabState(
......