Bug 1929028 - Use AppLinksFeature to prompt the user. r=android-reviewers,tthibaud
Differential Revision: https://phabricator.services.mozilla.com/D247410
This commit is contained in:
committed by
royang@mozilla.com
parent
9e25ff9fd3
commit
5957e9d4df
@@ -1023,7 +1023,12 @@ class GeckoEngineSession(
|
|||||||
is InterceptionResponse.AppIntent -> {
|
is InterceptionResponse.AppIntent -> {
|
||||||
appRedirectUrl = lastLoadRequestUri
|
appRedirectUrl = lastLoadRequestUri
|
||||||
notifyObservers {
|
notifyObservers {
|
||||||
onLaunchIntentRequest(url = url, appIntent = appIntent)
|
onLaunchIntentRequest(
|
||||||
|
url = url,
|
||||||
|
appIntent = appIntent,
|
||||||
|
fallbackUrl = fallbackUrl,
|
||||||
|
appName = appName,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,8 +112,6 @@ typealias GeckoAntiTracking = ContentBlocking.AntiTracking
|
|||||||
typealias GeckoSafeBrowsing = ContentBlocking.SafeBrowsing
|
typealias GeckoSafeBrowsing = ContentBlocking.SafeBrowsing
|
||||||
typealias GeckoCookieBehavior = ContentBlocking.CookieBehavior
|
typealias GeckoCookieBehavior = ContentBlocking.CookieBehavior
|
||||||
|
|
||||||
private const val AID = "AID"
|
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class GeckoEngineSessionTest {
|
class GeckoEngineSessionTest {
|
||||||
@@ -3380,6 +3378,8 @@ class GeckoEngineSessionTest {
|
|||||||
|
|
||||||
var observedUrl: String? = null
|
var observedUrl: String? = null
|
||||||
var observedIntent: Intent? = null
|
var observedIntent: Intent? = null
|
||||||
|
var observedFallbackUrl: String? = null
|
||||||
|
var observedAppName: String? = null
|
||||||
|
|
||||||
var observedLoadUrl: String? = null
|
var observedLoadUrl: String? = null
|
||||||
var observedTriggeredByRedirect: Boolean? = null
|
var observedTriggeredByRedirect: Boolean? = null
|
||||||
@@ -3399,7 +3399,7 @@ class GeckoEngineSessionTest {
|
|||||||
isSubframeRequest: Boolean,
|
isSubframeRequest: Boolean,
|
||||||
): RequestInterceptor.InterceptionResponse? {
|
): RequestInterceptor.InterceptionResponse? {
|
||||||
return when (uri) {
|
return when (uri) {
|
||||||
"sample:about" -> RequestInterceptor.InterceptionResponse.AppIntent(mock(), "result")
|
"sample:about" -> RequestInterceptor.InterceptionResponse.AppIntent(mock(), "result", "fallback", "app")
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3410,9 +3410,13 @@ class GeckoEngineSessionTest {
|
|||||||
override fun onLaunchIntentRequest(
|
override fun onLaunchIntentRequest(
|
||||||
url: String,
|
url: String,
|
||||||
appIntent: Intent?,
|
appIntent: Intent?,
|
||||||
|
fallbackUrl: String?,
|
||||||
|
appName: String?,
|
||||||
) {
|
) {
|
||||||
observedUrl = url
|
observedUrl = url
|
||||||
observedIntent = appIntent
|
observedIntent = appIntent
|
||||||
|
observedFallbackUrl = fallbackUrl
|
||||||
|
observedAppName = appName
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoadRequest(url: String, triggeredByRedirect: Boolean, triggeredByWebContent: Boolean) {
|
override fun onLoadRequest(url: String, triggeredByRedirect: Boolean, triggeredByWebContent: Boolean) {
|
||||||
@@ -3436,6 +3440,8 @@ class GeckoEngineSessionTest {
|
|||||||
assertEquals(result!!.poll(0), AllowOrDeny.DENY)
|
assertEquals(result!!.poll(0), AllowOrDeny.DENY)
|
||||||
assertNotNull(observedIntent)
|
assertNotNull(observedIntent)
|
||||||
assertEquals("result", observedUrl)
|
assertEquals("result", observedUrl)
|
||||||
|
assertNotNull(observedFallbackUrl)
|
||||||
|
assertNotNull(observedAppName)
|
||||||
assertNull(observedLoadUrl)
|
assertNull(observedLoadUrl)
|
||||||
assertNull(observedTriggeredByRedirect)
|
assertNull(observedTriggeredByRedirect)
|
||||||
assertNull(observedTriggeredByWebContent)
|
assertNull(observedTriggeredByWebContent)
|
||||||
@@ -3469,6 +3475,8 @@ class GeckoEngineSessionTest {
|
|||||||
|
|
||||||
var observedUrl: String? = null
|
var observedUrl: String? = null
|
||||||
var observedIntent: Intent? = null
|
var observedIntent: Intent? = null
|
||||||
|
var observedFallbackUrl: String? = null
|
||||||
|
var observedAppName: String? = null
|
||||||
|
|
||||||
var observedLoadUrl: String? = null
|
var observedLoadUrl: String? = null
|
||||||
var observedTriggeredByRedirect: Boolean? = null
|
var observedTriggeredByRedirect: Boolean? = null
|
||||||
@@ -3488,7 +3496,7 @@ class GeckoEngineSessionTest {
|
|||||||
isSubframeRequest: Boolean,
|
isSubframeRequest: Boolean,
|
||||||
): RequestInterceptor.InterceptionResponse? {
|
): RequestInterceptor.InterceptionResponse? {
|
||||||
return when (uri) {
|
return when (uri) {
|
||||||
"sample:about" -> RequestInterceptor.InterceptionResponse.AppIntent(mock(), "result")
|
"sample:about" -> RequestInterceptor.InterceptionResponse.AppIntent(mock(), "result", "fallback", "app")
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3499,9 +3507,13 @@ class GeckoEngineSessionTest {
|
|||||||
override fun onLaunchIntentRequest(
|
override fun onLaunchIntentRequest(
|
||||||
url: String,
|
url: String,
|
||||||
appIntent: Intent?,
|
appIntent: Intent?,
|
||||||
|
fallbackUrl: String?,
|
||||||
|
appName: String?,
|
||||||
) {
|
) {
|
||||||
observedUrl = url
|
observedUrl = url
|
||||||
observedIntent = appIntent
|
observedIntent = appIntent
|
||||||
|
observedFallbackUrl = fallbackUrl
|
||||||
|
observedAppName = appName
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoadRequest(url: String, triggeredByRedirect: Boolean, triggeredByWebContent: Boolean) {
|
override fun onLoadRequest(url: String, triggeredByRedirect: Boolean, triggeredByWebContent: Boolean) {
|
||||||
@@ -3526,6 +3538,8 @@ class GeckoEngineSessionTest {
|
|||||||
assertNull(observedIntent)
|
assertNull(observedIntent)
|
||||||
assertNull(observedUrl)
|
assertNull(observedUrl)
|
||||||
assertNull(observedLoadUrl)
|
assertNull(observedLoadUrl)
|
||||||
|
assertNull(observedFallbackUrl)
|
||||||
|
assertNull(observedAppName)
|
||||||
assertNull(observedTriggeredByRedirect)
|
assertNull(observedTriggeredByRedirect)
|
||||||
assertNull(observedTriggeredByWebContent)
|
assertNull(observedTriggeredByWebContent)
|
||||||
|
|
||||||
@@ -3543,6 +3557,8 @@ class GeckoEngineSessionTest {
|
|||||||
assertNull(observedIntent)
|
assertNull(observedIntent)
|
||||||
assertNull(observedUrl)
|
assertNull(observedUrl)
|
||||||
assertNull(observedLoadUrl)
|
assertNull(observedLoadUrl)
|
||||||
|
assertNull(observedFallbackUrl)
|
||||||
|
assertNull(observedAppName)
|
||||||
assertNull(observedTriggeredByRedirect)
|
assertNull(observedTriggeredByRedirect)
|
||||||
assertNull(observedTriggeredByWebContent)
|
assertNull(observedTriggeredByWebContent)
|
||||||
}
|
}
|
||||||
@@ -3574,7 +3590,7 @@ class GeckoEngineSessionTest {
|
|||||||
isSubframeRequest: Boolean,
|
isSubframeRequest: Boolean,
|
||||||
): RequestInterceptor.InterceptionResponse? {
|
): RequestInterceptor.InterceptionResponse? {
|
||||||
return when (uri) {
|
return when (uri) {
|
||||||
"sample:about" -> RequestInterceptor.InterceptionResponse.AppIntent(mock(), "result")
|
"sample:about" -> RequestInterceptor.InterceptionResponse.AppIntent(mock(), "result", null, null)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3724,29 +3740,37 @@ class GeckoEngineSessionTest {
|
|||||||
isSubframeRequest: Boolean,
|
isSubframeRequest: Boolean,
|
||||||
): RequestInterceptor.InterceptionResponse? {
|
): RequestInterceptor.InterceptionResponse? {
|
||||||
return when (uri) {
|
return when (uri) {
|
||||||
"sample:triggeredByRedirect" -> RequestInterceptor.InterceptionResponse.AppIntent(mock(), "result1")
|
"sample:triggeredByRedirect" -> RequestInterceptor.InterceptionResponse.AppIntent(mock(), "result1", "fallback1", "app1")
|
||||||
"sample:NotTriggeredByRedirect" -> RequestInterceptor.InterceptionResponse.AppIntent(mock(), "result2")
|
"sample:NotTriggeredByRedirect" -> RequestInterceptor.InterceptionResponse.AppIntent(mock(), "result2", "fallback2", "app2")
|
||||||
"sample:isDirectNavigation" -> RequestInterceptor.InterceptionResponse.AppIntent(mock(), "result3")
|
"sample:isDirectNavigation" -> RequestInterceptor.InterceptionResponse.AppIntent(mock(), "result3", "fallback3", "app3")
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val observer = object : EngineSession.Observer {
|
val observer = object : EngineSession.Observer {
|
||||||
var url: String? = null
|
var observedUrl: String? = null
|
||||||
var intent: Intent? = null
|
var observedIntent: Intent? = null
|
||||||
|
var observedFallbackUrl: String? = null
|
||||||
|
var observedAppName: String? = null
|
||||||
|
|
||||||
override fun onLaunchIntentRequest(
|
override fun onLaunchIntentRequest(
|
||||||
url: String,
|
url: String,
|
||||||
appIntent: Intent?,
|
appIntent: Intent?,
|
||||||
|
fallbackUrl: String?,
|
||||||
|
appName: String?,
|
||||||
) {
|
) {
|
||||||
this.url = url
|
observedUrl = url
|
||||||
intent = appIntent
|
observedIntent = appIntent
|
||||||
|
observedFallbackUrl = fallbackUrl
|
||||||
|
observedAppName = appName
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
url = null
|
observedUrl = null
|
||||||
intent = null
|
observedIntent = null
|
||||||
|
observedFallbackUrl = null
|
||||||
|
observedAppName = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3757,8 +3781,10 @@ class GeckoEngineSessionTest {
|
|||||||
mockLoadRequest("sample:triggeredByRedirect", triggeredByRedirect = true, isDirectNavigation = false),
|
mockLoadRequest("sample:triggeredByRedirect", triggeredByRedirect = true, isDirectNavigation = false),
|
||||||
)
|
)
|
||||||
|
|
||||||
assertNotNull(observer.intent)
|
assertNotNull(observer.observedIntent)
|
||||||
assertEquals("result1", observer.url)
|
assertEquals("result1", observer.observedUrl)
|
||||||
|
assertEquals("fallback1", observer.observedFallbackUrl)
|
||||||
|
assertEquals("app1", observer.observedAppName)
|
||||||
|
|
||||||
observer.reset()
|
observer.reset()
|
||||||
navigationDelegate.value.onLoadRequest(
|
navigationDelegate.value.onLoadRequest(
|
||||||
@@ -3766,8 +3792,10 @@ class GeckoEngineSessionTest {
|
|||||||
mockLoadRequest("sample:NotTriggeredByRedirect", triggeredByRedirect = false, isDirectNavigation = false),
|
mockLoadRequest("sample:NotTriggeredByRedirect", triggeredByRedirect = false, isDirectNavigation = false),
|
||||||
)
|
)
|
||||||
|
|
||||||
assertNotNull(observer.intent)
|
assertNotNull(observer.observedIntent)
|
||||||
assertEquals("result2", observer.url)
|
assertEquals("result2", observer.observedUrl)
|
||||||
|
assertEquals("fallback2", observer.observedFallbackUrl)
|
||||||
|
assertEquals("app2", observer.observedAppName)
|
||||||
|
|
||||||
observer.reset()
|
observer.reset()
|
||||||
navigationDelegate.value.onLoadRequest(
|
navigationDelegate.value.onLoadRequest(
|
||||||
@@ -3775,8 +3803,10 @@ class GeckoEngineSessionTest {
|
|||||||
mockLoadRequest("sample:isDirectNavigation", triggeredByRedirect = false, isDirectNavigation = true),
|
mockLoadRequest("sample:isDirectNavigation", triggeredByRedirect = false, isDirectNavigation = true),
|
||||||
)
|
)
|
||||||
|
|
||||||
assertNull(observer.intent)
|
assertNull(observer.observedIntent)
|
||||||
assertNull(observer.url)
|
assertNull(observer.observedUrl)
|
||||||
|
assertNull(observer.observedFallbackUrl)
|
||||||
|
assertNull(observer.observedAppName)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -3829,6 +3859,8 @@ class GeckoEngineSessionTest {
|
|||||||
|
|
||||||
var observedUrl: String? = null
|
var observedUrl: String? = null
|
||||||
var observedIntent: Intent? = null
|
var observedIntent: Intent? = null
|
||||||
|
var observedFallbackUrl: String? = null
|
||||||
|
var observedAppName: String? = null
|
||||||
var observedIsSubframe = false
|
var observedIsSubframe = false
|
||||||
|
|
||||||
engineSession.settings.requestInterceptor = object : RequestInterceptor {
|
engineSession.settings.requestInterceptor = object : RequestInterceptor {
|
||||||
@@ -3846,7 +3878,7 @@ class GeckoEngineSessionTest {
|
|||||||
): RequestInterceptor.InterceptionResponse? {
|
): RequestInterceptor.InterceptionResponse? {
|
||||||
observedIsSubframe = isSubframeRequest
|
observedIsSubframe = isSubframeRequest
|
||||||
return when (uri) {
|
return when (uri) {
|
||||||
"sample:about" -> RequestInterceptor.InterceptionResponse.AppIntent(mock(), "result")
|
"sample:about" -> RequestInterceptor.InterceptionResponse.AppIntent(mock(), "result", "fallback", "app")
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3857,9 +3889,13 @@ class GeckoEngineSessionTest {
|
|||||||
override fun onLaunchIntentRequest(
|
override fun onLaunchIntentRequest(
|
||||||
url: String,
|
url: String,
|
||||||
appIntent: Intent?,
|
appIntent: Intent?,
|
||||||
|
fallbackUrl: String?,
|
||||||
|
appName: String?,
|
||||||
) {
|
) {
|
||||||
observedUrl = url
|
observedUrl = url
|
||||||
observedIntent = appIntent
|
observedIntent = appIntent
|
||||||
|
observedFallbackUrl = fallbackUrl
|
||||||
|
observedAppName = appName
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -3871,8 +3907,14 @@ class GeckoEngineSessionTest {
|
|||||||
|
|
||||||
assertNotNull(observedIntent)
|
assertNotNull(observedIntent)
|
||||||
assertEquals("result", observedUrl)
|
assertEquals("result", observedUrl)
|
||||||
|
assertEquals("fallback", observedFallbackUrl)
|
||||||
|
assertEquals("app", observedAppName)
|
||||||
assertEquals(true, observedIsSubframe)
|
assertEquals(true, observedIsSubframe)
|
||||||
|
|
||||||
|
observedUrl = null
|
||||||
|
observedIntent = null
|
||||||
|
observedFallbackUrl = null
|
||||||
|
observedAppName = null
|
||||||
navigationDelegate.value.onSubframeLoadRequest(
|
navigationDelegate.value.onSubframeLoadRequest(
|
||||||
mock(),
|
mock(),
|
||||||
mockLoadRequest("sample:about", triggeredByRedirect = false),
|
mockLoadRequest("sample:about", triggeredByRedirect = false),
|
||||||
@@ -3880,6 +3922,8 @@ class GeckoEngineSessionTest {
|
|||||||
|
|
||||||
assertNotNull(observedIntent)
|
assertNotNull(observedIntent)
|
||||||
assertEquals("result", observedUrl)
|
assertEquals("result", observedUrl)
|
||||||
|
assertEquals("fallback", observedFallbackUrl)
|
||||||
|
assertEquals("app", observedAppName)
|
||||||
assertEquals(true, observedIsSubframe)
|
assertEquals(true, observedIsSubframe)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3892,8 +3936,10 @@ class GeckoEngineSessionTest {
|
|||||||
|
|
||||||
captureDelegates()
|
captureDelegates()
|
||||||
|
|
||||||
var observedLaunchIntentUrl: String? = null
|
var observedUrl: String? = null
|
||||||
var observedLaunchIntent: Intent? = null
|
var observedIntent: Intent? = null
|
||||||
|
var observedFallbackUrl: String? = null
|
||||||
|
var observedAppName: String? = null
|
||||||
var observedOnLoadRequestUrl: String? = null
|
var observedOnLoadRequestUrl: String? = null
|
||||||
var observedTriggeredByRedirect: Boolean? = null
|
var observedTriggeredByRedirect: Boolean? = null
|
||||||
var observedTriggeredByWebContent: Boolean? = null
|
var observedTriggeredByWebContent: Boolean? = null
|
||||||
@@ -3923,9 +3969,13 @@ class GeckoEngineSessionTest {
|
|||||||
override fun onLaunchIntentRequest(
|
override fun onLaunchIntentRequest(
|
||||||
url: String,
|
url: String,
|
||||||
appIntent: Intent?,
|
appIntent: Intent?,
|
||||||
|
fallbackUrl: String?,
|
||||||
|
appName: String?,
|
||||||
) {
|
) {
|
||||||
observedLaunchIntentUrl = url
|
observedUrl = url
|
||||||
observedLaunchIntent = appIntent
|
observedIntent = appIntent
|
||||||
|
observedFallbackUrl = fallbackUrl
|
||||||
|
observedAppName = appName
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoadRequest(
|
override fun onLoadRequest(
|
||||||
@@ -3945,8 +3995,10 @@ class GeckoEngineSessionTest {
|
|||||||
mockLoadRequest("sample:about", triggeredByRedirect = true),
|
mockLoadRequest("sample:about", triggeredByRedirect = true),
|
||||||
)
|
)
|
||||||
|
|
||||||
assertNull(observedLaunchIntentUrl)
|
assertNull(observedUrl)
|
||||||
assertNull(observedLaunchIntent)
|
assertNull(observedIntent)
|
||||||
|
assertNull(observedFallbackUrl)
|
||||||
|
assertNull(observedAppName)
|
||||||
assertNull(observedTriggeredByRedirect)
|
assertNull(observedTriggeredByRedirect)
|
||||||
assertNull(observedTriggeredByWebContent)
|
assertNull(observedTriggeredByWebContent)
|
||||||
assertNull(observedOnLoadRequestUrl)
|
assertNull(observedOnLoadRequestUrl)
|
||||||
@@ -3956,8 +4008,10 @@ class GeckoEngineSessionTest {
|
|||||||
mockLoadRequest("sample:about", triggeredByRedirect = false),
|
mockLoadRequest("sample:about", triggeredByRedirect = false),
|
||||||
)
|
)
|
||||||
|
|
||||||
assertNull(observedLaunchIntentUrl)
|
assertNull(observedUrl)
|
||||||
assertNull(observedLaunchIntent)
|
assertNull(observedIntent)
|
||||||
|
assertNull(observedFallbackUrl)
|
||||||
|
assertNull(observedAppName)
|
||||||
assertNull(observedTriggeredByRedirect)
|
assertNull(observedTriggeredByRedirect)
|
||||||
assertNull(observedTriggeredByWebContent)
|
assertNull(observedTriggeredByWebContent)
|
||||||
assertNull(observedOnLoadRequestUrl)
|
assertNull(observedOnLoadRequestUrl)
|
||||||
@@ -3972,8 +4026,10 @@ class GeckoEngineSessionTest {
|
|||||||
|
|
||||||
captureDelegates()
|
captureDelegates()
|
||||||
|
|
||||||
var observedLaunchIntentUrl: String? = null
|
var observedUrl: String? = null
|
||||||
var observedLaunchIntent: Intent? = null
|
var observedIntent: Intent? = null
|
||||||
|
var observedFallbackUrl: String? = null
|
||||||
|
var observedAppName: String? = null
|
||||||
var observedOnLoadRequestUrl: String? = null
|
var observedOnLoadRequestUrl: String? = null
|
||||||
var observedTriggeredByRedirect: Boolean? = null
|
var observedTriggeredByRedirect: Boolean? = null
|
||||||
var observedTriggeredByWebContent: Boolean? = null
|
var observedTriggeredByWebContent: Boolean? = null
|
||||||
@@ -3984,9 +4040,13 @@ class GeckoEngineSessionTest {
|
|||||||
override fun onLaunchIntentRequest(
|
override fun onLaunchIntentRequest(
|
||||||
url: String,
|
url: String,
|
||||||
appIntent: Intent?,
|
appIntent: Intent?,
|
||||||
|
fallbackUrl: String?,
|
||||||
|
appName: String?,
|
||||||
) {
|
) {
|
||||||
observedLaunchIntentUrl = url
|
observedUrl = url
|
||||||
observedLaunchIntent = appIntent
|
observedIntent = appIntent
|
||||||
|
observedFallbackUrl = fallbackUrl
|
||||||
|
observedAppName = appName
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoadRequest(
|
override fun onLoadRequest(
|
||||||
@@ -4006,8 +4066,10 @@ class GeckoEngineSessionTest {
|
|||||||
mockLoadRequest("sample:about", triggeredByRedirect = true),
|
mockLoadRequest("sample:about", triggeredByRedirect = true),
|
||||||
)
|
)
|
||||||
|
|
||||||
assertNull(observedLaunchIntentUrl)
|
assertNull(observedUrl)
|
||||||
assertNull(observedLaunchIntent)
|
assertNull(observedIntent)
|
||||||
|
assertNull(observedFallbackUrl)
|
||||||
|
assertNull(observedAppName)
|
||||||
assertNotNull(observedTriggeredByRedirect)
|
assertNotNull(observedTriggeredByRedirect)
|
||||||
assertTrue(observedTriggeredByRedirect!!)
|
assertTrue(observedTriggeredByRedirect!!)
|
||||||
assertNotNull(observedTriggeredByWebContent)
|
assertNotNull(observedTriggeredByWebContent)
|
||||||
@@ -4019,8 +4081,10 @@ class GeckoEngineSessionTest {
|
|||||||
mockLoadRequest("sample:about", triggeredByRedirect = false),
|
mockLoadRequest("sample:about", triggeredByRedirect = false),
|
||||||
)
|
)
|
||||||
|
|
||||||
assertNull(observedLaunchIntentUrl)
|
assertNull(observedUrl)
|
||||||
assertNull(observedLaunchIntent)
|
assertNull(observedIntent)
|
||||||
|
assertNull(observedFallbackUrl)
|
||||||
|
assertNull(observedAppName)
|
||||||
assertNotNull(observedTriggeredByRedirect)
|
assertNotNull(observedTriggeredByRedirect)
|
||||||
assertFalse(observedTriggeredByRedirect!!)
|
assertFalse(observedTriggeredByRedirect!!)
|
||||||
assertNotNull(observedTriggeredByWebContent)
|
assertNotNull(observedTriggeredByWebContent)
|
||||||
@@ -4054,7 +4118,7 @@ class GeckoEngineSessionTest {
|
|||||||
): RequestInterceptor.InterceptionResponse? {
|
): RequestInterceptor.InterceptionResponse? {
|
||||||
return when (uri) {
|
return when (uri) {
|
||||||
fakeUrl -> null
|
fakeUrl -> null
|
||||||
else -> RequestInterceptor.InterceptionResponse.AppIntent(mock(), fakeUrl)
|
else -> RequestInterceptor.InterceptionResponse.AppIntent(mock(), fakeUrl, null, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -266,7 +266,12 @@ class SystemEngineView @JvmOverloads constructor(
|
|||||||
is InterceptionResponse.AppIntent -> {
|
is InterceptionResponse.AppIntent -> {
|
||||||
if (request.isForMainFrame) {
|
if (request.isForMainFrame) {
|
||||||
session.notifyObservers {
|
session.notifyObservers {
|
||||||
onLaunchIntentRequest(url = url, appIntent = appIntent)
|
onLaunchIntentRequest(
|
||||||
|
url = url,
|
||||||
|
appIntent = appIntent,
|
||||||
|
fallbackUrl = fallbackUrl,
|
||||||
|
appName = appName,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,8 +104,18 @@ internal class EngineObserver(
|
|||||||
store.dispatch(ContentAction.UpdateLoadRequestAction(tabId, loadRequest))
|
store.dispatch(ContentAction.UpdateLoadRequestAction(tabId, loadRequest))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLaunchIntentRequest(url: String, appIntent: Intent?) {
|
override fun onLaunchIntentRequest(
|
||||||
store.dispatch(ContentAction.UpdateAppIntentAction(tabId, AppIntentState(url, appIntent)))
|
url: String,
|
||||||
|
appIntent: Intent?,
|
||||||
|
fallbackUrl: String?,
|
||||||
|
appName: String?,
|
||||||
|
) {
|
||||||
|
store.dispatch(
|
||||||
|
ContentAction.UpdateAppIntentAction(
|
||||||
|
tabId,
|
||||||
|
AppIntentState(url, appIntent, fallbackUrl, appName),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTitleChange(title: String) {
|
override fun onTitleChange(title: String) {
|
||||||
|
|||||||
@@ -11,8 +11,12 @@ import android.content.Intent
|
|||||||
*
|
*
|
||||||
* @param url the URL to launch in an external app.
|
* @param url the URL to launch in an external app.
|
||||||
* @param appIntent the [Intent] to launch.
|
* @param appIntent the [Intent] to launch.
|
||||||
|
* @param fallbackUrl the fallback URL if launch failed or denied by user.
|
||||||
|
* @param appName the target application name.
|
||||||
*/
|
*/
|
||||||
data class AppIntentState(
|
data class AppIntentState(
|
||||||
val url: String,
|
val url: String,
|
||||||
val appIntent: Intent?,
|
val appIntent: Intent?,
|
||||||
|
val fallbackUrl: String?,
|
||||||
|
val appName: String?,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1517,9 +1517,9 @@ class EngineObserverTest {
|
|||||||
val store: BrowserStore = mock()
|
val store: BrowserStore = mock()
|
||||||
val observer = EngineObserver("test-id", store)
|
val observer = EngineObserver("test-id", store)
|
||||||
val intent: Intent = mock()
|
val intent: Intent = mock()
|
||||||
observer.onLaunchIntentRequest(url = url, appIntent = intent)
|
observer.onLaunchIntentRequest(url = url, appIntent = intent, fallbackUrl = null, appName = null)
|
||||||
|
|
||||||
verify(store).dispatch(ContentAction.UpdateAppIntentAction("test-id", AppIntentState(url, intent)))
|
verify(store).dispatch(ContentAction.UpdateAppIntentAction("test-id", AppIntentState(url, intent, null, null)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -256,10 +256,14 @@ abstract class EngineSession(
|
|||||||
* @param url The string url that was requested.
|
* @param url The string url that was requested.
|
||||||
* @param appIntent The Android Intent that was requested.
|
* @param appIntent The Android Intent that was requested.
|
||||||
* web content (as opposed to via the browser chrome).
|
* web content (as opposed to via the browser chrome).
|
||||||
|
* @param fallbackUrl the fallback URL if launch failed or denied by user.
|
||||||
|
* @param appName the target application name.
|
||||||
*/
|
*/
|
||||||
fun onLaunchIntentRequest(
|
fun onLaunchIntentRequest(
|
||||||
url: String,
|
url: String,
|
||||||
appIntent: Intent?,
|
appIntent: Intent?,
|
||||||
|
fallbackUrl: String?,
|
||||||
|
appName: String?,
|
||||||
) = Unit
|
) = Unit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -40,7 +40,12 @@ interface RequestInterceptor {
|
|||||||
val additionalHeaders: Map<String, String>? = null,
|
val additionalHeaders: Map<String, String>? = null,
|
||||||
) : InterceptionResponse()
|
) : InterceptionResponse()
|
||||||
|
|
||||||
data class AppIntent(val appIntent: Intent, val url: String) : InterceptionResponse()
|
data class AppIntent(
|
||||||
|
val appIntent: Intent,
|
||||||
|
val url: String,
|
||||||
|
val fallbackUrl: String?,
|
||||||
|
val appName: String?,
|
||||||
|
) : InterceptionResponse()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deny request without further action.
|
* Deny request without further action.
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class EngineSessionTest {
|
|||||||
session.notifyInternalObservers { onMediaFullscreenChanged(true, mediaSessionElementMetadata) }
|
session.notifyInternalObservers { onMediaFullscreenChanged(true, mediaSessionElementMetadata) }
|
||||||
session.notifyInternalObservers { onCrash() }
|
session.notifyInternalObservers { onCrash() }
|
||||||
session.notifyInternalObservers { onLoadRequest("https://www.mozilla.org", true, true) }
|
session.notifyInternalObservers { onLoadRequest("https://www.mozilla.org", true, true) }
|
||||||
session.notifyInternalObservers { onLaunchIntentRequest("https://www.mozilla.org", null) }
|
session.notifyInternalObservers { onLaunchIntentRequest("https://www.mozilla.org", null, null, null) }
|
||||||
session.notifyInternalObservers { onProcessKilled() }
|
session.notifyInternalObservers { onProcessKilled() }
|
||||||
session.notifyInternalObservers { onShowDynamicToolbar() }
|
session.notifyInternalObservers { onShowDynamicToolbar() }
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ class EngineSessionTest {
|
|||||||
verify(observer).onMediaFullscreenChanged(true, mediaSessionElementMetadata)
|
verify(observer).onMediaFullscreenChanged(true, mediaSessionElementMetadata)
|
||||||
verify(observer).onCrash()
|
verify(observer).onCrash()
|
||||||
verify(observer).onLoadRequest("https://www.mozilla.org", true, true)
|
verify(observer).onLoadRequest("https://www.mozilla.org", true, true)
|
||||||
verify(observer).onLaunchIntentRequest("https://www.mozilla.org", null)
|
verify(observer).onLaunchIntentRequest("https://www.mozilla.org", null, null, null)
|
||||||
verify(observer).onProcessKilled()
|
verify(observer).onProcessKilled()
|
||||||
verify(observer).onShowDynamicToolbar()
|
verify(observer).onShowDynamicToolbar()
|
||||||
verifyNoMoreInteractions(observer)
|
verifyNoMoreInteractions(observer)
|
||||||
@@ -152,7 +152,7 @@ class EngineSessionTest {
|
|||||||
session.notifyInternalObservers { onWindowRequest(windowRequest) }
|
session.notifyInternalObservers { onWindowRequest(windowRequest) }
|
||||||
session.notifyInternalObservers { onCrash() }
|
session.notifyInternalObservers { onCrash() }
|
||||||
session.notifyInternalObservers { onLoadRequest("https://www.mozilla.org", true, true) }
|
session.notifyInternalObservers { onLoadRequest("https://www.mozilla.org", true, true) }
|
||||||
session.notifyInternalObservers { onLaunchIntentRequest("https://www.mozilla.org", null) }
|
session.notifyInternalObservers { onLaunchIntentRequest("https://www.mozilla.org", null, null, null) }
|
||||||
session.notifyInternalObservers { onShowDynamicToolbar() }
|
session.notifyInternalObservers { onShowDynamicToolbar() }
|
||||||
session.unregister(observer)
|
session.unregister(observer)
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ class EngineSessionTest {
|
|||||||
session.notifyInternalObservers { onMediaFullscreenChanged(true, mediaSessionElementMetadata) }
|
session.notifyInternalObservers { onMediaFullscreenChanged(true, mediaSessionElementMetadata) }
|
||||||
session.notifyInternalObservers { onCrash() }
|
session.notifyInternalObservers { onCrash() }
|
||||||
session.notifyInternalObservers { onLoadRequest("https://www.mozilla.org", true, true) }
|
session.notifyInternalObservers { onLoadRequest("https://www.mozilla.org", true, true) }
|
||||||
session.notifyInternalObservers { onLaunchIntentRequest("https://www.firefox.com", null) }
|
session.notifyInternalObservers { onLaunchIntentRequest("https://www.firefox.com", null, null, null) }
|
||||||
session.notifyInternalObservers { onShowDynamicToolbar() }
|
session.notifyInternalObservers { onShowDynamicToolbar() }
|
||||||
|
|
||||||
verify(observer).onScrollChange(1234, 4321)
|
verify(observer).onScrollChange(1234, 4321)
|
||||||
@@ -211,7 +211,7 @@ class EngineSessionTest {
|
|||||||
verify(observer).onWindowRequest(windowRequest)
|
verify(observer).onWindowRequest(windowRequest)
|
||||||
verify(observer).onCrash()
|
verify(observer).onCrash()
|
||||||
verify(observer).onLoadRequest("https://www.mozilla.org", true, true)
|
verify(observer).onLoadRequest("https://www.mozilla.org", true, true)
|
||||||
verify(observer).onLaunchIntentRequest("https://www.mozilla.org", null)
|
verify(observer).onLaunchIntentRequest("https://www.mozilla.org", null, null, null)
|
||||||
verify(observer).onShowDynamicToolbar()
|
verify(observer).onShowDynamicToolbar()
|
||||||
verify(observer, never()).onScrollChange(2345, 5432)
|
verify(observer, never()).onScrollChange(2345, 5432)
|
||||||
verify(observer, never()).onLocationChange("https://www.firefox.com", false)
|
verify(observer, never()).onLocationChange("https://www.firefox.com", false)
|
||||||
@@ -239,7 +239,7 @@ class EngineSessionTest {
|
|||||||
verify(observer, never()).onMediaMuteChanged(true)
|
verify(observer, never()).onMediaMuteChanged(true)
|
||||||
verify(observer, never()).onMediaFullscreenChanged(true, mediaSessionElementMetadata)
|
verify(observer, never()).onMediaFullscreenChanged(true, mediaSessionElementMetadata)
|
||||||
verify(observer, never()).onLoadRequest("https://www.mozilla.org", false, true)
|
verify(observer, never()).onLoadRequest("https://www.mozilla.org", false, true)
|
||||||
verify(observer, never()).onLaunchIntentRequest("https://www.firefox.com", null)
|
verify(observer, never()).onLaunchIntentRequest("https://www.firefox.com", null, null, null)
|
||||||
verifyNoMoreInteractions(observer)
|
verifyNoMoreInteractions(observer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -964,7 +964,7 @@ class EngineSessionTest {
|
|||||||
observer.onWindowRequest(windowRequest)
|
observer.onWindowRequest(windowRequest)
|
||||||
observer.onCrash()
|
observer.onCrash()
|
||||||
observer.onLoadRequest("https://www.mozilla.org", true, true)
|
observer.onLoadRequest("https://www.mozilla.org", true, true)
|
||||||
observer.onLaunchIntentRequest("https://www.mozilla.org", null)
|
observer.onLaunchIntentRequest("https://www.mozilla.org", null, null, null)
|
||||||
observer.onProcessKilled()
|
observer.onProcessKilled()
|
||||||
observer.onShowDynamicToolbar()
|
observer.onShowDynamicToolbar()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ import mozilla.components.lib.state.ext.flowScoped
|
|||||||
import mozilla.components.support.base.feature.LifecycleAwareFeature
|
import mozilla.components.support.base.feature.LifecycleAwareFeature
|
||||||
import mozilla.components.support.ktx.android.content.appName
|
import mozilla.components.support.ktx.android.content.appName
|
||||||
|
|
||||||
|
// Minimum time for dialog to settle before accepting user interactions.
|
||||||
|
internal const val MAX_SUCCESSIVE_DIALOG_MILLIS_LIMIT: Int = 500 // 0.5 seconds
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This feature implements observer for handling redirects to external apps. The users are asked to
|
* This feature implements observer for handling redirects to external apps. The users are asked to
|
||||||
* confirm their intention before leaving the app if in private session. These include the Android
|
* confirm their intention before leaving the app if in private session. These include the Android
|
||||||
@@ -45,6 +48,9 @@ import mozilla.components.support.ktx.android.content.appName
|
|||||||
* have registered to open.
|
* have registered to open.
|
||||||
* @param failedToLaunchAction Action to perform when failing to launch in third party app.
|
* @param failedToLaunchAction Action to perform when failing to launch in third party app.
|
||||||
* @param loadUrlUseCase Used to load URL if user decides not to launch in third party app.
|
* @param loadUrlUseCase Used to load URL if user decides not to launch in third party app.
|
||||||
|
* @param engineSupportedSchemes Set of URI schemes the engine supports.
|
||||||
|
* @param shouldPrompt If {true} then user should be prompted before launching app links.
|
||||||
|
* @param alwaysOpenCheckboxAction Action to perform when user checked the always open checkbox in the prompt.
|
||||||
**/
|
**/
|
||||||
class AppLinksFeature(
|
class AppLinksFeature(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@@ -58,6 +64,7 @@ class AppLinksFeature(
|
|||||||
private val loadUrlUseCase: SessionUseCases.DefaultLoadUrlUseCase? = null,
|
private val loadUrlUseCase: SessionUseCases.DefaultLoadUrlUseCase? = null,
|
||||||
private val engineSupportedSchemes: Set<String> = ENGINE_SUPPORTED_SCHEMES,
|
private val engineSupportedSchemes: Set<String> = ENGINE_SUPPORTED_SCHEMES,
|
||||||
private val shouldPrompt: () -> Boolean = { true },
|
private val shouldPrompt: () -> Boolean = { true },
|
||||||
|
private val alwaysOpenCheckboxAction: (() -> Unit)? = null,
|
||||||
) : LifecycleAwareFeature {
|
) : LifecycleAwareFeature {
|
||||||
|
|
||||||
private var scope: CoroutineScope? = null
|
private var scope: CoroutineScope? = null
|
||||||
@@ -71,10 +78,16 @@ class AppLinksFeature(
|
|||||||
.distinctUntilChangedBy {
|
.distinctUntilChangedBy {
|
||||||
it.content.appIntent
|
it.content.appIntent
|
||||||
}
|
}
|
||||||
.collect { tab ->
|
.collect { sessionState ->
|
||||||
tab.content.appIntent?.let {
|
sessionState.content.appIntent?.let {
|
||||||
handleAppIntent(tab, it.url, it.appIntent)
|
handleAppIntent(
|
||||||
store.dispatch(ContentAction.ConsumeAppIntentAction(tab.id))
|
sessionState = sessionState,
|
||||||
|
url = it.url,
|
||||||
|
appIntent = it.appIntent,
|
||||||
|
fallbackUrl = it.fallbackUrl,
|
||||||
|
appName = it.appName,
|
||||||
|
)
|
||||||
|
store.dispatch(ContentAction.ConsumeAppIntentAction(sessionState.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,105 +102,160 @@ class AppLinksFeature(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
internal fun handleAppIntent(tab: SessionState, url: String, appIntent: Intent?) {
|
internal fun handleAppIntent(
|
||||||
if (appIntent == null) {
|
sessionState: SessionState,
|
||||||
|
url: String,
|
||||||
|
appIntent: Intent?,
|
||||||
|
fallbackUrl: String?,
|
||||||
|
appName: String?,
|
||||||
|
) {
|
||||||
|
if (appIntent == null) return
|
||||||
|
|
||||||
|
val isPrivate = sessionState.content.private
|
||||||
|
val isAuthenticationFlow =
|
||||||
|
AppLinksInterceptor.isAuthentication(sessionState, appIntent.component?.packageName)
|
||||||
|
|
||||||
|
if (shouldBypassPrompt(isPrivate, isAuthenticationFlow, fragmentManager)) {
|
||||||
|
openApp(appIntent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val doNotOpenApp = {
|
if (isADialogAlreadyCreated() || fragmentManager?.isStateSaved == true) {
|
||||||
AppLinksInterceptor.addUserDoNotIntercept(url, appIntent, tab.id)
|
|
||||||
|
|
||||||
loadUrlIfSchemeSupported(tab, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
val doOpenApp = {
|
|
||||||
useCases.openAppLink(
|
|
||||||
appIntent,
|
|
||||||
failedToLaunchAction = failedToLaunchAction,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("ComplexCondition")
|
|
||||||
if (isAuthentication(tab, appIntent) || (!tab.content.private && !shouldPrompt()) ||
|
|
||||||
fragmentManager == null
|
|
||||||
) {
|
|
||||||
doOpenApp()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val dialog = getOrCreateDialog(tab.content.private, url)
|
showRedirectDialog(
|
||||||
dialog.onConfirmRedirect = { doOpenApp() }
|
sessionState = sessionState,
|
||||||
dialog.onCancelRedirect = doNotOpenApp
|
url = url,
|
||||||
|
fallbackUrl = fallbackUrl,
|
||||||
if (!isAlreadyADialogCreated()) {
|
appIntent = appIntent,
|
||||||
dialog.showNow(fragmentManager, FRAGMENT_TAG)
|
appName = appName,
|
||||||
}
|
isPrivate = isPrivate,
|
||||||
|
fragmentManager = fragmentManager,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
internal fun getOrCreateDialog(isPrivate: Boolean, url: String): RedirectDialogFragment {
|
internal fun shouldBypassPrompt(
|
||||||
|
isPrivate: Boolean,
|
||||||
|
isAuthenticationFlow: Boolean,
|
||||||
|
fragmentManager: FragmentManager?,
|
||||||
|
): Boolean {
|
||||||
|
val shouldShowPrompt = isPrivate || shouldPrompt()
|
||||||
|
return fragmentManager == null || !shouldShowPrompt || isAuthenticationFlow
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal fun openApp(appIntent: Intent) {
|
||||||
|
useCases.openAppLink(
|
||||||
|
appIntent,
|
||||||
|
failedToLaunchAction = failedToLaunchAction,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal fun cancelRedirect(
|
||||||
|
sessionState: SessionState,
|
||||||
|
url: String,
|
||||||
|
fallbackUrl: String?,
|
||||||
|
appIntent: Intent,
|
||||||
|
) {
|
||||||
|
AppLinksInterceptor.addUserDoNotIntercept(url, appIntent, sessionState.id)
|
||||||
|
|
||||||
|
val urlToLoad = when {
|
||||||
|
isSchemeSupported(url) -> url
|
||||||
|
fallbackUrl != null && isSchemeSupported(fallbackUrl) -> fallbackUrl
|
||||||
|
else -> return // No supported URL to load.
|
||||||
|
}
|
||||||
|
|
||||||
|
loadUrlUseCase?.invoke(
|
||||||
|
url = urlToLoad,
|
||||||
|
sessionId = sessionState.id,
|
||||||
|
flags = EngineSession.LoadUrlFlags.select(EXTERNAL, LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showRedirectDialog(
|
||||||
|
sessionState: SessionState,
|
||||||
|
url: String,
|
||||||
|
fallbackUrl: String?,
|
||||||
|
appIntent: Intent,
|
||||||
|
appName: String?,
|
||||||
|
isPrivate: Boolean,
|
||||||
|
fragmentManager: FragmentManager?,
|
||||||
|
) {
|
||||||
|
if (fragmentManager == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrCreateDialog(isPrivate, url, appName).apply {
|
||||||
|
onConfirmRedirect = { isCheckboxTicked ->
|
||||||
|
if (isCheckboxTicked) {
|
||||||
|
alwaysOpenCheckboxAction?.invoke()
|
||||||
|
}
|
||||||
|
openApp(appIntent)
|
||||||
|
}
|
||||||
|
onCancelRedirect = {
|
||||||
|
cancelRedirect(sessionState, url, fallbackUrl, appIntent)
|
||||||
|
}
|
||||||
|
}.showNow(fragmentManager, FRAGMENT_TAG)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal fun getOrCreateDialog(
|
||||||
|
isPrivate: Boolean,
|
||||||
|
url: String,
|
||||||
|
targetAppName: String?,
|
||||||
|
): RedirectDialogFragment {
|
||||||
if (dialog != null) {
|
if (dialog != null) {
|
||||||
return dialog
|
return dialog
|
||||||
}
|
}
|
||||||
|
|
||||||
val message = context.getString(
|
val dialogTitle = when {
|
||||||
R.string.mozac_feature_applinks_normal_confirm_dialog_message,
|
isPrivate && !targetAppName.isNullOrBlank() -> {
|
||||||
context.appName,
|
context.getString(R.string.mozac_feature_applinks_confirm_dialog_title_with_app_name, targetAppName)
|
||||||
)
|
}
|
||||||
|
isPrivate -> {
|
||||||
|
context.getString(R.string.mozac_feature_applinks_confirm_dialog_title)
|
||||||
|
}
|
||||||
|
!targetAppName.isNullOrBlank() -> {
|
||||||
|
context.getString(
|
||||||
|
R.string.mozac_feature_applinks_normal_confirm_dialog_title_with_app_name,
|
||||||
|
targetAppName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
context.getString(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(
|
return SimpleRedirectDialogFragment.newInstance(
|
||||||
dialogTitleString = if (isPrivate) {
|
dialogTitleString = dialogTitle,
|
||||||
context.getString(R.string.mozac_feature_applinks_confirm_dialog_title)
|
dialogMessageString = dialogMessage,
|
||||||
} else {
|
showCheckbox = if (isPrivate) false else alwaysOpenCheckboxAction != null,
|
||||||
context.getString(R.string.mozac_feature_applinks_normal_confirm_dialog_title)
|
maxSuccessiveDialogMillisLimit = MAX_SUCCESSIVE_DIALOG_MILLIS_LIMIT,
|
||||||
},
|
|
||||||
dialogMessageString = if (isPrivate) {
|
|
||||||
url
|
|
||||||
} else {
|
|
||||||
message
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
internal fun loadUrlIfSchemeSupported(tab: SessionState, url: String) {
|
internal fun isSchemeSupported(url: String): Boolean {
|
||||||
val schemeSupported = engineSupportedSchemes.contains(url.toUri().scheme)
|
return engineSupportedSchemes.contains(url.toUri().scheme)
|
||||||
if (schemeSupported) {
|
|
||||||
loadUrlUseCase?.invoke(
|
|
||||||
url = url,
|
|
||||||
sessionId = tab.id,
|
|
||||||
flags = EngineSession.LoadUrlFlags.select(EXTERNAL, LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isAlreadyADialogCreated(): Boolean {
|
private fun isADialogAlreadyCreated(): Boolean {
|
||||||
return findPreviousDialogFragment() != null
|
return findPreviousDialogFragment() != null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findPreviousDialogFragment(): RedirectDialogFragment? {
|
private fun findPreviousDialogFragment(): RedirectDialogFragment? {
|
||||||
return fragmentManager?.findFragmentByTag(FRAGMENT_TAG) as? RedirectDialogFragment
|
return fragmentManager?.findFragmentByTag(FRAGMENT_TAG) as? RedirectDialogFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
|
||||||
internal fun isAuthentication(tab: SessionState, appIntent: Intent): Boolean {
|
|
||||||
return when (tab.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,
|
|
||||||
-> {
|
|
||||||
(tab.source as? SessionState.Source.External)?.let { externalSource ->
|
|
||||||
when (externalSource.caller?.packageId) {
|
|
||||||
null -> false
|
|
||||||
appIntent.component?.packageName -> true
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
} ?: false
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,19 +10,13 @@ import android.content.Intent
|
|||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import mozilla.components.browser.state.selector.findTabOrCustomTab
|
import mozilla.components.browser.state.selector.findTabOrCustomTab
|
||||||
import mozilla.components.browser.state.state.SessionState
|
import mozilla.components.browser.state.state.SessionState
|
||||||
import mozilla.components.browser.state.store.BrowserStore
|
import mozilla.components.browser.state.store.BrowserStore
|
||||||
import mozilla.components.concept.engine.EngineSession
|
import mozilla.components.concept.engine.EngineSession
|
||||||
import mozilla.components.concept.engine.EngineSession.LoadUrlFlags.Companion.EXTERNAL
|
|
||||||
import mozilla.components.concept.engine.EngineSession.LoadUrlFlags.Companion.LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE
|
|
||||||
import mozilla.components.concept.engine.request.RequestInterceptor
|
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.ALWAYS_DENY_SCHEMES
|
||||||
import mozilla.components.feature.app.links.AppLinksUseCases.Companion.ENGINE_SUPPORTED_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.feature.session.SessionUseCases
|
|
||||||
import mozilla.components.support.ktx.android.content.appName
|
|
||||||
import mozilla.components.support.ktx.android.net.isHttpOrHttps
|
import mozilla.components.support.ktx.android.net.isHttpOrHttps
|
||||||
import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
|
import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
|
||||||
|
|
||||||
@@ -53,26 +47,18 @@ private val ALLOWED_SCHEMES_IN_SUBFRAME: List<String> = listOf(
|
|||||||
* and https://github.com/mozilla-mobile/android-components/issues/2975.
|
* and https://github.com/mozilla-mobile/android-components/issues/2975.
|
||||||
*
|
*
|
||||||
* @param context Context the feature is associated with.
|
* @param context Context the feature is associated with.
|
||||||
* @param interceptLinkClicks If {true} then intercept link clicks.
|
|
||||||
* @param engineSupportedSchemes List of schemes that the engine supports.
|
* @param engineSupportedSchemes List of schemes that the engine supports.
|
||||||
* @param alwaysDeniedSchemes List of schemes that will never be opened in a third-party app even if
|
* @param alwaysDeniedSchemes List of schemes that will never be opened in a third-party app
|
||||||
* [interceptLinkClicks] is `true`.
|
|
||||||
* @param launchInApp If {true} then launch app links in third party app(s). Default to false because
|
* @param launchInApp If {true} then launch app links in third party app(s). Default to false because
|
||||||
* of security concerns.
|
* of security concerns.
|
||||||
* @param useCases These use cases allow for the detection of, and opening of links that other apps
|
* @param useCases These use cases allow for the detection of, and opening of links that other apps
|
||||||
* have registered to open.
|
* have registered to open.
|
||||||
* @param launchFromInterceptor If {true} then the interceptor will prompt and launch the link in
|
* @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]
|
* third-party apps if available. Do not use this in conjunction with [AppLinksFeature]
|
||||||
* @param showCheckbox if {true} then the checkbox will be visible on normal tabs within [SimpleRedirectDialogFragment]
|
|
||||||
* @param store [BrowserStore] containing the information about the currently open tabs.
|
* @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.
|
|
||||||
* @param checkboxCheckedAction Action to perform when checkbox is ticked and positive button is clicked
|
|
||||||
* on redirect prompt.
|
|
||||||
*/
|
*/
|
||||||
class AppLinksInterceptor(
|
class AppLinksInterceptor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val interceptLinkClicks: Boolean = false,
|
|
||||||
private val engineSupportedSchemes: Set<String> = ENGINE_SUPPORTED_SCHEMES,
|
private val engineSupportedSchemes: Set<String> = ENGINE_SUPPORTED_SCHEMES,
|
||||||
private val alwaysDeniedSchemes: Set<String> = ALWAYS_DENY_SCHEMES,
|
private val alwaysDeniedSchemes: Set<String> = ALWAYS_DENY_SCHEMES,
|
||||||
private var launchInApp: () -> Boolean = { false },
|
private var launchInApp: () -> Boolean = { false },
|
||||||
@@ -82,32 +68,8 @@ class AppLinksInterceptor(
|
|||||||
alwaysDeniedSchemes = alwaysDeniedSchemes,
|
alwaysDeniedSchemes = alwaysDeniedSchemes,
|
||||||
),
|
),
|
||||||
private val launchFromInterceptor: Boolean = false,
|
private val launchFromInterceptor: Boolean = false,
|
||||||
private val showCheckbox: Boolean = false,
|
|
||||||
private val store: BrowserStore? = null,
|
private val store: BrowserStore? = null,
|
||||||
private val shouldPrompt: () -> Boolean = { true },
|
|
||||||
private val failedToLaunchAction: (fallbackUrl: String?) -> Unit = {},
|
|
||||||
private val loadUrlUseCase: SessionUseCases.DefaultLoadUrlUseCase? = null,
|
|
||||||
private val checkboxCheckedAction: () -> Unit = {},
|
|
||||||
) : RequestInterceptor {
|
) : 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
|
|
||||||
* @param launchInApp the new value of launchInApp
|
|
||||||
*/
|
|
||||||
fun updateLaunchInApp(launchInApp: () -> Boolean) {
|
|
||||||
this.launchInApp = launchInApp
|
|
||||||
useCases.updateLaunchInApp(launchInApp)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("ComplexMethod", "ReturnCount")
|
@Suppress("ComplexMethod", "ReturnCount")
|
||||||
override fun onLoadRequest(
|
override fun onLoadRequest(
|
||||||
@@ -137,8 +99,8 @@ class AppLinksInterceptor(
|
|||||||
(!hasUserGesture && !isAllowedRedirect && !isDirectNavigation) ||
|
(!hasUserGesture && !isAllowedRedirect && !isDirectNavigation) ||
|
||||||
isSameDomain(lastUri, uri)
|
isSameDomain(lastUri, uri)
|
||||||
) && engineSupportsScheme -> true
|
) && engineSupportsScheme -> true
|
||||||
// If scheme not in safelist then follow user preference
|
// If scheme not in supported list then follow user preference
|
||||||
(!interceptLinkClicks || !launchInApp()) && engineSupportsScheme -> true
|
!launchInApp() && !isPossibleAuthentication(tabSessionState) && engineSupportsScheme -> true
|
||||||
// Never go to an external app when scheme is in blocklist
|
// Never go to an external app when scheme is in blocklist
|
||||||
alwaysDeniedSchemes.contains(uriScheme) -> true
|
alwaysDeniedSchemes.contains(uriScheme) -> true
|
||||||
else -> false
|
else -> false
|
||||||
@@ -151,10 +113,14 @@ class AppLinksInterceptor(
|
|||||||
val tabId = tabSessionState?.id ?: ""
|
val tabId = tabSessionState?.id ?: ""
|
||||||
val redirect = useCases.interceptedAppLinkRedirect(uri)
|
val redirect = useCases.interceptedAppLinkRedirect(uri)
|
||||||
val result = handleRedirect(redirect, uri, tabId)
|
val result = handleRedirect(redirect, uri, tabId)
|
||||||
|
val packageName = redirect.appIntent?.component?.packageName
|
||||||
|
|
||||||
|
// Now that we have the package name, check again if this is not authentication.
|
||||||
|
if (!launchInApp() && !isAuthentication(tabSessionState, packageName) && engineSupportsScheme) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
if (redirect.hasExternalApp()) {
|
if (redirect.hasExternalApp()) {
|
||||||
val packageName = redirect.appIntent?.component?.packageName
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
lastApplinksPackageWithTimestamp.first == packageName && lastApplinksPackageWithTimestamp.second +
|
lastApplinksPackageWithTimestamp.first == packageName && lastApplinksPackageWithTimestamp.second +
|
||||||
APP_LINKS_DO_NOT_INTERCEPT_INTERVAL > SystemClock.elapsedRealtime()
|
APP_LINKS_DO_NOT_INTERCEPT_INTERVAL > SystemClock.elapsedRealtime()
|
||||||
@@ -166,20 +132,9 @@ class AppLinksInterceptor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (redirect.isRedirect()) {
|
if (redirect.isRedirect()) {
|
||||||
if (
|
if (launchFromInterceptor && result is RequestInterceptor.InterceptionResponse.AppIntent) {
|
||||||
launchFromInterceptor &&
|
result.appIntent.flags = result.appIntent.flags or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
result is RequestInterceptor.InterceptionResponse.AppIntent
|
useCases.openAppLink(result.appIntent)
|
||||||
) {
|
|
||||||
handleIntent(
|
|
||||||
sessionState = tabSessionState,
|
|
||||||
url = uri,
|
|
||||||
appIntent = redirect.appIntent,
|
|
||||||
fallbackUrl = redirect.fallbackUrl,
|
|
||||||
marketingIntent = redirect.marketplaceIntent,
|
|
||||||
appName = redirect.appName,
|
|
||||||
)
|
|
||||||
// We can avoid loading the page only if openInApp settings is set to Always
|
|
||||||
return if (shouldPrompt()) null else result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -211,19 +166,29 @@ class AppLinksInterceptor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!redirect.hasExternalApp()) {
|
if (!redirect.hasExternalApp()) {
|
||||||
redirect.marketplaceIntent?.let {
|
redirect.marketplaceIntent?.let { appIntent ->
|
||||||
return RequestInterceptor.InterceptionResponse.AppIntent(it, uri)
|
return RequestInterceptor.InterceptionResponse.AppIntent(
|
||||||
|
appIntent = appIntent,
|
||||||
|
url = uri,
|
||||||
|
fallbackUrl = redirect.fallbackUrl,
|
||||||
|
appName = redirect.appName,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
redirect.fallbackUrl?.let {
|
redirect.fallbackUrl?.let { fallbackUrl ->
|
||||||
return RequestInterceptor.InterceptionResponse.Url(it)
|
return RequestInterceptor.InterceptionResponse.Url(url = fallbackUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
redirect.appIntent?.let {
|
redirect.appIntent?.let { appIntent ->
|
||||||
return RequestInterceptor.InterceptionResponse.AppIntent(it, uri)
|
return RequestInterceptor.InterceptionResponse.AppIntent(
|
||||||
|
appIntent = appIntent,
|
||||||
|
url = uri,
|
||||||
|
fallbackUrl = redirect.fallbackUrl,
|
||||||
|
appName = redirect.appName,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
@@ -248,153 +213,6 @@ class AppLinksInterceptor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
|
||||||
internal fun handleIntent(
|
|
||||||
sessionState: SessionState?,
|
|
||||||
url: String,
|
|
||||||
appIntent: Intent?,
|
|
||||||
fallbackUrl: String?,
|
|
||||||
marketingIntent: Intent?,
|
|
||||||
appName: String,
|
|
||||||
) {
|
|
||||||
var isAuthenticationFlow = false
|
|
||||||
|
|
||||||
val targetIntent = when {
|
|
||||||
appIntent != null -> {
|
|
||||||
// Without fragment manager we are unable to prompt
|
|
||||||
// Only non private tabs can be redirected to external app without prompt
|
|
||||||
// Authentication flow should not prompt
|
|
||||||
isAuthenticationFlow =
|
|
||||||
sessionState?.let { isAuthentication(sessionState, appIntent) } == true
|
|
||||||
appIntent
|
|
||||||
}
|
|
||||||
marketingIntent != null -> marketingIntent
|
|
||||||
else -> return
|
|
||||||
}
|
|
||||||
|
|
||||||
val fragmentManager = fragmentManager
|
|
||||||
|
|
||||||
val isPrivate = sessionState?.content?.private == true
|
|
||||||
val doNotOpenApp = {
|
|
||||||
addUserDoNotIntercept(url, targetIntent, sessionState?.id)
|
|
||||||
if (sessionState != null && fallbackUrl != null) {
|
|
||||||
loadUrlIfSchemeSupported(sessionState, fallbackUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val doOpenApp = {
|
|
||||||
useCases.openAppLink(
|
|
||||||
targetIntent,
|
|
||||||
failedToLaunchAction = failedToLaunchAction,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val shouldShowPrompt = isPrivate || shouldPrompt()
|
|
||||||
if (fragmentManager == null || !shouldShowPrompt || isAuthenticationFlow) {
|
|
||||||
doOpenApp()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isADialogAlreadyCreated()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fragmentManager.isStateSaved) {
|
|
||||||
getOrCreateDialog(isPrivate, url, appName).apply {
|
|
||||||
onConfirmRedirect = { isCheckboxTicked ->
|
|
||||||
if (isCheckboxTicked) {
|
|
||||||
checkboxCheckedAction()
|
|
||||||
}
|
|
||||||
doOpenApp()
|
|
||||||
}
|
|
||||||
onCancelRedirect = doNotOpenApp
|
|
||||||
}.showNow(fragmentManager, FRAGMENT_TAG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
|
||||||
internal fun getOrCreateDialog(
|
|
||||||
isPrivate: Boolean,
|
|
||||||
url: String,
|
|
||||||
targetAppName: String,
|
|
||||||
): RedirectDialogFragment {
|
|
||||||
if (dialog != null) {
|
|
||||||
return dialog
|
|
||||||
}
|
|
||||||
|
|
||||||
val dialogTitle = when {
|
|
||||||
isPrivate && targetAppName.isNotBlank() -> {
|
|
||||||
context.getString(R.string.mozac_feature_applinks_confirm_dialog_title_with_app_name, targetAppName)
|
|
||||||
}
|
|
||||||
isPrivate -> {
|
|
||||||
context.getString(R.string.mozac_feature_applinks_confirm_dialog_title)
|
|
||||||
}
|
|
||||||
targetAppName.isNotBlank() -> {
|
|
||||||
context.getString(
|
|
||||||
R.string.mozac_feature_applinks_normal_confirm_dialog_title_with_app_name,
|
|
||||||
targetAppName,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
context.getString(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(
|
|
||||||
dialogTitleString = dialogTitle,
|
|
||||||
dialogMessageString = dialogMessage,
|
|
||||||
showCheckbox = if (isPrivate) false else showCheckbox,
|
|
||||||
maxSuccessiveDialogMillisLimit = MAX_SUCCESSIVE_DIALOG_MILLIS_LIMIT,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
|
||||||
internal fun loadUrlIfSchemeSupported(tab: SessionState, url: String) {
|
|
||||||
val schemeSupported = engineSupportedSchemes.contains(url.toUri().scheme)
|
|
||||||
if (schemeSupported) {
|
|
||||||
loadUrlUseCase?.invoke(
|
|
||||||
url = url,
|
|
||||||
sessionId = tab.id,
|
|
||||||
flags = EngineSession.LoadUrlFlags.select(EXTERNAL, LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
companion object {
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
internal var userDoNotInterceptCache: MutableMap<Int, Long> = mutableMapOf()
|
internal var userDoNotInterceptCache: MutableMap<Int, Long> = mutableMapOf()
|
||||||
@@ -443,7 +261,43 @@ class AppLinksInterceptor(
|
|||||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
internal const val APP_LINKS_DO_NOT_INTERCEPT_INTERVAL = 2000L // 2 second
|
internal const val APP_LINKS_DO_NOT_INTERCEPT_INTERVAL = 2000L // 2 second
|
||||||
|
|
||||||
// Minimum time for dialog to settle before accepting user interactions.
|
/**
|
||||||
internal const val MAX_SUCCESSIVE_DIALOG_MILLIS_LIMIT: Int = 500 // 0.5 seconds
|
* Determines whether a given tab session is being used for authentication purposes.
|
||||||
|
*
|
||||||
|
* @param sessionState The current [SessionState], representing the tab session to inspect.
|
||||||
|
*
|
||||||
|
* @return `true` if the tab session is possible authentication flow, `false` otherwise.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun isPossibleAuthentication(sessionState: SessionState?): Boolean {
|
||||||
|
return when (sessionState?.source) {
|
||||||
|
// CustomTab and ActionView can be used for authentication
|
||||||
|
is SessionState.Source.External.CustomTab,
|
||||||
|
is SessionState.Source.External.ActionView,
|
||||||
|
-> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether a given tab session is being used for authentication purposes.
|
||||||
|
*
|
||||||
|
* @param sessionState The current [SessionState], representing the tab session to inspect.
|
||||||
|
* @param packageName The target package name used to match with the caller's package name.
|
||||||
|
*
|
||||||
|
* @return `true` if the tab session is an authentication flow from the same app, `false` otherwise.
|
||||||
|
*/
|
||||||
|
fun isAuthentication(sessionState: SessionState?, packageName: String?): Boolean {
|
||||||
|
if (packageName != null && isPossibleAuthentication(sessionState)) {
|
||||||
|
val callerPackageId =
|
||||||
|
(sessionState?.source as? SessionState.Source.External)?.caller?.packageId
|
||||||
|
|
||||||
|
if (callerPackageId == packageName) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,14 +73,6 @@ class AppLinksUseCases(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update launchInApp for this instance of AppLinksUseCases
|
|
||||||
* @param launchInApp the new value of launchInApp
|
|
||||||
*/
|
|
||||||
fun updateLaunchInApp(launchInApp: () -> Boolean) {
|
|
||||||
this.launchInApp = launchInApp
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findDefaultActivity(intent: Intent): ResolveInfo? {
|
private fun findDefaultActivity(intent: Intent): ResolveInfo? {
|
||||||
return context.packageManager.resolveActivityCompat(intent, PackageManager.MATCH_DEFAULT_ONLY)
|
return context.packageManager.resolveActivityCompat(intent, PackageManager.MATCH_DEFAULT_ONLY)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,35 +111,35 @@ class AppLinksFeatureTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `feature observes app intents when started`() {
|
fun `WHEN feature started THEN feature observes app intents`() {
|
||||||
val tab = createTab(webUrl)
|
val tab = createTab(webUrl)
|
||||||
store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
|
store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
|
||||||
verify(feature, never()).handleAppIntent(any(), any(), any())
|
verify(feature, never()).handleAppIntent(any(), any(), any(), any(), any())
|
||||||
|
|
||||||
val intent: Intent = mock()
|
val intent: Intent = mock()
|
||||||
val appIntent = AppIntentState(intentUrl, intent)
|
val appIntent = AppIntentState(intentUrl, intent, null, null)
|
||||||
store.dispatch(ContentAction.UpdateAppIntentAction(tab.id, appIntent)).joinBlocking()
|
store.dispatch(ContentAction.UpdateAppIntentAction(tab.id, appIntent)).joinBlocking()
|
||||||
|
|
||||||
store.waitUntilIdle()
|
store.waitUntilIdle()
|
||||||
verify(feature).handleAppIntent(any(), any(), any())
|
verify(feature).handleAppIntent(any(), any(), any(), any(), any())
|
||||||
|
|
||||||
val tabWithConsumedAppIntent = store.state.findTab(tab.id)!!
|
val tabWithConsumedAppIntent = store.state.findTab(tab.id)!!
|
||||||
assertNull(tabWithConsumedAppIntent.content.appIntent)
|
assertNull(tabWithConsumedAppIntent.content.appIntent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `feature doesn't observes app intents when stopped`() {
|
fun `WHEN feature is stopped THEN feature doesn't observes app intents`() {
|
||||||
val tab = createTab(webUrl)
|
val tab = createTab(webUrl)
|
||||||
store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
|
store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
|
||||||
verify(feature, never()).handleAppIntent(any(), any(), any())
|
verify(feature, never()).handleAppIntent(any(), any(), any(), any(), any())
|
||||||
|
|
||||||
feature.stop()
|
feature.stop()
|
||||||
|
|
||||||
val intent: Intent = mock()
|
val intent: Intent = mock()
|
||||||
val appIntent = AppIntentState(intentUrl, intent)
|
val appIntent = AppIntentState(intentUrl, intent, null, null)
|
||||||
store.dispatch(ContentAction.UpdateAppIntentAction(tab.id, appIntent)).joinBlocking()
|
store.dispatch(ContentAction.UpdateAppIntentAction(tab.id, appIntent)).joinBlocking()
|
||||||
|
|
||||||
verify(feature, never()).handleAppIntent(any(), any(), any())
|
verify(feature, never()).handleAppIntent(any(), any(), any(), any(), any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -159,7 +159,7 @@ class AppLinksFeatureTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val tab = createTab(webUrl)
|
val tab = createTab(webUrl)
|
||||||
feature.handleAppIntent(tab, intentUrl, mock())
|
feature.handleAppIntent(tab, intentUrl, mock(), null, null)
|
||||||
|
|
||||||
verify(mockDialog).showNow(eq(mockFragmentManager), anyString())
|
verify(mockDialog).showNow(eq(mockFragmentManager), anyString())
|
||||||
verify(mockOpenRedirect, never()).invoke(any(), anyBoolean(), any())
|
verify(mockOpenRedirect, never()).invoke(any(), anyBoolean(), any())
|
||||||
@@ -182,7 +182,7 @@ class AppLinksFeatureTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val tab = createTab(webUrl)
|
val tab = createTab(webUrl)
|
||||||
feature.handleAppIntent(tab, intentUrl, mock())
|
feature.handleAppIntent(tab, intentUrl, mock(), null, null)
|
||||||
|
|
||||||
verify(mockDialog, never()).showNow(eq(mockFragmentManager), anyString())
|
verify(mockDialog, never()).showNow(eq(mockFragmentManager), anyString())
|
||||||
}
|
}
|
||||||
@@ -217,7 +217,7 @@ class AppLinksFeatureTest {
|
|||||||
doReturn(componentName).`when`(appIntent).component
|
doReturn(componentName).`when`(appIntent).component
|
||||||
doReturn("com.zxing.app").`when`(componentName).packageName
|
doReturn("com.zxing.app").`when`(componentName).packageName
|
||||||
|
|
||||||
feature.handleAppIntent(tab, intentUrl, appIntent)
|
feature.handleAppIntent(tab, intentUrl, appIntent, null, null)
|
||||||
|
|
||||||
verify(mockDialog, never()).showNow(eq(mockFragmentManager), anyString())
|
verify(mockDialog, never()).showNow(eq(mockFragmentManager), anyString())
|
||||||
}
|
}
|
||||||
@@ -252,7 +252,7 @@ class AppLinksFeatureTest {
|
|||||||
doReturn(componentName).`when`(appIntent).component
|
doReturn(componentName).`when`(appIntent).component
|
||||||
doReturn("com.zxing.app").`when`(componentName).packageName
|
doReturn("com.zxing.app").`when`(componentName).packageName
|
||||||
|
|
||||||
feature.handleAppIntent(tab, intentUrl, appIntent)
|
feature.handleAppIntent(tab, intentUrl, appIntent, null, null)
|
||||||
|
|
||||||
verify(mockDialog, never()).showNow(eq(mockFragmentManager), anyString())
|
verify(mockDialog, never()).showNow(eq(mockFragmentManager), anyString())
|
||||||
}
|
}
|
||||||
@@ -287,7 +287,7 @@ class AppLinksFeatureTest {
|
|||||||
doReturn(componentName).`when`(appIntent).component
|
doReturn(componentName).`when`(appIntent).component
|
||||||
doReturn("com.zxing.app").`when`(componentName).packageName
|
doReturn("com.zxing.app").`when`(componentName).packageName
|
||||||
|
|
||||||
feature.handleAppIntent(tab, intentUrl, appIntent)
|
feature.handleAppIntent(tab, intentUrl, appIntent, null, null)
|
||||||
|
|
||||||
verify(mockDialog).showNow(eq(mockFragmentManager), anyString())
|
verify(mockDialog).showNow(eq(mockFragmentManager), anyString())
|
||||||
verify(mockOpenRedirect, never()).invoke(any(), anyBoolean(), any())
|
verify(mockOpenRedirect, never()).invoke(any(), anyBoolean(), any())
|
||||||
@@ -323,7 +323,7 @@ class AppLinksFeatureTest {
|
|||||||
doReturn(componentName).`when`(appIntent).component
|
doReturn(componentName).`when`(appIntent).component
|
||||||
doReturn("com.zxing.app").`when`(componentName).packageName
|
doReturn("com.zxing.app").`when`(componentName).packageName
|
||||||
|
|
||||||
feature.handleAppIntent(tab, intentUrl, appIntent)
|
feature.handleAppIntent(tab, intentUrl, appIntent, null, null)
|
||||||
|
|
||||||
verify(mockDialog).showNow(eq(mockFragmentManager), anyString())
|
verify(mockDialog).showNow(eq(mockFragmentManager), anyString())
|
||||||
verify(mockOpenRedirect, never()).invoke(any(), anyBoolean(), any())
|
verify(mockOpenRedirect, never()).invoke(any(), anyBoolean(), any())
|
||||||
@@ -346,7 +346,7 @@ class AppLinksFeatureTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val tab = createTab(webUrl, private = true)
|
val tab = createTab(webUrl, private = true)
|
||||||
feature.handleAppIntent(tab, intentUrl, mock())
|
feature.handleAppIntent(tab, intentUrl, mock(), null, null)
|
||||||
|
|
||||||
verify(mockDialog).showNow(eq(mockFragmentManager), anyString())
|
verify(mockDialog).showNow(eq(mockFragmentManager), anyString())
|
||||||
verify(mockOpenRedirect, never()).invoke(any(), anyBoolean(), any())
|
verify(mockOpenRedirect, never()).invoke(any(), anyBoolean(), any())
|
||||||
@@ -369,7 +369,7 @@ class AppLinksFeatureTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val tab = createTab(webUrl, private = true)
|
val tab = createTab(webUrl, private = true)
|
||||||
feature.handleAppIntent(tab, intentUrl, mock())
|
feature.handleAppIntent(tab, intentUrl, mock(), null, null)
|
||||||
|
|
||||||
verify(mockDialog).showNow(eq(mockFragmentManager), anyString())
|
verify(mockDialog).showNow(eq(mockFragmentManager), anyString())
|
||||||
verify(mockOpenRedirect, never()).invoke(any(), anyBoolean(), any())
|
verify(mockOpenRedirect, never()).invoke(any(), anyBoolean(), any())
|
||||||
@@ -378,60 +378,41 @@ class AppLinksFeatureTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `redirect dialog is only added once`() {
|
fun `redirect dialog is only added once`() {
|
||||||
val tab = createTab(webUrl, private = true)
|
val tab = createTab(webUrl, private = true)
|
||||||
feature.handleAppIntent(tab, intentUrl, mock())
|
feature.handleAppIntent(tab, intentUrl, mock(), null, null)
|
||||||
|
|
||||||
verify(mockDialog).showNow(eq(mockFragmentManager), anyString())
|
verify(mockDialog).showNow(eq(mockFragmentManager), anyString())
|
||||||
|
|
||||||
doReturn(mockDialog).`when`(feature).getOrCreateDialog(false, "")
|
doReturn(mockDialog).`when`(feature).getOrCreateDialog(false, "", null)
|
||||||
doReturn(mockDialog).`when`(mockFragmentManager).findFragmentByTag(RedirectDialogFragment.FRAGMENT_TAG)
|
doReturn(mockDialog).`when`(mockFragmentManager).findFragmentByTag(RedirectDialogFragment.FRAGMENT_TAG)
|
||||||
feature.handleAppIntent(tab, intentUrl, mock())
|
feature.handleAppIntent(tab, intentUrl, mock(), null, null)
|
||||||
verify(mockDialog, times(1)).showNow(mockFragmentManager, RedirectDialogFragment.FRAGMENT_TAG)
|
verify(mockDialog, times(1)).showNow(mockFragmentManager, RedirectDialogFragment.FRAGMENT_TAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `only loads URL if scheme is supported`() {
|
fun `WHEN url is not supported THEN isSchemeSupported returns false`() {
|
||||||
val tab = createTab(webUrl, private = true)
|
assertFalse(feature.isSchemeSupported(intentUrl))
|
||||||
|
assertTrue(feature.isSchemeSupported(webUrl))
|
||||||
feature.loadUrlIfSchemeSupported(tab, intentUrl)
|
assertTrue(feature.isSchemeSupported(aboutUrl))
|
||||||
verify(mockLoadUrlUseCase, never()).invoke(anyString(), anyString(), any(), any(), any())
|
|
||||||
|
|
||||||
feature.loadUrlIfSchemeSupported(tab, webUrl)
|
|
||||||
verify(mockLoadUrlUseCase, times(1)).invoke(anyString(), anyString(), any(), any(), any())
|
|
||||||
|
|
||||||
feature.loadUrlIfSchemeSupported(tab, aboutUrl)
|
|
||||||
verify(mockLoadUrlUseCase, times(2)).invoke(anyString(), anyString(), any(), any(), any())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `WHEN caller and intent have the same package name THEN return true`() {
|
fun `WHEN url or fallback url scheme is supported THEN cancel redirect will load it`() {
|
||||||
val customTab =
|
|
||||||
createCustomTab(
|
|
||||||
id = "c",
|
|
||||||
url = webUrl,
|
|
||||||
source = SessionState.Source.External.CustomTab(
|
|
||||||
ExternalPackage("com.zxing.app", PackageCategory.PRODUCTIVITY),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
val appIntent: Intent = mock()
|
|
||||||
val componentName: ComponentName = mock()
|
|
||||||
doReturn(componentName).`when`(appIntent).component
|
|
||||||
doReturn("com.zxing.app").`when`(componentName).packageName
|
|
||||||
assertTrue(feature.isAuthentication(customTab, appIntent))
|
|
||||||
|
|
||||||
val tab = createTab(webUrl, private = true)
|
val tab = createTab(webUrl, private = true)
|
||||||
assertFalse(feature.isAuthentication(tab, appIntent))
|
val intent: Intent = mock()
|
||||||
|
|
||||||
val customTab2 =
|
feature.cancelRedirect(tab, intentUrl, null, intent)
|
||||||
createCustomTab(
|
verify(mockLoadUrlUseCase, never()).invoke(anyString(), anyString(), any(), any(), any())
|
||||||
id = "c",
|
|
||||||
url = webUrl,
|
|
||||||
source = SessionState.Source.External.CustomTab(
|
|
||||||
ExternalPackage("com.example.app", PackageCategory.PRODUCTIVITY),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
assertFalse(feature.isAuthentication(customTab2, appIntent))
|
|
||||||
|
|
||||||
doReturn(null).`when`(componentName).packageName
|
feature.cancelRedirect(tab, intentUrl, intentUrl, intent)
|
||||||
assertFalse(feature.isAuthentication(customTab, appIntent))
|
verify(mockLoadUrlUseCase, never()).invoke(anyString(), anyString(), any(), any(), any())
|
||||||
|
|
||||||
|
feature.cancelRedirect(tab, webUrl, null, intent)
|
||||||
|
verify(mockLoadUrlUseCase, times(1)).invoke(anyString(), anyString(), any(), any(), any())
|
||||||
|
|
||||||
|
feature.cancelRedirect(tab, aboutUrl, null, intent)
|
||||||
|
verify(mockLoadUrlUseCase, times(2)).invoke(anyString(), anyString(), any(), any(), any())
|
||||||
|
|
||||||
|
feature.cancelRedirect(tab, intentUrl, aboutUrl, intent)
|
||||||
|
verify(mockLoadUrlUseCase, times(3)).invoke(anyString(), anyString(), any(), any(), any())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import mozilla.components.browser.state.state.ExternalPackage
|
|||||||
import mozilla.components.browser.state.state.PackageCategory
|
import mozilla.components.browser.state.state.PackageCategory
|
||||||
import mozilla.components.browser.state.state.SessionState
|
import mozilla.components.browser.state.state.SessionState
|
||||||
import mozilla.components.browser.state.state.TabSessionState
|
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.browser.state.store.BrowserStore
|
||||||
import mozilla.components.concept.engine.EngineSession
|
import mozilla.components.concept.engine.EngineSession
|
||||||
import mozilla.components.concept.engine.request.RequestInterceptor
|
import mozilla.components.concept.engine.request.RequestInterceptor
|
||||||
@@ -28,7 +26,6 @@ import mozilla.components.feature.app.links.AppLinksInterceptor.Companion.lastAp
|
|||||||
import mozilla.components.feature.app.links.AppLinksInterceptor.Companion.userDoNotInterceptCache
|
import mozilla.components.feature.app.links.AppLinksInterceptor.Companion.userDoNotInterceptCache
|
||||||
import mozilla.components.feature.session.SessionUseCases
|
import mozilla.components.feature.session.SessionUseCases
|
||||||
import mozilla.components.support.test.any
|
import mozilla.components.support.test.any
|
||||||
import mozilla.components.support.test.eq
|
|
||||||
import mozilla.components.support.test.mock
|
import mozilla.components.support.test.mock
|
||||||
import mozilla.components.support.test.whenever
|
import mozilla.components.support.test.whenever
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
@@ -39,9 +36,7 @@ import org.junit.Before
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.mockito.ArgumentMatchers.anyBoolean
|
import org.mockito.ArgumentMatchers.anyBoolean
|
||||||
import org.mockito.ArgumentMatchers.anyString
|
|
||||||
import org.mockito.Mockito.doReturn
|
import org.mockito.Mockito.doReturn
|
||||||
import org.mockito.Mockito.never
|
|
||||||
import org.mockito.Mockito.spy
|
import org.mockito.Mockito.spy
|
||||||
import org.mockito.Mockito.times
|
import org.mockito.Mockito.times
|
||||||
import org.mockito.Mockito.verify
|
import org.mockito.Mockito.verify
|
||||||
@@ -65,7 +60,6 @@ class AppLinksInterceptorTest {
|
|||||||
private val intentUrl = "zxing://scan;S.browser_fallback_url=example.com"
|
private val intentUrl = "zxing://scan;S.browser_fallback_url=example.com"
|
||||||
private val fallbackUrl = "https://getpocket.com"
|
private val fallbackUrl = "https://getpocket.com"
|
||||||
private val marketplaceUrl = "market://details?id=example.com"
|
private val marketplaceUrl = "market://details?id=example.com"
|
||||||
private val aboutUrl = "about://scan"
|
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
@@ -98,7 +92,6 @@ class AppLinksInterceptorTest {
|
|||||||
|
|
||||||
appLinksInterceptor = AppLinksInterceptor(
|
appLinksInterceptor = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = true,
|
|
||||||
launchInApp = { true },
|
launchInApp = { true },
|
||||||
useCases = mockUseCases,
|
useCases = mockUseCases,
|
||||||
)
|
)
|
||||||
@@ -129,47 +122,27 @@ class AppLinksInterceptorTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `request is not intercepted when interceptLinkClicks is false`() {
|
fun `WHEN launchInApp preference is false THEN request is not intercepted`() {
|
||||||
|
appLinksInterceptor = AppLinksInterceptor(
|
||||||
|
context = mockContext,
|
||||||
|
launchInApp = { false },
|
||||||
|
useCases = mockUseCases,
|
||||||
|
)
|
||||||
|
|
||||||
|
val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, true, false, false, false, false)
|
||||||
|
assertEquals(null, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `WHEN launchInApp preference is true THEN request is intercepted`() {
|
||||||
appLinksInterceptor = AppLinksInterceptor(
|
appLinksInterceptor = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = false,
|
|
||||||
launchInApp = { true },
|
launchInApp = { true },
|
||||||
useCases = mockUseCases,
|
useCases = mockUseCases,
|
||||||
)
|
)
|
||||||
|
|
||||||
val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, true, false, false, false, false)
|
val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, true, false, false, false, false)
|
||||||
assertEquals(null, response)
|
assert(response is RequestInterceptor.InterceptionResponse.AppIntent)
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `request is not intercepted when launchInApp preference is false`() {
|
|
||||||
appLinksInterceptor = AppLinksInterceptor(
|
|
||||||
context = mockContext,
|
|
||||||
interceptLinkClicks = true,
|
|
||||||
launchInApp = { false },
|
|
||||||
useCases = mockUseCases,
|
|
||||||
)
|
|
||||||
|
|
||||||
val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, true, false, false, false, false)
|
|
||||||
assertEquals(null, response)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `request is not intercepted when launchInApp preference is updated to false`() {
|
|
||||||
appLinksInterceptor = AppLinksInterceptor(
|
|
||||||
context = mockContext,
|
|
||||||
interceptLinkClicks = true,
|
|
||||||
launchInApp = { false },
|
|
||||||
useCases = mockUseCases,
|
|
||||||
)
|
|
||||||
|
|
||||||
val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, true, false, false, false, false)
|
|
||||||
assertEquals(null, response)
|
|
||||||
|
|
||||||
appLinksInterceptor.updateLaunchInApp { true }
|
|
||||||
verify(mockUseCases).updateLaunchInApp(any())
|
|
||||||
val response2 = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, true, false, false, false, false)
|
|
||||||
assert(response2 is RequestInterceptor.InterceptionResponse.AppIntent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -229,7 +202,6 @@ class AppLinksInterceptorTest {
|
|||||||
val blocklistedScheme = "blocklisted"
|
val blocklistedScheme = "blocklisted"
|
||||||
val feature = AppLinksInterceptor(
|
val feature = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = true,
|
|
||||||
alwaysDeniedSchemes = setOf(blocklistedScheme),
|
alwaysDeniedSchemes = setOf(blocklistedScheme),
|
||||||
launchInApp = { true },
|
launchInApp = { true },
|
||||||
useCases = mockUseCases,
|
useCases = mockUseCases,
|
||||||
@@ -248,7 +220,6 @@ class AppLinksInterceptorTest {
|
|||||||
val supportedScheme = "supported"
|
val supportedScheme = "supported"
|
||||||
val feature = AppLinksInterceptor(
|
val feature = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = true,
|
|
||||||
engineSupportedSchemes = setOf(supportedScheme),
|
engineSupportedSchemes = setOf(supportedScheme),
|
||||||
launchInApp = { false },
|
launchInApp = { false },
|
||||||
useCases = mockUseCases,
|
useCases = mockUseCases,
|
||||||
@@ -261,32 +232,12 @@ class AppLinksInterceptorTest {
|
|||||||
assertEquals(null, response)
|
assertEquals(null, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `supported schemes request not launched if interceptLinkClicks is false`() {
|
|
||||||
val engineSession: EngineSession = mock()
|
|
||||||
val supportedScheme = "supported"
|
|
||||||
val feature = AppLinksInterceptor(
|
|
||||||
context = mockContext,
|
|
||||||
interceptLinkClicks = false,
|
|
||||||
engineSupportedSchemes = setOf(supportedScheme),
|
|
||||||
launchInApp = { true },
|
|
||||||
useCases = mockUseCases,
|
|
||||||
)
|
|
||||||
|
|
||||||
val supportedUrl = "$supportedScheme://example.com"
|
|
||||||
val supportedRedirect = AppLinkRedirect(Intent.parseUri(supportedUrl, 0), "", null, null)
|
|
||||||
whenever(mockGetRedirect.invoke(supportedUrl)).thenReturn(supportedRedirect)
|
|
||||||
val response = feature.onLoadRequest(engineSession, supportedUrl, null, true, false, false, false, false)
|
|
||||||
assertEquals(null, response)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `supported schemes request not launched if not triggered by user`() {
|
fun `supported schemes request not launched if not triggered by user`() {
|
||||||
val engineSession: EngineSession = mock()
|
val engineSession: EngineSession = mock()
|
||||||
val supportedScheme = "supported"
|
val supportedScheme = "supported"
|
||||||
val feature = AppLinksInterceptor(
|
val feature = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = true,
|
|
||||||
engineSupportedSchemes = setOf(supportedScheme),
|
engineSupportedSchemes = setOf(supportedScheme),
|
||||||
launchInApp = { true },
|
launchInApp = { true },
|
||||||
useCases = mockUseCases,
|
useCases = mockUseCases,
|
||||||
@@ -300,14 +251,13 @@ class AppLinksInterceptorTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `not supported schemes request always intercepted regardless of hasUserGesture, interceptLinkClicks or launchInApp`() {
|
fun `WHEN request is in not supported schemes THEN it is always intercepted regardless of hasUserGesture or launchInApp`() {
|
||||||
val engineSession: EngineSession = mock()
|
val engineSession: EngineSession = mock()
|
||||||
val supportedScheme = "supported"
|
val supportedScheme = "supported"
|
||||||
val notSupportedScheme = "not_supported"
|
val notSupportedScheme = "not_supported"
|
||||||
val blocklistedScheme = "blocklisted"
|
val blocklistedScheme = "blocklisted"
|
||||||
val feature = AppLinksInterceptor(
|
val feature = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = false,
|
|
||||||
engineSupportedSchemes = setOf(supportedScheme),
|
engineSupportedSchemes = setOf(supportedScheme),
|
||||||
alwaysDeniedSchemes = setOf(blocklistedScheme),
|
alwaysDeniedSchemes = setOf(blocklistedScheme),
|
||||||
launchInApp = { false },
|
launchInApp = { false },
|
||||||
@@ -328,7 +278,6 @@ class AppLinksInterceptorTest {
|
|||||||
val notSupportedScheme = "not_supported"
|
val notSupportedScheme = "not_supported"
|
||||||
val feature = AppLinksInterceptor(
|
val feature = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = false,
|
|
||||||
engineSupportedSchemes = setOf(supportedScheme),
|
engineSupportedSchemes = setOf(supportedScheme),
|
||||||
alwaysDeniedSchemes = setOf(notSupportedScheme),
|
alwaysDeniedSchemes = setOf(notSupportedScheme),
|
||||||
launchInApp = { false },
|
launchInApp = { false },
|
||||||
@@ -350,7 +299,6 @@ class AppLinksInterceptorTest {
|
|||||||
val blocklistedScheme = "blocklisted"
|
val blocklistedScheme = "blocklisted"
|
||||||
val feature = AppLinksInterceptor(
|
val feature = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = false,
|
|
||||||
engineSupportedSchemes = setOf(supportedScheme),
|
engineSupportedSchemes = setOf(supportedScheme),
|
||||||
alwaysDeniedSchemes = setOf(blocklistedScheme),
|
alwaysDeniedSchemes = setOf(blocklistedScheme),
|
||||||
launchInApp = { true },
|
launchInApp = { true },
|
||||||
@@ -372,7 +320,6 @@ class AppLinksInterceptorTest {
|
|||||||
val blocklistedScheme = "blocklisted"
|
val blocklistedScheme = "blocklisted"
|
||||||
val feature = AppLinksInterceptor(
|
val feature = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = true,
|
|
||||||
engineSupportedSchemes = setOf(supportedScheme),
|
engineSupportedSchemes = setOf(supportedScheme),
|
||||||
alwaysDeniedSchemes = setOf(blocklistedScheme),
|
alwaysDeniedSchemes = setOf(blocklistedScheme),
|
||||||
launchInApp = { false },
|
launchInApp = { false },
|
||||||
@@ -395,7 +342,6 @@ class AppLinksInterceptorTest {
|
|||||||
val blocklistedScheme = "blocklisted"
|
val blocklistedScheme = "blocklisted"
|
||||||
val feature = AppLinksInterceptor(
|
val feature = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = true,
|
|
||||||
engineSupportedSchemes = setOf(supportedScheme),
|
engineSupportedSchemes = setOf(supportedScheme),
|
||||||
alwaysDeniedSchemes = setOf(blocklistedScheme),
|
alwaysDeniedSchemes = setOf(blocklistedScheme),
|
||||||
launchInApp = { false },
|
launchInApp = { false },
|
||||||
@@ -415,7 +361,6 @@ class AppLinksInterceptorTest {
|
|||||||
val engineSession: EngineSession = mock()
|
val engineSession: EngineSession = mock()
|
||||||
val feature = AppLinksInterceptor(
|
val feature = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = false,
|
|
||||||
launchInApp = { false },
|
launchInApp = { false },
|
||||||
useCases = mockUseCases,
|
useCases = mockUseCases,
|
||||||
)
|
)
|
||||||
@@ -432,7 +377,6 @@ class AppLinksInterceptorTest {
|
|||||||
val engineSession: EngineSession = mock()
|
val engineSession: EngineSession = mock()
|
||||||
val feature = AppLinksInterceptor(
|
val feature = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = false,
|
|
||||||
launchInApp = { false },
|
launchInApp = { false },
|
||||||
useCases = mockUseCases,
|
useCases = mockUseCases,
|
||||||
)
|
)
|
||||||
@@ -472,11 +416,9 @@ class AppLinksInterceptorTest {
|
|||||||
fun `external app is launched when launch in app is set to true and it is user triggered`() {
|
fun `external app is launched when launch in app is set to true and it is user triggered`() {
|
||||||
appLinksInterceptor = AppLinksInterceptor(
|
appLinksInterceptor = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = true,
|
|
||||||
launchInApp = { true },
|
launchInApp = { true },
|
||||||
useCases = mockUseCases,
|
useCases = mockUseCases,
|
||||||
launchFromInterceptor = true,
|
launchFromInterceptor = true,
|
||||||
shouldPrompt = { false },
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, true, false, false, false, false)
|
val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, true, false, false, false, false)
|
||||||
@@ -488,15 +430,13 @@ class AppLinksInterceptorTest {
|
|||||||
fun `external app is launched when launchInApp settings is set to AlwaysAsk and it is user triggered`() {
|
fun `external app is launched when launchInApp settings is set to AlwaysAsk and it is user triggered`() {
|
||||||
appLinksInterceptor = AppLinksInterceptor(
|
appLinksInterceptor = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = true,
|
|
||||||
launchInApp = { true },
|
launchInApp = { true },
|
||||||
useCases = mockUseCases,
|
useCases = mockUseCases,
|
||||||
launchFromInterceptor = true,
|
launchFromInterceptor = true,
|
||||||
shouldPrompt = { true },
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, true, false, false, false, false)
|
val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, true, false, false, false, false)
|
||||||
assertNull(response)
|
assert(response is RequestInterceptor.InterceptionResponse.AppIntent)
|
||||||
verify(mockOpenRedirect).invoke(any(), anyBoolean(), any())
|
verify(mockOpenRedirect).invoke(any(), anyBoolean(), any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,11 +444,9 @@ class AppLinksInterceptorTest {
|
|||||||
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`() {
|
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(
|
appLinksInterceptor = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = true,
|
|
||||||
launchInApp = { true },
|
launchInApp = { true },
|
||||||
useCases = mockUseCases,
|
useCases = mockUseCases,
|
||||||
launchFromInterceptor = false,
|
launchFromInterceptor = false,
|
||||||
shouldPrompt = { false },
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, true, false, false, false, false)
|
val response = appLinksInterceptor.onLoadRequest(mockEngineSession, webUrlWithAppLink, null, true, false, false, false, false)
|
||||||
@@ -520,7 +458,6 @@ class AppLinksInterceptorTest {
|
|||||||
fun `try to use fallback url if user preference is not to launch in third party app`() {
|
fun `try to use fallback url if user preference is not to launch in third party app`() {
|
||||||
appLinksInterceptor = AppLinksInterceptor(
|
appLinksInterceptor = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = true,
|
|
||||||
launchInApp = { false },
|
launchInApp = { false },
|
||||||
useCases = mockUseCases,
|
useCases = mockUseCases,
|
||||||
launchFromInterceptor = true,
|
launchFromInterceptor = true,
|
||||||
@@ -532,17 +469,16 @@ class AppLinksInterceptorTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `external app is launched when url scheme is not supported by the engine`() {
|
fun `WHEN url scheme is not supported by the engine THEN external app is launched`() {
|
||||||
appLinksInterceptor = AppLinksInterceptor(
|
appLinksInterceptor = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = true,
|
|
||||||
launchInApp = { false },
|
launchInApp = { false },
|
||||||
useCases = mockUseCases,
|
useCases = mockUseCases,
|
||||||
launchFromInterceptor = true,
|
launchFromInterceptor = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
val response = appLinksInterceptor.onLoadRequest(mockEngineSession, intentUrl, null, false, true, false, false, false)
|
val response = appLinksInterceptor.onLoadRequest(mockEngineSession, intentUrl, null, false, true, false, false, false)
|
||||||
assertNull(response)
|
assert(response is RequestInterceptor.InterceptionResponse.AppIntent)
|
||||||
verify(mockOpenRedirect).invoke(any(), anyBoolean(), any())
|
verify(mockOpenRedirect).invoke(any(), anyBoolean(), any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,7 +486,6 @@ class AppLinksInterceptorTest {
|
|||||||
fun `WHEN url scheme is not supported by the engine AND launch from interceptor is false THEN app intent is returned`() {
|
fun `WHEN url scheme is not supported by the engine AND launch from interceptor is false THEN app intent is returned`() {
|
||||||
appLinksInterceptor = AppLinksInterceptor(
|
appLinksInterceptor = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = true,
|
|
||||||
launchInApp = { false },
|
launchInApp = { false },
|
||||||
useCases = mockUseCases,
|
useCases = mockUseCases,
|
||||||
launchFromInterceptor = false,
|
launchFromInterceptor = false,
|
||||||
@@ -565,7 +500,6 @@ class AppLinksInterceptorTest {
|
|||||||
fun `do not use fallback url if trigger by user gesture and preference is to launch in app`() {
|
fun `do not use fallback url if trigger by user gesture and preference is to launch in app`() {
|
||||||
appLinksInterceptor = AppLinksInterceptor(
|
appLinksInterceptor = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = true,
|
|
||||||
launchInApp = { true },
|
launchInApp = { true },
|
||||||
useCases = mockUseCases,
|
useCases = mockUseCases,
|
||||||
launchFromInterceptor = true,
|
launchFromInterceptor = true,
|
||||||
@@ -580,7 +514,6 @@ class AppLinksInterceptorTest {
|
|||||||
fun `launch marketplace intent if available and no external app`() {
|
fun `launch marketplace intent if available and no external app`() {
|
||||||
appLinksInterceptor = AppLinksInterceptor(
|
appLinksInterceptor = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = true,
|
|
||||||
launchInApp = { true },
|
launchInApp = { true },
|
||||||
useCases = mockUseCases,
|
useCases = mockUseCases,
|
||||||
launchFromInterceptor = true,
|
launchFromInterceptor = true,
|
||||||
@@ -595,7 +528,6 @@ class AppLinksInterceptorTest {
|
|||||||
fun `use fallback url if available and no external app`() {
|
fun `use fallback url if available and no external app`() {
|
||||||
appLinksInterceptor = AppLinksInterceptor(
|
appLinksInterceptor = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = true,
|
|
||||||
launchInApp = { true },
|
launchInApp = { true },
|
||||||
useCases = mockUseCases,
|
useCases = mockUseCases,
|
||||||
launchFromInterceptor = true,
|
launchFromInterceptor = true,
|
||||||
@@ -610,7 +542,6 @@ class AppLinksInterceptorTest {
|
|||||||
fun `WHEN url have same domain THEN is same domain returns true ELSE false`() {
|
fun `WHEN url have same domain THEN is same domain returns true ELSE false`() {
|
||||||
appLinksInterceptor = AppLinksInterceptor(
|
appLinksInterceptor = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = true,
|
|
||||||
launchInApp = { true },
|
launchInApp = { true },
|
||||||
useCases = mockUseCases,
|
useCases = mockUseCases,
|
||||||
launchFromInterceptor = true,
|
launchFromInterceptor = true,
|
||||||
@@ -629,7 +560,6 @@ class AppLinksInterceptorTest {
|
|||||||
fun `WHEN request is in user do not intercept cache THEN request is not intercepted`() {
|
fun `WHEN request is in user do not intercept cache THEN request is not intercepted`() {
|
||||||
appLinksInterceptor = AppLinksInterceptor(
|
appLinksInterceptor = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = true,
|
|
||||||
launchInApp = { true },
|
launchInApp = { true },
|
||||||
useCases = mockUseCases,
|
useCases = mockUseCases,
|
||||||
)
|
)
|
||||||
@@ -644,7 +574,6 @@ class AppLinksInterceptorTest {
|
|||||||
fun `WHEN request is in user do not intercept cache but there is a fallback THEN fallback is used`() {
|
fun `WHEN request is in user do not intercept cache but there is a fallback THEN fallback is used`() {
|
||||||
appLinksInterceptor = AppLinksInterceptor(
|
appLinksInterceptor = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = true,
|
|
||||||
launchInApp = { false },
|
launchInApp = { false },
|
||||||
useCases = mockUseCases,
|
useCases = mockUseCases,
|
||||||
launchFromInterceptor = true,
|
launchFromInterceptor = true,
|
||||||
@@ -664,7 +593,6 @@ class AppLinksInterceptorTest {
|
|||||||
val blocklistedScheme = "blocklisted"
|
val blocklistedScheme = "blocklisted"
|
||||||
val feature = AppLinksInterceptor(
|
val feature = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = false,
|
|
||||||
engineSupportedSchemes = setOf(supportedScheme),
|
engineSupportedSchemes = setOf(supportedScheme),
|
||||||
alwaysDeniedSchemes = setOf(blocklistedScheme),
|
alwaysDeniedSchemes = setOf(blocklistedScheme),
|
||||||
launchInApp = { true },
|
launchInApp = { true },
|
||||||
@@ -687,7 +615,6 @@ class AppLinksInterceptorTest {
|
|||||||
val blocklistedScheme = "blocklisted"
|
val blocklistedScheme = "blocklisted"
|
||||||
val feature = AppLinksInterceptor(
|
val feature = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = false,
|
|
||||||
engineSupportedSchemes = setOf(supportedScheme),
|
engineSupportedSchemes = setOf(supportedScheme),
|
||||||
alwaysDeniedSchemes = setOf(blocklistedScheme),
|
alwaysDeniedSchemes = setOf(blocklistedScheme),
|
||||||
launchInApp = { false },
|
launchInApp = { false },
|
||||||
@@ -746,7 +673,6 @@ class AppLinksInterceptorTest {
|
|||||||
fun `WHEN request is redirecting to external app quickly THEN request is not intercepted`() {
|
fun `WHEN request is redirecting to external app quickly THEN request is not intercepted`() {
|
||||||
appLinksInterceptor = AppLinksInterceptor(
|
appLinksInterceptor = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = true,
|
|
||||||
launchInApp = { true },
|
launchInApp = { true },
|
||||||
useCases = mockUseCases,
|
useCases = mockUseCases,
|
||||||
)
|
)
|
||||||
@@ -762,7 +688,6 @@ class AppLinksInterceptorTest {
|
|||||||
fun `WHEN request is redirecting to different app quickly THEN request is intercepted`() {
|
fun `WHEN request is redirecting to different app quickly THEN request is intercepted`() {
|
||||||
appLinksInterceptor = AppLinksInterceptor(
|
appLinksInterceptor = AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
interceptLinkClicks = true,
|
|
||||||
launchInApp = { true },
|
launchInApp = { true },
|
||||||
useCases = mockUseCases,
|
useCases = mockUseCases,
|
||||||
)
|
)
|
||||||
@@ -774,342 +699,17 @@ class AppLinksInterceptorTest {
|
|||||||
assertTrue(response is RequestInterceptor.InterceptionResponse.AppIntent)
|
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(), any())
|
|
||||||
|
|
||||||
appLinksInterceptor.handleIntent(tab, intentUrl, mockAppIntent, null, mock(), "")
|
|
||||||
|
|
||||||
verify(mockDialog).showNow(eq(mockFragmentManager), anyString())
|
|
||||||
verify(mockOpenRedirect, never()).invoke(any(), anyBoolean(), any())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN should prompt AND only have marketing intent 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 mockMarketIntent: Intent = mock()
|
|
||||||
val mockComponentName: ComponentName = mock()
|
|
||||||
whenever(mockComponentName.packageName).thenReturn("com.zxing.app")
|
|
||||||
doReturn(mockDialog).`when`(appLinksInterceptor).getOrCreateDialog(anyBoolean(), any(), any())
|
|
||||||
|
|
||||||
appLinksInterceptor.handleIntent(tab, intentUrl, null, null, mockMarketIntent, "")
|
|
||||||
|
|
||||||
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.handleIntent(tab, intentUrl, mock(), null, 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.handleIntent(tab, intentUrl, appIntent, null, null, "")
|
|
||||||
|
|
||||||
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.handleIntent(tab, intentUrl, appIntent, null, null, "")
|
|
||||||
|
|
||||||
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(), any())
|
|
||||||
|
|
||||||
appLinksInterceptor.handleIntent(tab, intentUrl, appIntent, null, null, "")
|
|
||||||
|
|
||||||
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(), any())
|
|
||||||
|
|
||||||
appLinksInterceptor.handleIntent(tab, intentUrl, appIntent, null, null, "")
|
|
||||||
|
|
||||||
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(), any())
|
|
||||||
|
|
||||||
appLinksInterceptor.handleIntent(tabSessionState, intentUrl, mockAppIntent, null, mock(), "")
|
|
||||||
|
|
||||||
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(), any())
|
|
||||||
|
|
||||||
appLinksInterceptor.handleIntent(tabSessionState, intentUrl, mockAppIntent, null, mock(), "")
|
|
||||||
|
|
||||||
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(), any())
|
|
||||||
|
|
||||||
appLinksInterceptor.handleIntent(tabSessionState, intentUrl, appIntent, null, null, "")
|
|
||||||
|
|
||||||
verify(mockDialog).showNow(eq(mockFragmentManager), anyString())
|
|
||||||
|
|
||||||
doReturn(mockDialog).`when`(appLinksInterceptor).getOrCreateDialog(false, "", "")
|
|
||||||
doReturn(mockDialog).`when`(mockFragmentManager).findFragmentByTag(RedirectDialogFragment.FRAGMENT_TAG)
|
|
||||||
appLinksInterceptor.handleIntent(tabSessionState, intentUrl, mock(), null, mock(), "")
|
|
||||||
verify(mockDialog, times(1)).showNow(mockFragmentManager, RedirectDialogFragment.FRAGMENT_TAG)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `WHEN caller and intent have the same package name THEN return true`() {
|
fun `WHEN caller and intent have the same package name THEN return true`() {
|
||||||
appLinksInterceptor = spy(
|
appLinksInterceptor = spy(
|
||||||
AppLinksInterceptor(
|
AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
store = store,
|
store = store,
|
||||||
interceptLinkClicks = true,
|
|
||||||
launchInApp = { true },
|
launchInApp = { true },
|
||||||
useCases = mockUseCases,
|
useCases = mockUseCases,
|
||||||
shouldPrompt = { true },
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
appLinksInterceptor.updateFragmentManger(mockFragmentManager)
|
|
||||||
|
|
||||||
val tabSessionState = TabSessionState(
|
val tabSessionState = TabSessionState(
|
||||||
id = "tab1",
|
id = "tab1",
|
||||||
content = ContentState(
|
content = ContentState(
|
||||||
@@ -1120,11 +720,7 @@ class AppLinksInterceptorTest {
|
|||||||
source = SessionState.Source.External.CustomTab(ExternalPackage("com.zxing.app", PackageCategory.PRODUCTIVITY)),
|
source = SessionState.Source.External.CustomTab(ExternalPackage("com.zxing.app", PackageCategory.PRODUCTIVITY)),
|
||||||
)
|
)
|
||||||
|
|
||||||
val appIntent: Intent = mock()
|
assertTrue(AppLinksInterceptor.isAuthentication(tabSessionState, "com.zxing.app"))
|
||||||
val componentName: ComponentName = mock()
|
|
||||||
whenever(appIntent.component).thenReturn(componentName)
|
|
||||||
whenever(componentName.packageName).thenReturn("com.zxing.app")
|
|
||||||
assertTrue(appLinksInterceptor.isAuthentication(tabSessionState, appIntent))
|
|
||||||
|
|
||||||
val tabSessionState2 = TabSessionState(
|
val tabSessionState2 = TabSessionState(
|
||||||
id = "tab1",
|
id = "tab1",
|
||||||
@@ -1134,7 +730,7 @@ class AppLinksInterceptorTest {
|
|||||||
isProductUrl = false,
|
isProductUrl = false,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
assertFalse(appLinksInterceptor.isAuthentication(tabSessionState2, appIntent))
|
assertFalse(AppLinksInterceptor.isAuthentication(tabSessionState2, "com.zxing.app"))
|
||||||
|
|
||||||
val tabSessionState3 = TabSessionState(
|
val tabSessionState3 = TabSessionState(
|
||||||
id = "tab1",
|
id = "tab1",
|
||||||
@@ -1145,10 +741,8 @@ class AppLinksInterceptorTest {
|
|||||||
),
|
),
|
||||||
source = SessionState.Source.External.CustomTab(ExternalPackage("com.example.app", PackageCategory.PRODUCTIVITY)),
|
source = SessionState.Source.External.CustomTab(ExternalPackage("com.example.app", PackageCategory.PRODUCTIVITY)),
|
||||||
)
|
)
|
||||||
assertFalse(appLinksInterceptor.isAuthentication(tabSessionState3, appIntent))
|
assertFalse(AppLinksInterceptor.isAuthentication(tabSessionState3, "com.zxing.app"))
|
||||||
|
assertFalse(AppLinksInterceptor.isAuthentication(tabSessionState, null))
|
||||||
doReturn(null).`when`(componentName).packageName
|
|
||||||
assertFalse(appLinksInterceptor.isAuthentication(tabSessionState, appIntent))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -1157,19 +751,11 @@ class AppLinksInterceptorTest {
|
|||||||
AppLinksInterceptor(
|
AppLinksInterceptor(
|
||||||
context = mockContext,
|
context = mockContext,
|
||||||
store = store,
|
store = store,
|
||||||
interceptLinkClicks = true,
|
|
||||||
launchInApp = { true },
|
launchInApp = { true },
|
||||||
useCases = mockUseCases,
|
useCases = mockUseCases,
|
||||||
shouldPrompt = { true },
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
val appIntent: Intent = mock()
|
|
||||||
val componentName: ComponentName = mock()
|
|
||||||
whenever(appIntent.component).thenReturn(componentName)
|
|
||||||
whenever(componentName.packageName).thenReturn("com.zxing.app")
|
|
||||||
appLinksInterceptor.updateFragmentManger(mockFragmentManager)
|
|
||||||
|
|
||||||
val tabSessionState = TabSessionState(
|
val tabSessionState = TabSessionState(
|
||||||
id = "tab1",
|
id = "tab1",
|
||||||
content = ContentState(
|
content = ContentState(
|
||||||
@@ -1180,7 +766,7 @@ class AppLinksInterceptorTest {
|
|||||||
source = SessionState.Source.External.CustomTab(ExternalPackage("com.zxing.app", PackageCategory.PRODUCTIVITY)),
|
source = SessionState.Source.External.CustomTab(ExternalPackage("com.zxing.app", PackageCategory.PRODUCTIVITY)),
|
||||||
)
|
)
|
||||||
|
|
||||||
assertTrue(appLinksInterceptor.isAuthentication(tabSessionState, appIntent))
|
assertTrue(AppLinksInterceptor.isAuthentication(tabSessionState, "com.zxing.app"))
|
||||||
|
|
||||||
val tabSessionState2 = TabSessionState(
|
val tabSessionState2 = TabSessionState(
|
||||||
id = "tab1",
|
id = "tab1",
|
||||||
@@ -1191,7 +777,7 @@ class AppLinksInterceptorTest {
|
|||||||
),
|
),
|
||||||
source = SessionState.Source.External.ActionView(ExternalPackage("com.zxing.app", PackageCategory.PRODUCTIVITY)),
|
source = SessionState.Source.External.ActionView(ExternalPackage("com.zxing.app", PackageCategory.PRODUCTIVITY)),
|
||||||
)
|
)
|
||||||
assertTrue(appLinksInterceptor.isAuthentication(tabSessionState2, appIntent))
|
assertTrue(AppLinksInterceptor.isAuthentication(tabSessionState2, "com.zxing.app"))
|
||||||
|
|
||||||
val tabSessionState3 = TabSessionState(
|
val tabSessionState3 = TabSessionState(
|
||||||
id = "tab1",
|
id = "tab1",
|
||||||
@@ -1202,7 +788,7 @@ class AppLinksInterceptorTest {
|
|||||||
),
|
),
|
||||||
source = SessionState.Source.External.ActionSend(ExternalPackage("com.zxing.app", PackageCategory.PRODUCTIVITY)),
|
source = SessionState.Source.External.ActionSend(ExternalPackage("com.zxing.app", PackageCategory.PRODUCTIVITY)),
|
||||||
)
|
)
|
||||||
assertFalse(appLinksInterceptor.isAuthentication(tabSessionState3, appIntent))
|
assertFalse(AppLinksInterceptor.isAuthentication(tabSessionState3, "com.zxing.app"))
|
||||||
|
|
||||||
val tabSessionState4 = TabSessionState(
|
val tabSessionState4 = TabSessionState(
|
||||||
id = "tab1",
|
id = "tab1",
|
||||||
@@ -1213,7 +799,7 @@ class AppLinksInterceptorTest {
|
|||||||
),
|
),
|
||||||
source = SessionState.Source.External.ActionSearch(ExternalPackage("com.zxing.app", PackageCategory.PRODUCTIVITY)),
|
source = SessionState.Source.External.ActionSearch(ExternalPackage("com.zxing.app", PackageCategory.PRODUCTIVITY)),
|
||||||
)
|
)
|
||||||
assertFalse(appLinksInterceptor.isAuthentication(tabSessionState4, appIntent))
|
assertFalse(AppLinksInterceptor.isAuthentication(tabSessionState4, "com.zxing.app"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -1224,30 +810,4 @@ class AppLinksInterceptorTest {
|
|||||||
assertFalse(isSubframeAllowed("http")) // we should never allow http for subframes
|
assertFalse(isSubframeAllowed("http")) // we should never allow http for subframes
|
||||||
assertFalse(isSubframeAllowed("https")) // we should never allow https for subframes
|
assertFalse(isSubframeAllowed("https")) // we should never allow https for subframes
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN scheme is supported THEN loads URL`() {
|
|
||||||
val tab = createTab(webUrl, private = true)
|
|
||||||
|
|
||||||
appLinksInterceptor = spy(
|
|
||||||
AppLinksInterceptor(
|
|
||||||
context = mockContext,
|
|
||||||
store = store,
|
|
||||||
interceptLinkClicks = true,
|
|
||||||
launchInApp = { true },
|
|
||||||
useCases = mockUseCases,
|
|
||||||
shouldPrompt = { true },
|
|
||||||
loadUrlUseCase = mockLoadUrlUseCase,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
appLinksInterceptor.loadUrlIfSchemeSupported(tab, intentUrl)
|
|
||||||
verify(mockLoadUrlUseCase, never()).invoke(anyString(), anyString(), any(), any(), any())
|
|
||||||
|
|
||||||
appLinksInterceptor.loadUrlIfSchemeSupported(tab, webUrl)
|
|
||||||
verify(mockLoadUrlUseCase, times(1)).invoke(anyString(), anyString(), any(), any(), any())
|
|
||||||
|
|
||||||
appLinksInterceptor.loadUrlIfSchemeSupported(tab, aboutUrl)
|
|
||||||
verify(mockLoadUrlUseCase, times(2)).invoke(anyString(), anyString(), any(), any(), any())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -676,20 +676,6 @@ class AppLinksUseCasesTest {
|
|||||||
assertEquals(result?.`package`, "org.mozilla.test")
|
assertEquals(result?.`package`, "org.mozilla.test")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN launch in app is updated to true THEN should redirect`() {
|
|
||||||
val context = createContext(Triple(appUrl, appPackage, ""))
|
|
||||||
val subject = AppLinksUseCases(context, { false })
|
|
||||||
|
|
||||||
var redirect = subject.interceptedAppLinkRedirect(appUrl)
|
|
||||||
assertFalse(redirect.isRedirect())
|
|
||||||
|
|
||||||
AppLinksUseCases.clearRedirectCache()
|
|
||||||
subject.updateLaunchInApp { true }
|
|
||||||
redirect = subject.interceptedAppLinkRedirect(appUrl)
|
|
||||||
assertTrue(redirect.isRedirect())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `WHEN opening a app scheme uri WITH fallback URL WHERE the URL is Google PlayStore THEN ignore fallback URL`() {
|
fun `WHEN opening a app scheme uri WITH fallback URL WHERE the URL is Google PlayStore THEN ignore fallback URL`() {
|
||||||
val context = createContext(Triple(appIntentWithPackageAndPlayStoreFallback, appPackage, ""))
|
val context = createContext(Triple(appIntentWithPackageAndPlayStoreFallback, appPackage, ""))
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ import mozilla.components.feature.pwa.intent.WebAppIntentProcessor
|
|||||||
/**
|
/**
|
||||||
* This feature will intercept requests and reopen them in the corresponding installed PWA, if any.
|
* This feature will intercept requests and reopen them in the corresponding installed PWA, if any.
|
||||||
*
|
*
|
||||||
* @param shortcutManager current shortcut manager instance to lookup web app install states
|
* @param context application context used for launching activities or accessing system services
|
||||||
|
* @param manifestStorage Disk storage for [WebAppManifest]. Other components use this class to
|
||||||
|
* reload a saved manifest.
|
||||||
|
* @param launchFromInterceptor flag to determine whether intercepted requests should directly launch
|
||||||
|
* the PWA
|
||||||
*/
|
*/
|
||||||
class WebAppInterceptor(
|
class WebAppInterceptor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@@ -39,7 +43,7 @@ class WebAppInterceptor(
|
|||||||
val intent = createIntentFromUri(startUrl, uri)
|
val intent = createIntentFromUri(startUrl, uri)
|
||||||
|
|
||||||
if (!launchFromInterceptor) {
|
if (!launchFromInterceptor) {
|
||||||
return RequestInterceptor.InterceptionResponse.AppIntent(intent, uri)
|
return RequestInterceptor.InterceptionResponse.AppIntent(intent, uri, null, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_NEW_TASK
|
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
@@ -51,7 +55,10 @@ class WebAppInterceptor(
|
|||||||
/**
|
/**
|
||||||
* Creates a new VIEW_PWA intent for a URL.
|
* Creates a new VIEW_PWA intent for a URL.
|
||||||
*
|
*
|
||||||
* @param uri target URL for the new intent
|
* @param startUrl the original start URL associated with the PWA
|
||||||
|
* @param urlOverride an optional override URL to open instead of the start URL; defaults to [startUrl]
|
||||||
|
*
|
||||||
|
* @return an [Intent] configured to launch the PWA with the given URL
|
||||||
*/
|
*/
|
||||||
private fun createIntentFromUri(startUrl: String, urlOverride: String = startUrl): Intent {
|
private fun createIntentFromUri(startUrl: String, urlOverride: String = startUrl): Intent {
|
||||||
return Intent(WebAppIntentProcessor.ACTION_VIEW_PWA, startUrl.toUri()).apply {
|
return Intent(WebAppIntentProcessor.ACTION_VIEW_PWA, startUrl.toUri()).apply {
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ permalink: /changelog/
|
|||||||
* ⚠️ **Breaking change**: Added `downloadEstimator` property to `DownloadJobState`. [Bug 1956577](https://bugzilla.mozilla.org/show_bug.cgi?id=1956577).
|
* ⚠️ **Breaking change**: Added `downloadEstimator` property to `DownloadJobState`. [Bug 1956577](https://bugzilla.mozilla.org/show_bug.cgi?id=1956577).
|
||||||
* ⚠️ **Breaking change**: Added new 'dateTimeProvider' abstract val to `AbstractFetchDownloadService`. [Bug 1956577](https://bugzilla.mozilla.org/show_bug.cgi?id=1956577).
|
* ⚠️ **Breaking change**: Added new 'dateTimeProvider' abstract val to `AbstractFetchDownloadService`. [Bug 1956577](https://bugzilla.mozilla.org/show_bug.cgi?id=1956577).
|
||||||
* ⚠️ **Breaking change**: Removed Deprecated `Long.toMegabyteOrKilobyteString` in v140. [Bug 1955689](https://bugzilla.mozilla.org/show_bug.cgi?id=1955689).
|
* ⚠️ **Breaking change**: Removed Deprecated `Long.toMegabyteOrKilobyteString` in v140. [Bug 1955689](https://bugzilla.mozilla.org/show_bug.cgi?id=1955689).
|
||||||
|
* **feature-app-links**
|
||||||
|
* Added `alwaysOpenCheckboxAction` parameter to `AppLinksFeature`, this was moved from `AppLinksInterceptor`.
|
||||||
|
* ⚠️ **Breaking change**: Moved prompt functionality back to `AppLinksFeature`.
|
||||||
|
* ⚠️ **Breaking change**: Removed `interceptLinkClicks` parameter from `AppLinksInterceptor` since no usage sets this to false.
|
||||||
|
|
||||||
# 139.0
|
# 139.0
|
||||||
* **feature-downloads**
|
* **feature-downloads**
|
||||||
|
|||||||
@@ -240,10 +240,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
|||||||
customTabId = sessionId,
|
customTabId = sessionId,
|
||||||
)
|
)
|
||||||
|
|
||||||
components.appLinksInterceptor.updateFragmentManger(
|
|
||||||
fragmentManager = parentFragmentManager,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Observe the lifecycle for supported features
|
// Observe the lifecycle for supported features
|
||||||
lifecycle.addObservers(
|
lifecycle.addObservers(
|
||||||
scrollFeature,
|
scrollFeature,
|
||||||
@@ -292,9 +288,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
|||||||
}
|
}
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
components.appLinksInterceptor.updateFragmentManger(
|
|
||||||
fragmentManager = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
binding.engineView.setActivityContext(null)
|
binding.engineView.setActivityContext(null)
|
||||||
_binding = null
|
_binding = null
|
||||||
|
|||||||
@@ -243,8 +243,7 @@ open class DefaultComponents(private val applicationContext: Context) {
|
|||||||
|
|
||||||
val appLinksInterceptor by lazy {
|
val appLinksInterceptor by lazy {
|
||||||
AppLinksInterceptor(
|
AppLinksInterceptor(
|
||||||
applicationContext,
|
context = applicationContext,
|
||||||
interceptLinkClicks = true,
|
|
||||||
launchInApp = {
|
launchInApp = {
|
||||||
applicationContext.components.preferences.getBoolean(PREF_LAUNCH_EXTERNAL_APP, false)
|
applicationContext.components.preferences.getBoolean(PREF_LAUNCH_EXTERNAL_APP, false)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ import mozilla.components.concept.storage.CreditCardEntry
|
|||||||
import mozilla.components.concept.storage.Login
|
import mozilla.components.concept.storage.Login
|
||||||
import mozilla.components.concept.storage.LoginEntry
|
import mozilla.components.concept.storage.LoginEntry
|
||||||
import mozilla.components.feature.accounts.push.SendTabUseCases
|
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.ContextMenuCandidate
|
||||||
import mozilla.components.feature.contextmenu.ContextMenuFeature
|
import mozilla.components.feature.contextmenu.ContextMenuFeature
|
||||||
import mozilla.components.feature.downloads.DownloadsFeature
|
import mozilla.components.feature.downloads.DownloadsFeature
|
||||||
@@ -302,6 +303,7 @@ abstract class BaseBrowserFragment :
|
|||||||
private val lastTabFeature = ViewBoundFeatureWrapper<LastTabFeature>()
|
private val lastTabFeature = ViewBoundFeatureWrapper<LastTabFeature>()
|
||||||
private val contextMenuFeature = ViewBoundFeatureWrapper<ContextMenuFeature>()
|
private val contextMenuFeature = ViewBoundFeatureWrapper<ContextMenuFeature>()
|
||||||
private val downloadsFeature = ViewBoundFeatureWrapper<DownloadsFeature>()
|
private val downloadsFeature = ViewBoundFeatureWrapper<DownloadsFeature>()
|
||||||
|
private val appLinksFeature = ViewBoundFeatureWrapper<AppLinksFeature>()
|
||||||
private val shareResourceFeature = ViewBoundFeatureWrapper<ShareResourceFeature>()
|
private val shareResourceFeature = ViewBoundFeatureWrapper<ShareResourceFeature>()
|
||||||
private val copyDownloadsFeature = ViewBoundFeatureWrapper<CopyDownloadFeature>()
|
private val copyDownloadsFeature = ViewBoundFeatureWrapper<CopyDownloadFeature>()
|
||||||
private val promptsFeature = ViewBoundFeatureWrapper<PromptFeature>()
|
private val promptsFeature = ViewBoundFeatureWrapper<PromptFeature>()
|
||||||
@@ -934,6 +936,33 @@ abstract class BaseBrowserFragment :
|
|||||||
onHide = ::onAutocompleteBarHide,
|
onHide = ::onAutocompleteBarHide,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
appLinksFeature.set(
|
||||||
|
feature = AppLinksFeature(
|
||||||
|
context = requireContext(),
|
||||||
|
store = store,
|
||||||
|
sessionId = tab.id,
|
||||||
|
fragmentManager = parentFragmentManager,
|
||||||
|
launchInApp = { context.settings().shouldOpenLinksInApp(customTabSessionId != null) },
|
||||||
|
loadUrlUseCase = requireComponents.useCases.sessionUseCases.loadUrl,
|
||||||
|
shouldPrompt = { context.settings().shouldPromptOpenLinksInApp() },
|
||||||
|
alwaysOpenCheckboxAction = {
|
||||||
|
context.settings().openLinksInExternalApp =
|
||||||
|
context.getString(R.string.pref_key_open_links_in_apps_always)
|
||||||
|
},
|
||||||
|
failedToLaunchAction = { fallbackUrl ->
|
||||||
|
fallbackUrl?.let {
|
||||||
|
val appLinksUseCases = requireComponents.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 = binding.root,
|
||||||
|
)
|
||||||
|
|
||||||
promptsFeature.set(
|
promptsFeature.set(
|
||||||
feature = PromptFeature(
|
feature = PromptFeature(
|
||||||
activity = activity,
|
activity = activity,
|
||||||
@@ -1854,16 +1883,6 @@ abstract class BaseBrowserFragment :
|
|||||||
}
|
}
|
||||||
hideToolbar()
|
hideToolbar()
|
||||||
|
|
||||||
components.services.appLinksInterceptor.updateFragmentManger(
|
|
||||||
fragmentManager = parentFragmentManager,
|
|
||||||
)
|
|
||||||
context?.settings()?.shouldOpenLinksInApp(customTabSessionId != null)
|
|
||||||
?.let { openLinksInExternalApp ->
|
|
||||||
components.services.appLinksInterceptor.updateLaunchInApp {
|
|
||||||
openLinksInExternalApp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
evaluateMessagesForMicrosurvey(components)
|
evaluateMessagesForMicrosurvey(components)
|
||||||
|
|
||||||
BiometricAuthenticationManager.biometricAuthenticationNeededInfo.shouldShowAuthenticationPrompt =
|
BiometricAuthenticationManager.biometricAuthenticationNeededInfo.shouldShowAuthenticationPrompt =
|
||||||
@@ -1881,10 +1900,6 @@ abstract class BaseBrowserFragment :
|
|||||||
if (findNavController().currentDestination?.id != R.id.searchDialogFragment) {
|
if (findNavController().currentDestination?.id != R.id.searchDialogFragment) {
|
||||||
view?.hideKeyboard()
|
view?.hideKeyboard()
|
||||||
}
|
}
|
||||||
|
|
||||||
requireComponents.services.appLinksInterceptor.updateFragmentManger(
|
|
||||||
fragmentManager = null,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import mozilla.components.browser.state.store.BrowserStore
|
|||||||
import mozilla.components.feature.accounts.FirefoxAccountsAuthFeature
|
import mozilla.components.feature.accounts.FirefoxAccountsAuthFeature
|
||||||
import mozilla.components.feature.app.links.AppLinksInterceptor
|
import mozilla.components.feature.app.links.AppLinksInterceptor
|
||||||
import mozilla.components.service.fxa.manager.FxaAccountManager
|
import mozilla.components.service.fxa.manager.FxaAccountManager
|
||||||
import org.mozilla.fenix.R
|
|
||||||
import org.mozilla.fenix.ext.components
|
|
||||||
import org.mozilla.fenix.ext.settings
|
import org.mozilla.fenix.ext.settings
|
||||||
import org.mozilla.fenix.perf.lazyMonitored
|
import org.mozilla.fenix.perf.lazyMonitored
|
||||||
import org.mozilla.fenix.settings.SupportUtils
|
import org.mozilla.fenix.settings.SupportUtils
|
||||||
@@ -48,17 +46,9 @@ class Services(
|
|||||||
val appLinksInterceptor by lazyMonitored {
|
val appLinksInterceptor by lazyMonitored {
|
||||||
AppLinksInterceptor(
|
AppLinksInterceptor(
|
||||||
context = context,
|
context = context,
|
||||||
interceptLinkClicks = true,
|
|
||||||
showCheckbox = true,
|
|
||||||
launchInApp = { context.settings().shouldOpenLinksInApp() },
|
launchInApp = { context.settings().shouldOpenLinksInApp() },
|
||||||
shouldPrompt = { context.settings().shouldPromptOpenLinksInApp() },
|
launchFromInterceptor = false,
|
||||||
checkboxCheckedAction = {
|
|
||||||
context.settings().openLinksInExternalApp =
|
|
||||||
context.getString(R.string.pref_key_open_links_in_apps_always)
|
|
||||||
},
|
|
||||||
launchFromInterceptor = true,
|
|
||||||
store = store,
|
store = store,
|
||||||
loadUrlUseCase = context.components.useCases.sessionUseCases.loadUrl,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -249,7 +249,6 @@ class Components(
|
|||||||
val appLinksInterceptor by lazy {
|
val appLinksInterceptor by lazy {
|
||||||
AppLinksInterceptor(
|
AppLinksInterceptor(
|
||||||
context,
|
context,
|
||||||
interceptLinkClicks = true,
|
|
||||||
launchInApp = {
|
launchInApp = {
|
||||||
context.settings.openLinksInExternalApp
|
context.settings.openLinksInExternalApp
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -821,13 +821,6 @@ class BrowserFragment :
|
|||||||
if (tab.isCustomTab()) {
|
if (tab.isCustomTab()) {
|
||||||
view?.isVisible = true
|
view?.isVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
context?.settings?.openLinksInExternalApp?.let { openLinksInExternalApp ->
|
|
||||||
val isCustomTab = tab.isCustomTab()
|
|
||||||
components?.appLinksInterceptor?.updateLaunchInApp {
|
|
||||||
openLinksInExternalApp || isCustomTab
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateEngineColorScheme() {
|
private fun updateEngineColorScheme() {
|
||||||
|
|||||||
Reference in New Issue
Block a user