mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-23 21:01:08 +00:00
Bug 1915612 - Enable app links interceptor to also handle user prompts r=android-reviewers,tthibaud
Differential Revision: https://phabricator.services.mozilla.com/D223887
This commit is contained in:
parent
d3f5a90afc
commit
2d73b99c54
@ -8,6 +8,7 @@ import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.state.CustomTabSessionState
|
||||
import mozilla.components.browser.state.state.SessionState
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import mozilla.components.concept.engine.EngineSession
|
||||
import mozilla.components.support.base.log.logger.Logger
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
@ -39,6 +40,17 @@ fun BrowserState.findTab(tabId: String): TabSessionState? {
|
||||
return tabs.firstOrNull { it.id == tabId }
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns the tab with the given [EngineSession]. Returns null if no matching tab could be
|
||||
* found.
|
||||
*
|
||||
* @param engineSession The engineSession of the tab to search for.
|
||||
* @return The [TabSessionState] with the provided [EngineSession] or null if it could not be found.
|
||||
*/
|
||||
fun BrowserState.findTab(engineSession: EngineSession): TabSessionState? {
|
||||
return tabs.firstOrNull { it.engineState.engineSession == engineSession }
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns the Custom Tab with the given id. Returns null if no matching tab could be
|
||||
* found.
|
||||
|
@ -104,6 +104,22 @@ class SelectorsKtTest {
|
||||
assertNull(state.findTab(customTab.id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN findTab WITH engine session THEN currect tab is returned`() {
|
||||
val tab = createTab("https://www.firefox.com")
|
||||
val otherTab = createTab("https://getpocket.com")
|
||||
val customTab = createCustomTab("https://www.mozilla.org")
|
||||
|
||||
val state = BrowserState(
|
||||
tabs = listOf(tab, otherTab),
|
||||
customTabs = listOf(customTab),
|
||||
)
|
||||
|
||||
assertEquals(tab, state.findTab(tab.id))
|
||||
assertEquals(otherTab, state.findTab(otherTab.id))
|
||||
assertNull(state.findTab(customTab.id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `findNormalTab extension function`() {
|
||||
val privateTab = createTab("https://www.firefox.com", private = true)
|
||||
|
@ -95,7 +95,7 @@ class AppLinksFeature(
|
||||
}
|
||||
|
||||
val doNotOpenApp = {
|
||||
AppLinksInterceptor.addUserDoNotIntercept(url, appIntent)
|
||||
AppLinksInterceptor.addUserDoNotIntercept(url, appIntent, tab.id)
|
||||
|
||||
loadUrlIfSchemeSupported(tab, url)
|
||||
}
|
||||
|
@ -10,10 +10,16 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.SystemClock
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import mozilla.components.browser.state.selector.findTab
|
||||
import mozilla.components.browser.state.state.SessionState
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.concept.engine.EngineSession
|
||||
import mozilla.components.concept.engine.request.RequestInterceptor
|
||||
import mozilla.components.feature.app.links.AppLinksUseCases.Companion.ALWAYS_DENY_SCHEMES
|
||||
import mozilla.components.feature.app.links.AppLinksUseCases.Companion.ENGINE_SUPPORTED_SCHEMES
|
||||
import mozilla.components.feature.app.links.RedirectDialogFragment.Companion.FRAGMENT_TAG
|
||||
import mozilla.components.support.ktx.android.content.appName
|
||||
import mozilla.components.support.ktx.android.net.isHttpOrHttps
|
||||
import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
|
||||
|
||||
@ -47,7 +53,11 @@ private const val MAPS = "maps."
|
||||
* of security concerns.
|
||||
* @param useCases These use cases allow for the detection of, and opening of links that other apps
|
||||
* have registered to open.
|
||||
* @param launchFromInterceptor If {true} then the interceptor will launch the link in third-party apps if available.
|
||||
* @param launchFromInterceptor If {true} then the interceptor will prompt and launch the link in
|
||||
* third-party apps if available. Do not use this in conjunction with [AppLinksFeature]
|
||||
* @param store [BrowserStore] containing the information about the currently open tabs.
|
||||
* @param shouldPrompt If {true} then we should prompt the user before redirect.
|
||||
* @param failedToLaunchAction Action to perform when failing to launch in third party app.
|
||||
*/
|
||||
class AppLinksInterceptor(
|
||||
private val context: Context,
|
||||
@ -61,7 +71,20 @@ class AppLinksInterceptor(
|
||||
alwaysDeniedSchemes = alwaysDeniedSchemes,
|
||||
),
|
||||
private val launchFromInterceptor: Boolean = false,
|
||||
private val store: BrowserStore? = null,
|
||||
private val shouldPrompt: () -> Boolean = { true },
|
||||
private val failedToLaunchAction: (fallbackUrl: String?) -> Unit = {},
|
||||
) : RequestInterceptor {
|
||||
private var fragmentManager: FragmentManager? = null
|
||||
private val dialog: RedirectDialogFragment? = null
|
||||
|
||||
/**
|
||||
* Update [FragmentManager] for this instance of AppLinksInterceptor
|
||||
* @param fragmentManager the new value of [FragmentManager]
|
||||
*/
|
||||
fun updateFragmentManger(fragmentManager: FragmentManager?) {
|
||||
this.fragmentManager = fragmentManager
|
||||
}
|
||||
|
||||
/**
|
||||
* Update launchInApp for this instance of AppLinksInterceptor
|
||||
@ -87,6 +110,7 @@ class AppLinksInterceptor(
|
||||
val uriScheme = encodedUri.scheme
|
||||
val engineSupportsScheme = engineSupportedSchemes.contains(uriScheme)
|
||||
val isAllowedRedirect = (isRedirect && !isSubframeRequest)
|
||||
val tabSessionState = store?.state?.findTab(engineSession)
|
||||
|
||||
val doNotIntercept = when {
|
||||
uriScheme == null -> true
|
||||
@ -109,8 +133,9 @@ class AppLinksInterceptor(
|
||||
return null
|
||||
}
|
||||
|
||||
val tabId = tabSessionState?.id ?: ""
|
||||
val redirect = useCases.interceptedAppLinkRedirect(uri)
|
||||
val result = handleRedirect(redirect, uri, engineSupportedSchemes.contains(uriScheme))
|
||||
val result = handleRedirect(redirect, uri, tabId, engineSupportedSchemes.contains(uriScheme))
|
||||
|
||||
if (redirect.hasExternalApp()) {
|
||||
val packageName = redirect.appIntent?.component?.packageName
|
||||
@ -126,9 +151,12 @@ class AppLinksInterceptor(
|
||||
}
|
||||
|
||||
if (redirect.isRedirect()) {
|
||||
if (launchFromInterceptor && result is RequestInterceptor.InterceptionResponse.AppIntent) {
|
||||
result.appIntent.flags = result.appIntent.flags or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
useCases.openAppLink(result.appIntent)
|
||||
if (
|
||||
launchFromInterceptor &&
|
||||
result is RequestInterceptor.InterceptionResponse.AppIntent
|
||||
) {
|
||||
handleAppIntent(tabSessionState, uri, redirect.appIntent)
|
||||
return null
|
||||
}
|
||||
|
||||
return result
|
||||
@ -143,15 +171,16 @@ class AppLinksInterceptor(
|
||||
internal fun handleRedirect(
|
||||
redirect: AppLinkRedirect,
|
||||
uri: String,
|
||||
tabId: String,
|
||||
schemeSupported: Boolean,
|
||||
): RequestInterceptor.InterceptionResponse? {
|
||||
if (!launchInApp() || inUserDoNotIntercept(uri, redirect.appIntent)) {
|
||||
if (!launchInApp() || inUserDoNotIntercept(uri, redirect.appIntent, tabId)) {
|
||||
redirect.fallbackUrl?.let {
|
||||
return RequestInterceptor.InterceptionResponse.Url(it)
|
||||
}
|
||||
}
|
||||
|
||||
if (schemeSupported && inUserDoNotIntercept(uri, redirect.appIntent)) {
|
||||
if (schemeSupported && inUserDoNotIntercept(uri, redirect.appIntent, tabId)) {
|
||||
return null
|
||||
}
|
||||
|
||||
@ -193,6 +222,105 @@ class AppLinksInterceptor(
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal fun handleAppIntent(
|
||||
sessionState: SessionState?,
|
||||
url: String,
|
||||
appIntent: Intent?,
|
||||
) {
|
||||
if (appIntent == null) {
|
||||
return
|
||||
}
|
||||
val fragmentManager = fragmentManager
|
||||
|
||||
val isPrivate = sessionState?.content?.private == true
|
||||
val doNotOpenApp = {
|
||||
addUserDoNotIntercept(url, appIntent, sessionState?.id)
|
||||
}
|
||||
|
||||
val doOpenApp = {
|
||||
useCases.openAppLink(
|
||||
appIntent,
|
||||
failedToLaunchAction = failedToLaunchAction,
|
||||
)
|
||||
}
|
||||
|
||||
// Without fragment manager we are unable to prompt
|
||||
// Only non private tabs we can redirect to external app without prompt
|
||||
// Authentication flow should not prompt
|
||||
val isAuthenticationFlow =
|
||||
sessionState?.let { isAuthentication(sessionState, appIntent) } == true
|
||||
val shouldShowPrompt = isPrivate || shouldPrompt()
|
||||
if (fragmentManager == null || !shouldShowPrompt || isAuthenticationFlow) {
|
||||
doOpenApp()
|
||||
return
|
||||
}
|
||||
|
||||
if (isADialogAlreadyCreated()) {
|
||||
return
|
||||
}
|
||||
|
||||
getOrCreateDialog(isPrivate, url).apply {
|
||||
onConfirmRedirect = doOpenApp
|
||||
onCancelRedirect = doNotOpenApp
|
||||
}.showNow(fragmentManager, FRAGMENT_TAG)
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal fun getOrCreateDialog(isPrivate: Boolean, url: String): RedirectDialogFragment {
|
||||
if (dialog != null) {
|
||||
return dialog
|
||||
}
|
||||
|
||||
val dialogTitle = if (isPrivate) {
|
||||
R.string.mozac_feature_applinks_confirm_dialog_title
|
||||
} else {
|
||||
R.string.mozac_feature_applinks_normal_confirm_dialog_title
|
||||
}
|
||||
|
||||
val dialogMessage = if (isPrivate) {
|
||||
url
|
||||
} else {
|
||||
context.getString(
|
||||
R.string.mozac_feature_applinks_normal_confirm_dialog_message,
|
||||
context.appName,
|
||||
)
|
||||
}
|
||||
|
||||
return SimpleRedirectDialogFragment.newInstance(
|
||||
dialogTitleText = dialogTitle,
|
||||
dialogMessageString = dialogMessage,
|
||||
positiveButtonText = R.string.mozac_feature_applinks_confirm_dialog_confirm,
|
||||
negativeButtonText = R.string.mozac_feature_applinks_confirm_dialog_deny,
|
||||
)
|
||||
}
|
||||
|
||||
private fun isADialogAlreadyCreated(): Boolean {
|
||||
return fragmentManager?.findFragmentByTag(FRAGMENT_TAG) as? RedirectDialogFragment != null
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal fun isAuthentication(sessionState: SessionState, appIntent: Intent): Boolean {
|
||||
return when (sessionState.source) {
|
||||
is SessionState.Source.External.ActionSend,
|
||||
is SessionState.Source.External.ActionSearch,
|
||||
-> false
|
||||
// CustomTab and ActionView can be used for authentication
|
||||
is SessionState.Source.External.CustomTab,
|
||||
is SessionState.Source.External.ActionView,
|
||||
-> {
|
||||
(sessionState.source as? SessionState.Source.External)?.let { externalSource ->
|
||||
when (externalSource.caller?.packageId) {
|
||||
null -> false
|
||||
appIntent.component?.packageName -> true
|
||||
else -> false
|
||||
}
|
||||
} ?: false
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal var userDoNotInterceptCache: MutableMap<Int, Long> = mutableMapOf()
|
||||
@ -201,19 +329,21 @@ class AppLinksInterceptor(
|
||||
internal var lastApplinksPackageWithTimestamp: Pair<String?, Long> = Pair(null, 0L)
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun getCacheKey(url: String, appIntent: Intent?): Int? {
|
||||
internal fun getCacheKey(url: String, appIntent: Intent?, tabId: String?): Int? {
|
||||
return Uri.parse(url)?.let { uri ->
|
||||
when {
|
||||
appIntent?.component?.packageName != null -> appIntent.component?.packageName
|
||||
!uri.isHttpOrHttps -> uri.scheme
|
||||
else -> uri.host // worst case we do not prompt again on this host
|
||||
}.hashCode()
|
||||
}?.let {
|
||||
(it + (tabId.orEmpty())).hashCode() // do not open cache should only apply to this tab
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun inUserDoNotIntercept(url: String, appIntent: Intent?): Boolean {
|
||||
val cacheKey = getCacheKey(url, appIntent)
|
||||
internal fun inUserDoNotIntercept(url: String, appIntent: Intent?, tabId: String?): Boolean {
|
||||
val cacheKey = getCacheKey(url, appIntent, tabId)
|
||||
val cacheTimeStamp = userDoNotInterceptCache[cacheKey]
|
||||
val currentTimeStamp = SystemClock.elapsedRealtime()
|
||||
|
||||
@ -221,8 +351,8 @@ class AppLinksInterceptor(
|
||||
currentTimeStamp <= (cacheTimeStamp + APP_LINKS_DO_NOT_OPEN_CACHE_INTERVAL)
|
||||
}
|
||||
|
||||
internal fun addUserDoNotIntercept(url: String, appIntent: Intent?) {
|
||||
val cacheKey = getCacheKey(url, appIntent)
|
||||
internal fun addUserDoNotIntercept(url: String, appIntent: Intent?, tabId: String?) {
|
||||
val cacheKey = getCacheKey(url, appIntent, tabId)
|
||||
cacheKey?.let {
|
||||
userDoNotInterceptCache[it] = SystemClock.elapsedRealtime()
|
||||
}
|
||||
|
@ -223,7 +223,7 @@ class AppLinksFeatureTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN tab have action view and caller is the same as external app THEN an external app dialog is shown`() {
|
||||
fun `WHEN tab have action view and caller is the same as external app THEN an external app dialog is not shown`() {
|
||||
feature = spy(
|
||||
AppLinksFeature(
|
||||
context = mockContext,
|
||||
|
@ -7,7 +7,16 @@ package mozilla.components.feature.app.links
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import mozilla.components.browser.state.state.ContentState
|
||||
import mozilla.components.browser.state.state.ExternalPackage
|
||||
import mozilla.components.browser.state.state.PackageCategory
|
||||
import mozilla.components.browser.state.state.SessionState
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import mozilla.components.browser.state.state.createCustomTab
|
||||
import mozilla.components.browser.state.state.createTab
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.concept.engine.EngineSession
|
||||
import mozilla.components.concept.engine.request.RequestInterceptor
|
||||
import mozilla.components.feature.app.links.AppLinksInterceptor.Companion.APP_LINKS_DO_NOT_INTERCEPT_INTERVAL
|
||||
@ -16,7 +25,9 @@ import mozilla.components.feature.app.links.AppLinksInterceptor.Companion.addUse
|
||||
import mozilla.components.feature.app.links.AppLinksInterceptor.Companion.inUserDoNotIntercept
|
||||
import mozilla.components.feature.app.links.AppLinksInterceptor.Companion.lastApplinksPackageWithTimestamp
|
||||
import mozilla.components.feature.app.links.AppLinksInterceptor.Companion.userDoNotInterceptCache
|
||||
import mozilla.components.feature.session.SessionUseCases
|
||||
import mozilla.components.support.test.any
|
||||
import mozilla.components.support.test.eq
|
||||
import mozilla.components.support.test.mock
|
||||
import mozilla.components.support.test.whenever
|
||||
import org.junit.Assert.assertEquals
|
||||
@ -27,16 +38,24 @@ import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentMatchers.anyBoolean
|
||||
import org.mockito.ArgumentMatchers.anyString
|
||||
import org.mockito.Mockito.doReturn
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.spy
|
||||
import org.mockito.Mockito.times
|
||||
import org.mockito.Mockito.verify
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AppLinksInterceptorTest {
|
||||
private lateinit var mockContext: Context
|
||||
private lateinit var store: BrowserStore
|
||||
private lateinit var mockUseCases: AppLinksUseCases
|
||||
private lateinit var mockGetRedirect: AppLinksUseCases.GetAppLinkRedirect
|
||||
private lateinit var mockEngineSession: EngineSession
|
||||
private lateinit var mockOpenRedirect: AppLinksUseCases.OpenAppLinkRedirect
|
||||
private lateinit var mockFragmentManager: FragmentManager
|
||||
private lateinit var mockDialog: RedirectDialogFragment
|
||||
private lateinit var mockLoadUrlUseCase: SessionUseCases.DefaultLoadUrlUseCase
|
||||
|
||||
private lateinit var appLinksInterceptor: AppLinksInterceptor
|
||||
|
||||
@ -49,12 +68,17 @@ class AppLinksInterceptorTest {
|
||||
@Before
|
||||
fun setup() {
|
||||
mockContext = mock()
|
||||
store = BrowserStore()
|
||||
mockUseCases = mock()
|
||||
mockEngineSession = mock()
|
||||
mockGetRedirect = mock()
|
||||
mockOpenRedirect = mock()
|
||||
mockDialog = mock()
|
||||
mockLoadUrlUseCase = mock()
|
||||
mockFragmentManager = mock()
|
||||
whenever(mockUseCases.interceptedAppLinkRedirect).thenReturn(mockGetRedirect)
|
||||
whenever(mockUseCases.openAppLink).thenReturn(mockOpenRedirect)
|
||||
whenever(mockFragmentManager.beginTransaction()).thenReturn(mock())
|
||||
userDoNotInterceptCache.clear()
|
||||
lastApplinksPackageWithTimestamp = Pair(null, -APP_LINKS_DO_NOT_INTERCEPT_INTERVAL)
|
||||
|
||||
@ -450,11 +474,28 @@ class AppLinksInterceptorTest {
|
||||
launchInApp = { true },
|
||||
useCases = mockUseCases,
|
||||
launchFromInterceptor = true,
|
||||
shouldPrompt = { false },
|
||||
)
|
||||
|
||||
val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, true, false, false, false, false)
|
||||
assertNull(response)
|
||||
verify(mockOpenRedirect).invoke(any(), anyBoolean(), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN launch from intercept is false AND launch in app is set to true and it is user triggered THEN app intent is returned`() {
|
||||
appLinksInterceptor = AppLinksInterceptor(
|
||||
context = mockContext,
|
||||
interceptLinkClicks = true,
|
||||
launchInApp = { true },
|
||||
useCases = mockUseCases,
|
||||
launchFromInterceptor = false,
|
||||
shouldPrompt = { false },
|
||||
)
|
||||
|
||||
val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, true, false, false, false, false)
|
||||
assert(response is RequestInterceptor.InterceptionResponse.AppIntent)
|
||||
verify(mockOpenRedirect).invoke(any(), anyBoolean(), any())
|
||||
verify(mockOpenRedirect, times(0)).invoke(any(), anyBoolean(), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -468,7 +509,7 @@ class AppLinksInterceptorTest {
|
||||
)
|
||||
|
||||
val testRedirect = AppLinkRedirect(Intent.parseUri(intentUrl, 0), "", fallbackUrl, null)
|
||||
val response = appLinksInterceptor.handleRedirect(testRedirect, intentUrl, true)
|
||||
val response = appLinksInterceptor.handleRedirect(testRedirect, intentUrl, "", true)
|
||||
assert(response is RequestInterceptor.InterceptionResponse.Url)
|
||||
}
|
||||
|
||||
@ -483,10 +524,25 @@ class AppLinksInterceptorTest {
|
||||
)
|
||||
|
||||
val response = appLinksInterceptor.onLoadRequest(mockEngineSession, intentUrl, null, false, true, false, false, false)
|
||||
assert(response is RequestInterceptor.InterceptionResponse.AppIntent)
|
||||
assertNull(response)
|
||||
verify(mockOpenRedirect).invoke(any(), anyBoolean(), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN url scheme is not supported by the engine AND launch from interceptor is false THEN app intent is returned`() {
|
||||
appLinksInterceptor = AppLinksInterceptor(
|
||||
context = mockContext,
|
||||
interceptLinkClicks = true,
|
||||
launchInApp = { false },
|
||||
useCases = mockUseCases,
|
||||
launchFromInterceptor = false,
|
||||
)
|
||||
|
||||
val response = appLinksInterceptor.onLoadRequest(mockEngineSession, intentUrl, null, false, true, false, false, false)
|
||||
assert(response is RequestInterceptor.InterceptionResponse.AppIntent)
|
||||
verify(mockOpenRedirect, times(0)).invoke(any(), anyBoolean(), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `do not use fallback url if trigger by user gesture and preference is to launch in app`() {
|
||||
appLinksInterceptor = AppLinksInterceptor(
|
||||
@ -498,7 +554,7 @@ class AppLinksInterceptorTest {
|
||||
)
|
||||
|
||||
val testRedirect = AppLinkRedirect(Intent.parseUri(intentUrl, 0), "", fallbackUrl, null)
|
||||
val response = appLinksInterceptor.handleRedirect(testRedirect, intentUrl, true)
|
||||
val response = appLinksInterceptor.handleRedirect(testRedirect, intentUrl, "", true)
|
||||
assert(response is RequestInterceptor.InterceptionResponse.AppIntent)
|
||||
}
|
||||
|
||||
@ -513,7 +569,7 @@ class AppLinksInterceptorTest {
|
||||
)
|
||||
|
||||
val testRedirect = AppLinkRedirect(null, "", fallbackUrl, Intent.parseUri(marketplaceUrl, 0))
|
||||
val response = appLinksInterceptor.handleRedirect(testRedirect, webUrl, true)
|
||||
val response = appLinksInterceptor.handleRedirect(testRedirect, webUrl, "", true)
|
||||
assert(response is RequestInterceptor.InterceptionResponse.AppIntent)
|
||||
}
|
||||
|
||||
@ -528,7 +584,7 @@ class AppLinksInterceptorTest {
|
||||
)
|
||||
|
||||
val testRedirect = AppLinkRedirect(null, "", fallbackUrl, null)
|
||||
val response = appLinksInterceptor.handleRedirect(testRedirect, webUrl, true)
|
||||
val response = appLinksInterceptor.handleRedirect(testRedirect, webUrl, "", true)
|
||||
assert(response is RequestInterceptor.InterceptionResponse.Url)
|
||||
}
|
||||
|
||||
@ -560,7 +616,7 @@ class AppLinksInterceptorTest {
|
||||
useCases = mockUseCases,
|
||||
)
|
||||
|
||||
addUserDoNotIntercept("https://soundcloud.com", null)
|
||||
addUserDoNotIntercept("https://soundcloud.com", null, "")
|
||||
|
||||
val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, true, false, false, false, false)
|
||||
assertNull(response)
|
||||
@ -576,9 +632,9 @@ class AppLinksInterceptorTest {
|
||||
launchFromInterceptor = true,
|
||||
)
|
||||
|
||||
addUserDoNotIntercept(intentUrl, null)
|
||||
addUserDoNotIntercept(intentUrl, null, "")
|
||||
val testRedirect = AppLinkRedirect(Intent.parseUri(intentUrl, 0), "", fallbackUrl, null)
|
||||
val response = appLinksInterceptor.handleRedirect(testRedirect, intentUrl, true)
|
||||
val response = appLinksInterceptor.handleRedirect(testRedirect, intentUrl, "", true)
|
||||
assert(response is RequestInterceptor.InterceptionResponse.Url)
|
||||
}
|
||||
|
||||
@ -598,7 +654,7 @@ class AppLinksInterceptorTest {
|
||||
)
|
||||
|
||||
val notSupportedUrl = "$notSupportedScheme://example.com"
|
||||
addUserDoNotIntercept(notSupportedUrl, null)
|
||||
addUserDoNotIntercept(notSupportedUrl, null, "")
|
||||
val notSupportedRedirect = AppLinkRedirect(Intent.parseUri(notSupportedUrl, 0), "", null, null)
|
||||
whenever(mockGetRedirect.invoke(notSupportedUrl)).thenReturn(notSupportedRedirect)
|
||||
val response = feature.onLoadRequest(engineSession, notSupportedUrl, null, false, false, false, false, false)
|
||||
@ -607,26 +663,26 @@ class AppLinksInterceptorTest {
|
||||
|
||||
@Test
|
||||
fun `WHEN added to user do not open cache THEN return true if user do no intercept cache exists`() {
|
||||
addUserDoNotIntercept("test://test.com", null)
|
||||
assertTrue(inUserDoNotIntercept("test://test.com", null))
|
||||
assertFalse(inUserDoNotIntercept("https://test.com", null))
|
||||
addUserDoNotIntercept("test://test.com", null, "")
|
||||
assertTrue(inUserDoNotIntercept("test://test.com", null, ""))
|
||||
assertFalse(inUserDoNotIntercept("https://test.com", null, ""))
|
||||
|
||||
addUserDoNotIntercept("http://test.com", null)
|
||||
assertTrue(inUserDoNotIntercept("https://test.com", null))
|
||||
assertFalse(inUserDoNotIntercept("https://example.com", null))
|
||||
addUserDoNotIntercept("http://test.com", null, "")
|
||||
assertTrue(inUserDoNotIntercept("https://test.com", null, ""))
|
||||
assertFalse(inUserDoNotIntercept("https://example.com", null, ""))
|
||||
|
||||
val testIntent: Intent = mock()
|
||||
val componentName: ComponentName = mock()
|
||||
doReturn(componentName).`when`(testIntent).component
|
||||
doReturn("app.example.com").`when`(componentName).packageName
|
||||
|
||||
addUserDoNotIntercept("https://example.com", testIntent)
|
||||
assertTrue(inUserDoNotIntercept("https://example.com", testIntent))
|
||||
assertTrue(inUserDoNotIntercept("https://test.com", testIntent))
|
||||
addUserDoNotIntercept("https://example.com", testIntent, "")
|
||||
assertTrue(inUserDoNotIntercept("https://example.com", testIntent, ""))
|
||||
assertTrue(inUserDoNotIntercept("https://test.com", testIntent, ""))
|
||||
|
||||
doReturn("app.test.com").`when`(componentName).packageName
|
||||
assertFalse(inUserDoNotIntercept("https://test.com", testIntent))
|
||||
assertFalse(inUserDoNotIntercept("https://mozilla.org", null))
|
||||
assertFalse(inUserDoNotIntercept("https://test.com", testIntent, ""))
|
||||
assertFalse(inUserDoNotIntercept("https://mozilla.org", null, ""))
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -636,13 +692,13 @@ class AppLinksInterceptorTest {
|
||||
doReturn(componentName).`when`(testIntent).component
|
||||
doReturn("app.example.com").`when`(componentName).packageName
|
||||
|
||||
addUserDoNotIntercept("https://example.com", testIntent)
|
||||
assertTrue(inUserDoNotIntercept("https://example.com", testIntent))
|
||||
assertTrue(inUserDoNotIntercept("https://test.com", testIntent))
|
||||
addUserDoNotIntercept("https://example.com", testIntent, "")
|
||||
assertTrue(inUserDoNotIntercept("https://example.com", testIntent, ""))
|
||||
assertTrue(inUserDoNotIntercept("https://test.com", testIntent, ""))
|
||||
|
||||
userDoNotInterceptCache["app.example.com".hashCode()] = -APP_LINKS_DO_NOT_OPEN_CACHE_INTERVAL
|
||||
assertFalse(inUserDoNotIntercept("https://example.com", testIntent))
|
||||
assertFalse(inUserDoNotIntercept("https://test.com", testIntent))
|
||||
assertFalse(inUserDoNotIntercept("https://example.com", testIntent, ""))
|
||||
assertFalse(inUserDoNotIntercept("https://test.com", testIntent, ""))
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -676,4 +732,354 @@ class AppLinksInterceptorTest {
|
||||
response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, true, false, false, false, false)
|
||||
assertTrue(response is RequestInterceptor.InterceptionResponse.AppIntent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN should prompt AND in non-private mode THEN an external app dialog is shown`() {
|
||||
appLinksInterceptor = spy(
|
||||
AppLinksInterceptor(
|
||||
context = mockContext,
|
||||
store = store,
|
||||
interceptLinkClicks = true,
|
||||
launchInApp = { true },
|
||||
useCases = mockUseCases,
|
||||
shouldPrompt = { true },
|
||||
),
|
||||
)
|
||||
|
||||
appLinksInterceptor.updateFragmentManger(mockFragmentManager)
|
||||
|
||||
val tab = createTab(webUrl)
|
||||
val mockAppIntent: Intent = mock()
|
||||
val mockComponentName: ComponentName = mock()
|
||||
whenever(mockAppIntent.component).thenReturn(mockComponentName)
|
||||
whenever(mockComponentName.packageName).thenReturn("com.zxing.app")
|
||||
doReturn(mockDialog).`when`(appLinksInterceptor).getOrCreateDialog(anyBoolean(), any())
|
||||
|
||||
appLinksInterceptor.handleAppIntent(tab, intentUrl, mockAppIntent)
|
||||
|
||||
verify(mockDialog).showNow(eq(mockFragmentManager), anyString())
|
||||
verify(mockOpenRedirect, never()).invoke(any(), anyBoolean(), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN should not prompt AND in non-private mode THEN an external app dialog is not shown`() {
|
||||
appLinksInterceptor = spy(
|
||||
AppLinksInterceptor(
|
||||
context = mockContext,
|
||||
store = store,
|
||||
interceptLinkClicks = true,
|
||||
launchInApp = { true },
|
||||
useCases = mockUseCases,
|
||||
shouldPrompt = { false },
|
||||
),
|
||||
)
|
||||
|
||||
appLinksInterceptor.updateFragmentManger(mockFragmentManager)
|
||||
|
||||
val tab = createTab(webUrl)
|
||||
appLinksInterceptor.handleAppIntent(tab, intentUrl, mock())
|
||||
|
||||
verify(mockDialog, never()).showNow(eq(mockFragmentManager), anyString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN custom tab and caller is the same as external app THEN an external app dialog is not shown`() {
|
||||
appLinksInterceptor = spy(
|
||||
AppLinksInterceptor(
|
||||
context = mockContext,
|
||||
store = store,
|
||||
interceptLinkClicks = true,
|
||||
launchInApp = { true },
|
||||
useCases = mockUseCases,
|
||||
shouldPrompt = { true },
|
||||
),
|
||||
)
|
||||
|
||||
appLinksInterceptor.updateFragmentManger(mockFragmentManager)
|
||||
|
||||
val tab = createCustomTab(
|
||||
id = "c",
|
||||
url = webUrl,
|
||||
source = SessionState.Source.External.CustomTab(
|
||||
ExternalPackage("com.zxing.app", PackageCategory.PRODUCTIVITY),
|
||||
),
|
||||
)
|
||||
|
||||
val appIntent: Intent = mock()
|
||||
val componentName: ComponentName = mock()
|
||||
whenever(appIntent.component).thenReturn(componentName)
|
||||
whenever(componentName.packageName).thenReturn("com.zxing.app")
|
||||
|
||||
appLinksInterceptor.handleAppIntent(tab, intentUrl, appIntent)
|
||||
|
||||
verify(mockDialog, never()).showNow(eq(mockFragmentManager), anyString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN tab have action view and caller is the same as external app THEN an external app dialog is not shown`() {
|
||||
appLinksInterceptor = spy(
|
||||
AppLinksInterceptor(
|
||||
context = mockContext,
|
||||
store = store,
|
||||
interceptLinkClicks = true,
|
||||
launchInApp = { true },
|
||||
useCases = mockUseCases,
|
||||
shouldPrompt = { true },
|
||||
),
|
||||
)
|
||||
|
||||
appLinksInterceptor.updateFragmentManger(mockFragmentManager)
|
||||
|
||||
val tab = createCustomTab(
|
||||
id = "d",
|
||||
url = webUrl,
|
||||
source = SessionState.Source.External.ActionView(
|
||||
ExternalPackage("com.zxing.app", PackageCategory.PRODUCTIVITY),
|
||||
),
|
||||
)
|
||||
|
||||
val appIntent: Intent = mock()
|
||||
val componentName: ComponentName = mock()
|
||||
whenever(appIntent.component).thenReturn(componentName)
|
||||
whenever(componentName.packageName).thenReturn("com.zxing.app")
|
||||
|
||||
appLinksInterceptor.handleAppIntent(tab, intentUrl, appIntent)
|
||||
|
||||
verify(mockDialog, never()).showNow(eq(mockFragmentManager), anyString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN tab have action send and caller is the same as external app THEN an external app dialog is shown`() {
|
||||
appLinksInterceptor = spy(
|
||||
AppLinksInterceptor(
|
||||
context = mockContext,
|
||||
store = store,
|
||||
interceptLinkClicks = true,
|
||||
launchInApp = { true },
|
||||
useCases = mockUseCases,
|
||||
shouldPrompt = { true },
|
||||
),
|
||||
)
|
||||
|
||||
appLinksInterceptor.updateFragmentManger(mockFragmentManager)
|
||||
|
||||
val tab = createCustomTab(
|
||||
id = "d",
|
||||
url = webUrl,
|
||||
source = SessionState.Source.External.ActionSend(
|
||||
ExternalPackage("com.zxing.app", PackageCategory.PRODUCTIVITY),
|
||||
),
|
||||
)
|
||||
|
||||
val appIntent: Intent = mock()
|
||||
val componentName: ComponentName = mock()
|
||||
whenever(appIntent.component).thenReturn(componentName)
|
||||
whenever(componentName.packageName).thenReturn("com.zxing.app")
|
||||
doReturn(mockDialog).`when`(appLinksInterceptor).getOrCreateDialog(anyBoolean(), any())
|
||||
|
||||
appLinksInterceptor.handleAppIntent(tab, intentUrl, appIntent)
|
||||
|
||||
verify(mockDialog).showNow(eq(mockFragmentManager), anyString())
|
||||
verify(mockOpenRedirect, never()).invoke(any(), anyBoolean(), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN tab have action search and caller is the same as external app THEN an external app dialog is shown`() {
|
||||
appLinksInterceptor = spy(
|
||||
AppLinksInterceptor(
|
||||
context = mockContext,
|
||||
store = store,
|
||||
interceptLinkClicks = true,
|
||||
launchInApp = { true },
|
||||
useCases = mockUseCases,
|
||||
shouldPrompt = { true },
|
||||
),
|
||||
)
|
||||
|
||||
appLinksInterceptor.updateFragmentManger(mockFragmentManager)
|
||||
|
||||
val tab = createCustomTab(
|
||||
id = "d",
|
||||
url = webUrl,
|
||||
source = SessionState.Source.External.ActionSearch(
|
||||
ExternalPackage("com.zxing.app", PackageCategory.PRODUCTIVITY),
|
||||
),
|
||||
)
|
||||
|
||||
val appIntent: Intent = mock()
|
||||
val componentName: ComponentName = mock()
|
||||
whenever(appIntent.component).thenReturn(componentName)
|
||||
whenever(componentName.packageName).thenReturn("com.zxing.app")
|
||||
doReturn(mockDialog).`when`(appLinksInterceptor).getOrCreateDialog(anyBoolean(), any())
|
||||
|
||||
appLinksInterceptor.handleAppIntent(tab, intentUrl, appIntent)
|
||||
|
||||
verify(mockDialog).showNow(eq(mockFragmentManager), anyString())
|
||||
verify(mockOpenRedirect, never()).invoke(any(), anyBoolean(), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN should prompt and in private mode THEN an external app dialog is shown`() {
|
||||
appLinksInterceptor = spy(
|
||||
AppLinksInterceptor(
|
||||
context = mockContext,
|
||||
store = store,
|
||||
interceptLinkClicks = true,
|
||||
launchInApp = { true },
|
||||
useCases = mockUseCases,
|
||||
shouldPrompt = { true },
|
||||
),
|
||||
)
|
||||
|
||||
appLinksInterceptor.updateFragmentManger(mockFragmentManager)
|
||||
|
||||
val tabSessionState = TabSessionState(
|
||||
id = "tab1",
|
||||
content = ContentState(
|
||||
url = "https://mozilla.org",
|
||||
private = true,
|
||||
isProductUrl = false,
|
||||
),
|
||||
)
|
||||
val mockAppIntent: Intent = mock()
|
||||
val mockComponentName: ComponentName = mock()
|
||||
whenever(mockAppIntent.component).thenReturn(mockComponentName)
|
||||
whenever(mockComponentName.packageName).thenReturn("com.zxing.app")
|
||||
doReturn(mockDialog).`when`(appLinksInterceptor).getOrCreateDialog(anyBoolean(), any())
|
||||
|
||||
appLinksInterceptor.handleAppIntent(tabSessionState, intentUrl, mockAppIntent)
|
||||
|
||||
verify(mockDialog).showNow(eq(mockFragmentManager), anyString())
|
||||
verify(mockOpenRedirect, never()).invoke(any(), anyBoolean(), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN should not prompt and in private mode THEN an external app dialog is shown`() {
|
||||
appLinksInterceptor = spy(
|
||||
AppLinksInterceptor(
|
||||
context = mockContext,
|
||||
store = store,
|
||||
interceptLinkClicks = true,
|
||||
launchInApp = { true },
|
||||
useCases = mockUseCases,
|
||||
shouldPrompt = { false },
|
||||
),
|
||||
)
|
||||
|
||||
appLinksInterceptor.updateFragmentManger(mockFragmentManager)
|
||||
|
||||
val tabSessionState = TabSessionState(
|
||||
id = "tab1",
|
||||
content = ContentState(
|
||||
url = "https://mozilla.org",
|
||||
private = true,
|
||||
isProductUrl = false,
|
||||
),
|
||||
)
|
||||
val mockAppIntent: Intent = mock()
|
||||
val mockComponentName: ComponentName = mock()
|
||||
whenever(mockAppIntent.component).thenReturn(mockComponentName)
|
||||
whenever(mockComponentName.packageName).thenReturn("com.zxing.app")
|
||||
doReturn(mockDialog).`when`(appLinksInterceptor).getOrCreateDialog(anyBoolean(), any())
|
||||
|
||||
appLinksInterceptor.handleAppIntent(tabSessionState, intentUrl, mockAppIntent)
|
||||
|
||||
verify(mockDialog).showNow(eq(mockFragmentManager), anyString())
|
||||
verify(mockOpenRedirect, never()).invoke(any(), anyBoolean(), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `redirect dialog is only added once`() {
|
||||
appLinksInterceptor = spy(
|
||||
AppLinksInterceptor(
|
||||
context = mockContext,
|
||||
store = store,
|
||||
interceptLinkClicks = true,
|
||||
launchInApp = { true },
|
||||
useCases = mockUseCases,
|
||||
shouldPrompt = { true },
|
||||
),
|
||||
)
|
||||
|
||||
appLinksInterceptor.updateFragmentManger(mockFragmentManager)
|
||||
val tabSessionState = TabSessionState(
|
||||
id = "tab1",
|
||||
content = ContentState(
|
||||
url = "https://mozilla.org",
|
||||
private = false,
|
||||
isProductUrl = false,
|
||||
),
|
||||
)
|
||||
|
||||
val appIntent: Intent = mock()
|
||||
val componentName: ComponentName = mock()
|
||||
whenever(appIntent.component).thenReturn(componentName)
|
||||
whenever(componentName.packageName).thenReturn("com.zxing.app")
|
||||
doReturn(mockDialog).`when`(appLinksInterceptor).getOrCreateDialog(anyBoolean(), any())
|
||||
|
||||
appLinksInterceptor.handleAppIntent(tabSessionState, intentUrl, appIntent)
|
||||
|
||||
verify(mockDialog).showNow(eq(mockFragmentManager), anyString())
|
||||
|
||||
doReturn(mockDialog).`when`(appLinksInterceptor).getOrCreateDialog(false, "")
|
||||
doReturn(mockDialog).`when`(mockFragmentManager).findFragmentByTag(RedirectDialogFragment.FRAGMENT_TAG)
|
||||
appLinksInterceptor.handleAppIntent(tabSessionState, intentUrl, mock())
|
||||
verify(mockDialog, times(1)).showNow(mockFragmentManager, RedirectDialogFragment.FRAGMENT_TAG)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN caller and intent have the same package name THEN return true`() {
|
||||
appLinksInterceptor = spy(
|
||||
AppLinksInterceptor(
|
||||
context = mockContext,
|
||||
store = store,
|
||||
interceptLinkClicks = true,
|
||||
launchInApp = { true },
|
||||
useCases = mockUseCases,
|
||||
shouldPrompt = { true },
|
||||
),
|
||||
)
|
||||
|
||||
appLinksInterceptor.updateFragmentManger(mockFragmentManager)
|
||||
|
||||
val tabSessionState = TabSessionState(
|
||||
id = "tab1",
|
||||
content = ContentState(
|
||||
url = "https://mozilla.org",
|
||||
private = false,
|
||||
isProductUrl = false,
|
||||
),
|
||||
source = SessionState.Source.External.CustomTab(ExternalPackage("com.zxing.app", PackageCategory.PRODUCTIVITY)),
|
||||
)
|
||||
|
||||
val appIntent: Intent = mock()
|
||||
val componentName: ComponentName = mock()
|
||||
whenever(appIntent.component).thenReturn(componentName)
|
||||
whenever(componentName.packageName).thenReturn("com.zxing.app")
|
||||
assertTrue(appLinksInterceptor.isAuthentication(tabSessionState, appIntent))
|
||||
|
||||
val tabSessionState2 = TabSessionState(
|
||||
id = "tab1",
|
||||
content = ContentState(
|
||||
url = "https://mozilla.org",
|
||||
private = false,
|
||||
isProductUrl = false,
|
||||
),
|
||||
)
|
||||
assertFalse(appLinksInterceptor.isAuthentication(tabSessionState2, appIntent))
|
||||
|
||||
val tabSessionState3 = TabSessionState(
|
||||
id = "tab1",
|
||||
content = ContentState(
|
||||
url = "https://mozilla.org",
|
||||
private = false,
|
||||
isProductUrl = false,
|
||||
),
|
||||
source = SessionState.Source.External.CustomTab(ExternalPackage("com.example.app", PackageCategory.PRODUCTIVITY)),
|
||||
)
|
||||
assertFalse(appLinksInterceptor.isAuthentication(tabSessionState3, appIntent))
|
||||
|
||||
doReturn(null).`when`(componentName).packageName
|
||||
assertFalse(appLinksInterceptor.isAuthentication(tabSessionState, appIntent))
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ import androidx.fragment.app.Fragment
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
|
||||
import mozilla.components.browser.toolbar.display.DisplayToolbar
|
||||
import mozilla.components.feature.app.links.AppLinksFeature
|
||||
import mozilla.components.feature.downloads.DownloadsFeature
|
||||
import mozilla.components.feature.downloads.manager.FetchDownloadManager
|
||||
import mozilla.components.feature.privatemode.feature.SecureWindowFeature
|
||||
@ -53,7 +52,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
||||
private val toolbarFeature = ViewBoundFeatureWrapper<ToolbarFeature>()
|
||||
private val contextMenuIntegration = ViewBoundFeatureWrapper<ContextMenuIntegration>()
|
||||
private val downloadsFeature = ViewBoundFeatureWrapper<DownloadsFeature>()
|
||||
private val appLinksFeature = ViewBoundFeatureWrapper<AppLinksFeature>()
|
||||
private val promptFeature = ViewBoundFeatureWrapper<PromptFeature>()
|
||||
private val findInPageIntegration = ViewBoundFeatureWrapper<FindInPageIntegration>()
|
||||
private val sitePermissionsFeature = ViewBoundFeatureWrapper<SitePermissionsFeature>()
|
||||
@ -167,19 +165,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
||||
view = binding.root,
|
||||
)
|
||||
|
||||
appLinksFeature.set(
|
||||
feature = AppLinksFeature(
|
||||
context = requireContext(),
|
||||
store = components.store,
|
||||
sessionId = sessionId,
|
||||
fragmentManager = parentFragmentManager,
|
||||
launchInApp = { components.preferences.getBoolean(DefaultComponents.PREF_LAUNCH_EXTERNAL_APP, false) },
|
||||
loadUrlUseCase = components.sessionUseCases.loadUrl,
|
||||
),
|
||||
owner = this,
|
||||
view = binding.root,
|
||||
)
|
||||
|
||||
promptFeature.set(
|
||||
feature = PromptFeature(
|
||||
fragment = this,
|
||||
@ -255,6 +240,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
||||
customTabId = sessionId,
|
||||
)
|
||||
|
||||
components.appLinksInterceptor.updateFragmentManger(
|
||||
fragmentManager = parentFragmentManager,
|
||||
)
|
||||
|
||||
// Observe the lifecycle for supported features
|
||||
lifecycle.addObservers(
|
||||
scrollFeature,
|
||||
@ -303,6 +292,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
||||
}
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
components.appLinksInterceptor.updateFragmentManger(
|
||||
fragmentManager = null,
|
||||
)
|
||||
|
||||
binding.engineView.setActivityContext(null)
|
||||
_binding = null
|
||||
}
|
||||
|
@ -254,6 +254,8 @@ open class DefaultComponents(private val applicationContext: Context) {
|
||||
launchInApp = {
|
||||
applicationContext.components.preferences.getBoolean(PREF_LAUNCH_EXTERNAL_APP, false)
|
||||
},
|
||||
launchFromInterceptor = true,
|
||||
store = store,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -86,7 +86,6 @@ import mozilla.components.concept.engine.permission.SitePermissions
|
||||
import mozilla.components.concept.engine.prompt.ShareData
|
||||
import mozilla.components.concept.storage.LoginEntry
|
||||
import mozilla.components.feature.accounts.push.SendTabUseCases
|
||||
import mozilla.components.feature.app.links.AppLinksFeature
|
||||
import mozilla.components.feature.contextmenu.ContextMenuCandidate
|
||||
import mozilla.components.feature.contextmenu.ContextMenuFeature
|
||||
import mozilla.components.feature.downloads.DownloadsFeature
|
||||
@ -293,7 +292,6 @@ abstract class BaseBrowserFragment :
|
||||
private val downloadsFeature = ViewBoundFeatureWrapper<DownloadsFeature>()
|
||||
private val shareDownloadsFeature = ViewBoundFeatureWrapper<ShareDownloadFeature>()
|
||||
private val copyDownloadsFeature = ViewBoundFeatureWrapper<CopyDownloadFeature>()
|
||||
private val appLinksFeature = ViewBoundFeatureWrapper<AppLinksFeature>()
|
||||
private val promptsFeature = ViewBoundFeatureWrapper<PromptFeature>()
|
||||
|
||||
@VisibleForTesting
|
||||
@ -889,29 +887,6 @@ abstract class BaseBrowserFragment :
|
||||
tabId = customTabSessionId,
|
||||
)
|
||||
|
||||
appLinksFeature.set(
|
||||
feature = AppLinksFeature(
|
||||
context,
|
||||
store = store,
|
||||
sessionId = customTabSessionId,
|
||||
fragmentManager = parentFragmentManager,
|
||||
launchInApp = { context.settings().shouldOpenLinksInApp(customTabSessionId != null) },
|
||||
loadUrlUseCase = context.components.useCases.sessionUseCases.loadUrl,
|
||||
shouldPrompt = { context.settings().shouldPromptOpenLinksInApp() },
|
||||
failedToLaunchAction = { fallbackUrl ->
|
||||
fallbackUrl?.let {
|
||||
val appLinksUseCases = activity.components.useCases.appLinksUseCases
|
||||
val getRedirect = appLinksUseCases.appLinkRedirect
|
||||
val redirect = getRedirect.invoke(fallbackUrl)
|
||||
redirect.appIntent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
appLinksUseCases.openAppLink.invoke(redirect.appIntent)
|
||||
}
|
||||
},
|
||||
),
|
||||
owner = this,
|
||||
view = view,
|
||||
)
|
||||
|
||||
biometricPromptFeature.set(
|
||||
feature = BiometricPromptFeature(
|
||||
context = context,
|
||||
@ -1991,6 +1966,9 @@ abstract class BaseBrowserFragment :
|
||||
}
|
||||
hideToolbar()
|
||||
|
||||
components.services.appLinksInterceptor.updateFragmentManger(
|
||||
fragmentManager = parentFragmentManager,
|
||||
)
|
||||
context?.settings()?.shouldOpenLinksInApp(customTabSessionId != null)
|
||||
?.let { openLinksInExternalApp ->
|
||||
components.services.appLinksInterceptor.updateLaunchInApp {
|
||||
@ -2497,6 +2475,9 @@ abstract class BaseBrowserFragment :
|
||||
|
||||
binding.engineView.setActivityContext(null)
|
||||
requireContext().accessibilityManager.removeAccessibilityStateChangeListener(this)
|
||||
requireContext().components.services.appLinksInterceptor.updateFragmentManger(
|
||||
fragmentManager = null,
|
||||
)
|
||||
|
||||
_bottomToolbarContainerView = null
|
||||
_browserToolbarView = null
|
||||
|
@ -77,7 +77,7 @@ class Components(private val context: Context) {
|
||||
strictMode,
|
||||
)
|
||||
}
|
||||
val services by lazyMonitored { Services(context, backgroundServices.accountManager) }
|
||||
val services by lazyMonitored { Services(context, core.store, backgroundServices.accountManager) }
|
||||
val core by lazyMonitored { Core(context, analytics.crashReporter, strictMode) }
|
||||
|
||||
@Suppress("Deprecation")
|
||||
|
@ -9,6 +9,7 @@ import android.net.Uri
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.feature.accounts.FirefoxAccountsAuthFeature
|
||||
import mozilla.components.feature.app.links.AppLinksInterceptor
|
||||
import mozilla.components.service.fxa.manager.FxaAccountManager
|
||||
@ -21,6 +22,7 @@ import org.mozilla.fenix.settings.SupportUtils
|
||||
*/
|
||||
class Services(
|
||||
private val context: Context,
|
||||
private val store: BrowserStore,
|
||||
private val accountManager: FxaAccountManager,
|
||||
) {
|
||||
val accountsAuthFeature by lazyMonitored {
|
||||
@ -43,9 +45,12 @@ class Services(
|
||||
|
||||
val appLinksInterceptor by lazyMonitored {
|
||||
AppLinksInterceptor(
|
||||
context,
|
||||
context = context,
|
||||
interceptLinkClicks = true,
|
||||
launchInApp = { context.settings().shouldOpenLinksInApp() },
|
||||
shouldPrompt = { context.settings().shouldPromptOpenLinksInApp() },
|
||||
launchFromInterceptor = true,
|
||||
store = store,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -271,7 +271,7 @@ class DefaultTabsTrayControllerTest {
|
||||
every { browserStore.state } returns mockk()
|
||||
try {
|
||||
mockkStatic("mozilla.components.browser.state.selector.SelectorsKt")
|
||||
every { browserStore.state.findTab(any()) } returns tab
|
||||
every { browserStore.state.findTab("testTabId") } returns tab
|
||||
every { browserStore.state.getNormalOrPrivateTabs(any()) } returns listOf(tab)
|
||||
|
||||
createController().handleTabDeletion("testTabId", "unknown")
|
||||
@ -303,7 +303,7 @@ class DefaultTabsTrayControllerTest {
|
||||
)
|
||||
try {
|
||||
mockkStatic("mozilla.components.browser.state.selector.SelectorsKt")
|
||||
every { browserStore.state.findTab(any()) } returns tab
|
||||
every { browserStore.state.findTab("testTabId") } returns tab
|
||||
every { browserStore.state.getNormalOrPrivateTabs(any()) } returns listOf(tab)
|
||||
every { browserStore.state.selectedTabId } returns "testTabId"
|
||||
|
||||
@ -419,7 +419,7 @@ class DefaultTabsTrayControllerTest {
|
||||
every { browserStore.state } returns mockk()
|
||||
try {
|
||||
mockkStatic("mozilla.components.browser.state.selector.SelectorsKt")
|
||||
every { browserStore.state.findTab(any()) } returns tab
|
||||
every { browserStore.state.findTab("22") } returns tab
|
||||
every { browserStore.state.getNormalOrPrivateTabs(any()) } returns listOf(tab, mockk())
|
||||
|
||||
var showUndoSnackbarForTabInvoked = false
|
||||
@ -449,7 +449,7 @@ class DefaultTabsTrayControllerTest {
|
||||
try {
|
||||
val testTabId = "33"
|
||||
mockkStatic("mozilla.components.browser.state.selector.SelectorsKt")
|
||||
every { browserStore.state.findTab(any()) } returns tab
|
||||
every { browserStore.state.findTab(testTabId) } returns tab
|
||||
every { browserStore.state.getNormalOrPrivateTabs(any()) } returns listOf(tab)
|
||||
every { browserStore.state.selectedTabId } returns testTabId
|
||||
|
||||
|
@ -30,6 +30,7 @@ import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.RuleChain
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentMatchers.anyString
|
||||
import org.mozilla.fenix.GleanMetrics.Events
|
||||
import org.mozilla.fenix.GleanMetrics.TabsTray
|
||||
import org.mozilla.fenix.components.accounts.FenixFxAEntryPoint
|
||||
@ -146,7 +147,7 @@ class NavigationInteractorTest {
|
||||
)
|
||||
try {
|
||||
mockkStatic("mozilla.components.browser.state.selector.SelectorsKt")
|
||||
every { mockedStore.state.findTab(any()) } returns tab
|
||||
every { mockedStore.state.findTab(anyString()) } returns tab
|
||||
every { mockedStore.state.getNormalOrPrivateTabs(any()) } returns listOf(tab)
|
||||
|
||||
controller.onCloseAllTabsClicked(true)
|
||||
|
Loading…
Reference in New Issue
Block a user