Skip to content
GitLab
Menu
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
The Tor Project
Applications
android-components
Commits
45ef5233
Commit
45ef5233
authored
Jul 10, 2020
by
Roger Yang
Browse files
For #7614: Simplify app link query for external application
parent
27dd1be4
Changes
2
Hide whitespace changes
Inline
Side-by-side
components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksUseCases.kt
View file @
45ef5233
...
...
@@ -8,15 +8,16 @@ import android.content.ActivityNotFoundException
import
android.content.ComponentName
import
android.content.Context
import
android.content.Intent
import
android.content.IntentFilter
import
android.content.pm.PackageManager
import
android.content.pm.ResolveInfo
import
android.net.Uri
import
android.os.SystemClock
import
android.provider.Browser.EXTRA_APPLICATION_ID
import
androidx.annotation.VisibleForTesting
import
mozilla.components.support.base.log.logger.Logger
import
mozilla.components.support.ktx.android.net.isHttpOrHttps
import
java.net.URISyntaxException
import
java.util.UUID
private
const
val
EXTRA_BROWSER_FALLBACK_URL
=
"browser_fallback_url"
private
const
val
MARKET_INTENT_URI_PACKAGE_PREFIX
=
"market://details?id="
...
...
@@ -39,19 +40,17 @@ internal const val APP_LINKS_CACHE_INTERVAL = 30 * 1000L // 30 seconds
* @param context Context the feature is associated with.
* @param launchInApp If {true} then launch app links in third party app(s). Default to false because
* of security concerns.
* @param unguessableWebUrl URL is not likely to be opened by a native app but will fallback to a browser.
* @param alwaysDeniedSchemes List of schemes that will never be opened in a third-party app.
*/
class
AppLinksUseCases
(
private
val
context
:
Context
,
private
val
launchInApp
:
()
->
Boolean
=
{
false
},
private
val
unguessableWebUrl
:
String
=
"https://${UUID.randomUUID()}.net"
,
private
val
alwaysDeniedSchemes
:
Set
<
String
>
=
ALWAYS_DENY_SCHEMES
)
{
@VisibleForTesting
(
otherwise
=
VisibleForTesting
.
PRIVATE
)
internal
fun
findActivities
(
intent
:
Intent
):
List
<
ResolveInfo
>
{
return
context
.
packageManager
.
queryIntentActivities
(
intent
,
PackageManager
.
MATCH_DEFAULT_ONLY
)
?:
emptyList
()
.
queryIntentActivities
(
intent
,
PackageManager
.
GET_RESOLVED_FILTER
)
?:
emptyList
()
}
private
fun
findDefaultActivity
(
intent
:
Intent
):
ResolveInfo
?
{
...
...
@@ -66,31 +65,6 @@ class AppLinksUseCases(
}
}
@VisibleForTesting
(
otherwise
=
VisibleForTesting
.
PRIVATE
)
internal
fun
findExcludedPackages
(
randomWebURLString
:
String
):
Set
<
String
>
{
val
intent
=
safeParseUri
(
randomWebURLString
,
0
)
?:
return
emptySet
()
// We generate a URL is not likely to be opened by a native app
// but will fallback to a browser.
// In this way, we're looking for only the browsers — including us.
return
findActivities
(
intent
.
addCategory
(
Intent
.
CATEGORY_BROWSABLE
))
.
map
{
it
.
activityInfo
.
packageName
}
.
toHashSet
()
}
@VisibleForTesting
(
otherwise
=
VisibleForTesting
.
PRIVATE
)
internal
fun
getBrowserPackageNames
():
Set
<
String
>
{
val
currentTimeStamp
=
SystemClock
.
elapsedRealtime
()
val
cache
=
browserNamesCache
if
(
cache
!=
null
&&
currentTimeStamp
<=
cache
.
cacheTimeStamp
+
APP_LINKS_CACHE_INTERVAL
)
{
return
cache
.
cachedBrowserNames
}
val
browserNames
=
findExcludedPackages
(
unguessableWebUrl
)
browserNamesCache
=
AppLinkBrowserNamesCache
(
currentTimeStamp
,
browserNames
)
return
browserNames
}
/**
* Parse a URL and check if it can be handled by an app elsewhere on the Android device.
* If that app is not available, then a market place intent is also provided.
...
...
@@ -122,9 +96,11 @@ class AppLinksUseCases(
val
redirectData
=
createBrowsableIntents
(
url
)
val
isAppIntentHttpOrHttps
=
redirectData
.
appIntent
?.
data
?.
isHttpOrHttps
?:
false
val
isEngineSupportedScheme
=
ENGINE_SUPPORTED_SCHEMES
.
contains
(
Uri
.
parse
(
url
).
scheme
)
val
appIntent
=
when
{
redirectData
.
resolveInfo
==
null
->
null
redirectData
.
resolveInfo
==
null
&&
isEngineSupportedScheme
->
null
redirectData
.
resolveInfo
==
null
&&
redirectData
.
marketplaceIntent
!=
null
->
null
includeHttpAppLinks
&&
(
ignoreDefaultBrowser
||
(
redirectData
.
appIntent
!=
null
&&
isDefaultBrowser
(
redirectData
.
appIntent
)))
->
null
includeHttpAppLinks
&&
isAppIntentHttpOrHttps
->
redirectData
.
appIntent
...
...
@@ -147,22 +123,8 @@ class AppLinksUseCases(
private
fun
isDefaultBrowser
(
intent
:
Intent
)
=
findDefaultActivity
(
intent
)
?.
activityInfo
?.
packageName
==
context
.
packageName
private
fun
getNonBrowserActivities
(
intent
:
Intent
):
List
<
ResolveInfo
>
{
return
findActivities
(
intent
)
.
map
{
it
.
activityInfo
.
packageName
to
it
}
.
filter
{
intent
.
`package`
==
it
.
first
||
!
getBrowserPackageNames
().
contains
(
it
.
first
)
}
.
map
{
it
.
second
}
}
private
fun
createBrowsableIntents
(
url
:
String
):
RedirectData
{
val
intent
=
safeParseUri
(
url
,
0
)
if
(
intent
!=
null
&&
intent
.
action
==
Intent
.
ACTION_VIEW
)
{
intent
.
addCategory
(
Intent
.
CATEGORY_BROWSABLE
)
intent
.
component
=
null
intent
.
selector
=
null
intent
.
flags
=
Intent
.
FLAG_ACTIVITY_NEW_TASK
}
val
intent
=
safeParseUri
(
url
,
Intent
.
URI_INTENT_SCHEME
)
val
fallbackIntent
=
intent
?.
getStringExtra
(
EXTRA_BROWSER_FALLBACK_URL
)
?.
let
{
Intent
.
parseUri
(
it
,
0
)
}
...
...
@@ -185,8 +147,20 @@ class AppLinksUseCases(
else
->
intent
}
appIntent
?.
let
{
it
.
addCategory
(
Intent
.
CATEGORY_BROWSABLE
)
it
.
component
=
null
it
.
flags
=
Intent
.
FLAG_ACTIVITY_NEW_TASK
it
.
selector
?.
addCategory
(
Intent
.
CATEGORY_BROWSABLE
)
it
.
selector
?.
component
=
null
it
.
putExtra
(
EXTRA_APPLICATION_ID
,
context
.
packageName
)
}
val
resolveInfoList
=
appIntent
?.
let
{
getNonBrowserActivities
(
it
)
findActivities
(
appIntent
).
filter
{
it
.
filter
!=
null
&&
!(
it
.
filter
.
countDataPaths
()
==
0
&&
it
.
filter
.
countDataAuthorities
()
==
0
)
}
}
val
resolveInfo
=
resolveInfoList
?.
firstOrNull
()
...
...
@@ -228,6 +202,9 @@ class AppLinksUseCases(
}
catch
(
e
:
ActivityNotFoundException
)
{
failedToLaunchAction
()
Logger
.
error
(
"failed to start third party app activity"
,
e
)
}
catch
(
e
:
SecurityException
)
{
failedToLaunchAction
()
Logger
.
error
(
"failed to start third party app activity"
,
e
)
}
}
}
...
...
@@ -268,23 +245,15 @@ class AppLinksUseCases(
var
cachedAppLinkRedirect
:
AppLinkRedirect
)
@VisibleForTesting
(
otherwise
=
VisibleForTesting
.
PRIVATE
)
internal
data class
AppLinkBrowserNamesCache
(
var
cacheTimeStamp
:
Long
,
var
cachedBrowserNames
:
Set
<
String
>
)
companion
object
{
@VisibleForTesting
(
otherwise
=
VisibleForTesting
.
PRIVATE
)
internal
var
redirectCache
:
AppLinkRedirectCache
?
=
null
@VisibleForTesting
(
otherwise
=
VisibleForTesting
.
PRIVATE
)
internal
var
browserNamesCache
:
AppLinkBrowserNamesCache
?
=
null
@VisibleForTesting
(
otherwise
=
VisibleForTesting
.
PRIVATE
)
// list of scheme from https://searchfox.org/mozilla-central/source/netwerk/build/components.conf
internal
val
ENGINE_SUPPORTED_SCHEMES
:
Set
<
String
>
=
setOf
(
"about"
,
"data"
,
"file"
,
"ftp"
,
"http"
,
"https"
,
"moz-extension"
,
"moz-safe-about"
,
"resource"
,
"view-source"
,
"ws"
,
"wss"
)
internal
val
ALWAYS_DENY_SCHEMES
:
Set
<
String
>
=
setOf
(
"file"
,
"javascript"
,
"data"
,
"about"
)
}
}
components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksUseCasesTest.kt
View file @
45ef5233
...
...
@@ -7,6 +7,7 @@ package mozilla.components.feature.app.links
import
android.content.ActivityNotFoundException
import
android.content.Context
import
android.content.Intent
import
android.content.IntentFilter
import
android.content.pm.ActivityInfo
import
android.content.pm.ResolveInfo
import
android.net.Uri
...
...
@@ -17,7 +18,6 @@ import kotlinx.coroutines.test.TestCoroutineDispatcher
import
kotlinx.coroutines.test.TestCoroutineScope
import
mozilla.components.support.test.mock
import
mozilla.components.support.test.robolectric.testContext
import
mozilla.components.support.test.whenever
import
org.junit.Assert.assertEquals
import
org.junit.Assert.assertFalse
import
org.junit.Assert.assertNotNull
...
...
@@ -53,11 +53,12 @@ class AppLinksUseCasesTest {
private
val
layerUrl
=
"https://exmaple.com"
private
val
layerPackage
=
"com.example.app"
private
val
layerActivity
=
"com.example2.app.intentActivity"
private
val
mailUrl
=
"mailto:example@example.com"
private
val
mailPackage
=
"com.mail.app"
@Before
fun
setup
()
{
AppLinksUseCases
.
redirectCache
=
null
AppLinksUseCases
.
browserNamesCache
=
null
}
private
fun
createContext
(
vararg
urlToPackages
:
Triple
<
String
,
String
,
String
>,
default
:
Boolean
=
false
):
Context
{
...
...
@@ -77,6 +78,10 @@ class AppLinksUseCasesTest {
val
resolveInfo
=
ResolveInfo
().
apply
{
labelRes
=
android
.
R
.
string
.
ok
activityInfo
=
info
if
(
pkgName
!=
browserPackage
&&
pkgName
!=
mailPackage
)
{
filter
=
IntentFilter
()
filter
.
addDataPath
(
"test"
,
0
)
}
}
packageManager
.
addResolveInfoForIntent
(
intent
,
resolveInfo
)
packageManager
.
addDrawableResolution
(
pkgName
,
android
.
R
.
drawable
.
btn_default
,
mock
())
...
...
@@ -96,7 +101,7 @@ class AppLinksUseCasesTest {
val
context
=
createContext
()
val
subject
=
AppLinksUseCases
(
context
,
{
true
})
val
redirect
=
subject
.
interceptedAppLinkRedirect
(
"test://test#Intent;"
)
assert
False
(
redirect
.
isRedirect
())
assert
(
redirect
.
isRedirect
())
}
@Test
...
...
@@ -191,9 +196,9 @@ class AppLinksUseCasesTest {
}
@Test
fun
`A
URL
that
matches
only
excluded
packages
is
not
an
app
link`
()
{
fun
`A
URL
that
matches
only
general
packages
is
not
an
app
link`
()
{
val
context
=
createContext
(
Triple
(
appUrl
,
browserPackage
,
""
),
Triple
(
browserUrl
,
browserPackage
,
""
))
val
subject
=
AppLinksUseCases
(
context
,
{
true
}
,
unguessableWebUrl
=
browserUrl
)
val
subject
=
AppLinksUseCases
(
context
,
{
true
})
val
redirect
=
subject
.
interceptedAppLinkRedirect
(
appUrl
)
assertFalse
(
redirect
.
isRedirect
())
...
...
@@ -203,9 +208,9 @@ class AppLinksUseCasesTest {
}
@Test
fun
`A
URL
that
also
matches
excluded
packages
is
an
app
link`
()
{
fun
`A
URL
that
also
matches
both
specialized
and
general
packages
is
an
app
link`
()
{
val
context
=
createContext
(
Triple
(
appUrl
,
appPackage
,
""
),
Triple
(
appUrl
,
browserPackage
,
""
),
Triple
(
browserUrl
,
browserPackage
,
""
))
val
subject
=
AppLinksUseCases
(
context
,
{
true
}
,
unguessableWebUrl
=
browserUrl
)
val
subject
=
AppLinksUseCases
(
context
,
{
true
})
val
redirect
=
subject
.
interceptedAppLinkRedirect
(
appUrl
)
assertTrue
(
redirect
.
isRedirect
())
...
...
@@ -216,22 +221,21 @@ class AppLinksUseCasesTest {
}
@Test
fun
`A
URL
that
only
matches
default
activity
is
not
an
app
link`
()
{
val
context
=
createContext
(
Triple
(
app
Url
,
app
Package
,
""
)
,
default
=
true
)
fun
`A
URL
that
also
matches
general
packages
but
the
scheme
is
not
supported
is
an
app
link`
()
{
val
context
=
createContext
(
Triple
(
mail
Url
,
mail
Package
,
""
))
val
subject
=
AppLinksUseCases
(
context
,
{
true
})
val
menuR
edirect
=
subject
.
a
ppLinkRedirect
(
app
Url
)
assert
False
(
menuRedirect
.
hasExternalApp
())
val
r
edirect
=
subject
.
interceptedA
ppLinkRedirect
(
mail
Url
)
assert
True
(
redirect
.
isRedirect
())
}
@Test
fun
`A
list
of
browser
package
names
can
be
generated
if
not
supplied`
()
{
val
unguessable
=
"https://unguessable-test-url.com"
val
context
=
createContext
(
Triple
(
unguessable
,
browserPackage
,
""
))
val
subject
=
AppLinksUseCases
(
context
,
unguessableWebUrl
=
unguessable
)
fun
`A
URL
that
only
matches
default
activity
is
not
an
app
link`
()
{
val
context
=
createContext
(
Triple
(
appUrl
,
appPackage
,
""
),
default
=
true
)
val
subject
=
AppLinksUseCases
(
context
,
{
true
})
subject
.
appLinkRedirect
(
unguessable
)
assert
Equals
(
subject
.
getBrowserPackageNames
(),
setOf
(
browserPackage
))
val
menuRedirect
=
subject
.
appLinkRedirect
(
appUrl
)
assert
False
(
menuRedirect
.
hasExternalApp
(
))
}
@Test
...
...
@@ -353,34 +357,6 @@ class AppLinksUseCasesTest {
}
}
@Test
fun
`AppLinksUsecases
uses
browser
names
cache`
()
{
val
testDispatcher
=
TestCoroutineDispatcher
()
TestCoroutineScope
(
testDispatcher
).
launch
{
val
context
=
createContext
(
Triple
(
appUrl
,
appPackage
,
""
))
var
subject
=
AppLinksUseCases
(
context
,
{
true
})
whenever
(
subject
.
findExcludedPackages
(
any
())).
thenReturn
(
emptySet
())
var
browserNames
=
subject
.
getBrowserPackageNames
()
assertTrue
(
browserNames
.
isEmpty
())
val
timestamp
=
AppLinksUseCases
.
browserNamesCache
?.
cacheTimeStamp
whenever
(
subject
.
findExcludedPackages
(
any
())).
thenReturn
(
setOf
(
appPackage
))
testDispatcher
.
advanceTimeBy
(
APP_LINKS_CACHE_INTERVAL
/
2
)
subject
=
AppLinksUseCases
(
context
,
{
true
})
browserNames
=
subject
.
getBrowserPackageNames
()
assertTrue
(
browserNames
.
isEmpty
())
assert
(
timestamp
==
AppLinksUseCases
.
browserNamesCache
?.
cacheTimeStamp
)
testDispatcher
.
advanceTimeBy
(
APP_LINKS_CACHE_INTERVAL
/
2
+
1
)
subject
=
AppLinksUseCases
(
context
,
{
true
})
browserNames
=
subject
.
getBrowserPackageNames
()
assertFalse
(
browserNames
.
isEmpty
())
assertFalse
(
browserNames
.
contains
(
appPackage
))
assert
(
timestamp
!=
AppLinksUseCases
.
browserNamesCache
?.
cacheTimeStamp
)
}
}
@Test
fun
`OpenAppLinkRedirect
should
not
try
to
open
files`
()
{
val
context
=
spy
(
createContext
())
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment