parent
b7b601eee1
commit
26c7562e62
@ -0,0 +1,149 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.home.intent
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.provider.Settings
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.navigation.NavController
|
||||
import mozilla.components.concept.engine.EngineSession
|
||||
import mozilla.components.support.base.log.logger.Logger
|
||||
import org.mozilla.fenix.BrowserDirection
|
||||
import org.mozilla.fenix.BuildConfig
|
||||
import org.mozilla.fenix.GlobalDirections
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||
import org.mozilla.fenix.ext.alreadyOnDestination
|
||||
import org.mozilla.fenix.settings.SupportUtils
|
||||
|
||||
/**
|
||||
* Deep links in the form of `fenix://host` open different parts of the app.
|
||||
*/
|
||||
class HomeDeepLinkIntentProcessor(
|
||||
private val activity: HomeActivity,
|
||||
) : HomeIntentProcessor {
|
||||
private val logger = Logger("DeepLinkIntentProcessor")
|
||||
|
||||
override fun process(intent: Intent, navController: NavController, out: Intent): Boolean {
|
||||
val scheme = intent.scheme?.equals(BuildConfig.DEEP_LINK_SCHEME, ignoreCase = true) ?: return false
|
||||
return if (scheme) {
|
||||
intent.data?.let { handleDeepLink(it, navController) }
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
private fun handleDeepLink(deepLink: Uri, navController: NavController) {
|
||||
handleDeepLinkSideEffects(deepLink)
|
||||
|
||||
val globalDirections = when (deepLink.host) {
|
||||
"home", "enable_private_browsing" -> GlobalDirections.Home
|
||||
"urls_bookmarks" -> GlobalDirections.Bookmarks
|
||||
"urls_history" -> GlobalDirections.History
|
||||
"settings" -> GlobalDirections.Settings
|
||||
"turn_on_sync" -> GlobalDirections.Sync
|
||||
"settings_search_engine" -> GlobalDirections.SearchEngine
|
||||
"settings_accessibility" -> GlobalDirections.Accessibility
|
||||
"settings_delete_browsing_data" -> GlobalDirections.DeleteData
|
||||
"settings_addon_manager" -> GlobalDirections.SettingsAddonManager
|
||||
"settings_logins" -> GlobalDirections.SettingsLogins
|
||||
"settings_tracking_protection" -> GlobalDirections.SettingsTrackingProtection
|
||||
// We'd like to highlight views within the fragment
|
||||
// https://github.com/mozilla-mobile/fenix/issues/11856
|
||||
// The current version of UI has these features in more complex screens.
|
||||
"settings_privacy" -> GlobalDirections.Settings
|
||||
"settings_wallpapers" -> GlobalDirections.WallpaperSettings
|
||||
"home_collections" -> GlobalDirections.Home
|
||||
|
||||
else -> return
|
||||
}
|
||||
|
||||
if (!navController.alreadyOnDestination(globalDirections.destinationId)) {
|
||||
navController.navigate(globalDirections.navDirections)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle links that require more than just simple navigation.
|
||||
*/
|
||||
private fun handleDeepLinkSideEffects(deepLink: Uri) {
|
||||
when (deepLink.host) {
|
||||
"enable_private_browsing" -> {
|
||||
activity.browsingModeManager.mode = BrowsingMode.Private
|
||||
}
|
||||
"make_default_browser" -> {
|
||||
if (SDK_INT >= Build.VERSION_CODES.N) {
|
||||
val settingsIntent = Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS)
|
||||
settingsIntent.putExtra(SETTINGS_SELECT_OPTION_KEY, DEFAULT_BROWSER_APP_OPTION)
|
||||
settingsIntent.putExtra(
|
||||
SETTINGS_SHOW_FRAGMENT_ARGS,
|
||||
bundleOf(SETTINGS_SELECT_OPTION_KEY to DEFAULT_BROWSER_APP_OPTION)
|
||||
)
|
||||
activity.startActivity(
|
||||
settingsIntent
|
||||
)
|
||||
} else {
|
||||
activity.openToBrowserAndLoad(
|
||||
searchTermOrURL = SupportUtils.getSumoURLForTopic(
|
||||
activity,
|
||||
SupportUtils.SumoTopic.SET_AS_DEFAULT_BROWSER
|
||||
),
|
||||
newTab = true,
|
||||
from = BrowserDirection.FromGlobal,
|
||||
flags = EngineSession.LoadUrlFlags.external()
|
||||
)
|
||||
}
|
||||
}
|
||||
"open" -> {
|
||||
val url = deepLink.getQueryParameter("url")
|
||||
if (url == null || !url.startsWith("https://")) {
|
||||
logger.info("Not opening deep link: $url")
|
||||
return
|
||||
}
|
||||
|
||||
activity.openToBrowserAndLoad(
|
||||
url,
|
||||
newTab = true,
|
||||
from = BrowserDirection.FromGlobal,
|
||||
flags = EngineSession.LoadUrlFlags.external()
|
||||
)
|
||||
}
|
||||
"settings_notifications" -> {
|
||||
val intent = notificationSettings(activity)
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun notificationSettings(context: Context, channel: String? = null) =
|
||||
Intent().apply {
|
||||
when {
|
||||
SDK_INT >= Build.VERSION_CODES.O -> {
|
||||
action = channel?.let {
|
||||
putExtra(Settings.EXTRA_CHANNEL_ID, it)
|
||||
Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS
|
||||
} ?: Settings.ACTION_APP_NOTIFICATION_SETTINGS
|
||||
putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
|
||||
}
|
||||
else -> {
|
||||
action = "android.settings.APP_NOTIFICATION_SETTINGS"
|
||||
putExtra("app_package", context.packageName)
|
||||
putExtra("app_uid", context.applicationInfo.uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SETTINGS_SELECT_OPTION_KEY = ":settings:fragment_args_key"
|
||||
private const val SETTINGS_SHOW_FRAGMENT_ARGS = ":settings:show_fragment_args"
|
||||
private const val DEFAULT_BROWSER_APP_OPTION = "default_browser"
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.intent
|
||||
|
||||
import android.content.Intent
|
||||
import mozilla.components.feature.intent.processing.IntentProcessor
|
||||
import org.mozilla.fenix.BuildConfig
|
||||
|
||||
/**
|
||||
* Process public deep links that are coming from external apps.
|
||||
*/
|
||||
class ExternalDeepLinkIntentProcessor : IntentProcessor {
|
||||
/**
|
||||
* Processes the given [Intent] verifying if it is an external deeplink.
|
||||
*
|
||||
* Adding extra flags if it's a deeplink for opening the app as a separate task from the source app of the intent.
|
||||
*/
|
||||
override fun process(intent: Intent): Boolean {
|
||||
val isDeeplink = intent.scheme?.equals(BuildConfig.DEEP_LINK_SCHEME, ignoreCase = true) ?: false
|
||||
if (isDeeplink) {
|
||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
}
|
||||
return isDeeplink
|
||||
}
|
||||
}
|
@ -0,0 +1,286 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.home.intent
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build.VERSION_CODES.M
|
||||
import android.os.Build.VERSION_CODES.N
|
||||
import android.os.Build.VERSION_CODES.P
|
||||
import androidx.core.net.toUri
|
||||
import androidx.navigation.NavController
|
||||
import io.mockk.Called
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import mozilla.appservices.places.BookmarkRoot
|
||||
import mozilla.components.concept.engine.EngineSession
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.BrowserDirection
|
||||
import org.mozilla.fenix.BuildConfig.DEEP_LINK_SCHEME
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.NavGraphDirections
|
||||
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
import org.mozilla.fenix.settings.SupportUtils
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class HomeDeepLinkIntentProcessorTest {
|
||||
private lateinit var activity: HomeActivity
|
||||
private lateinit var navController: NavController
|
||||
private lateinit var out: Intent
|
||||
private lateinit var processorHome: HomeDeepLinkIntentProcessor
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
activity = mockk(relaxed = true)
|
||||
navController = mockk(relaxed = true)
|
||||
out = mockk()
|
||||
processorHome = HomeDeepLinkIntentProcessor(activity)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `do not process blank intents`() {
|
||||
assertFalse(processorHome.process(Intent(), navController, out))
|
||||
|
||||
verify { activity wasNot Called }
|
||||
verify { navController wasNot Called }
|
||||
verify { out wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `return true if scheme is fenix`() {
|
||||
assertTrue(processorHome.process(testIntent("test"), navController, out))
|
||||
|
||||
verify { activity wasNot Called }
|
||||
verify { navController wasNot Called }
|
||||
verify { out wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `return true if scheme is a fenix variant`() {
|
||||
assertTrue(processorHome.process(testIntent("fenix-beta://test"), navController, out))
|
||||
|
||||
verify { activity wasNot Called }
|
||||
verify { navController wasNot Called }
|
||||
verify { out wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process home deep link`() {
|
||||
assertTrue(processorHome.process(testIntent("home"), navController, out))
|
||||
|
||||
verify { activity wasNot Called }
|
||||
verify { navController.navigate(NavGraphDirections.actionGlobalHome()) }
|
||||
verify { out wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process urls_bookmarks deep link`() {
|
||||
assertTrue(processorHome.process(testIntent("urls_bookmarks"), navController, out))
|
||||
|
||||
verify { navController.navigate(NavGraphDirections.actionGlobalBookmarkFragment(BookmarkRoot.Root.id)) }
|
||||
verify { out wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process urls_history deep link`() {
|
||||
assertTrue(processorHome.process(testIntent("urls_history"), navController, out))
|
||||
|
||||
verify { navController.navigate(NavGraphDirections.actionGlobalHistoryFragment()) }
|
||||
verify { out wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process home_collections deep link`() {
|
||||
assertTrue(processorHome.process(testIntent("home_collections"), navController, out))
|
||||
|
||||
verify { navController.navigate(NavGraphDirections.actionGlobalHome()) }
|
||||
verify { out wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process settings deep link`() {
|
||||
assertTrue(processorHome.process(testIntent("settings"), navController, out))
|
||||
|
||||
verify { activity wasNot Called }
|
||||
verify { navController.navigate(NavGraphDirections.actionGlobalSettingsFragment()) }
|
||||
verify { out wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process turn_on_sync deep link`() {
|
||||
assertTrue(processorHome.process(testIntent("turn_on_sync"), navController, out))
|
||||
|
||||
verify { activity wasNot Called }
|
||||
verify { navController.navigate(NavGraphDirections.actionGlobalTurnOnSync()) }
|
||||
verify { out wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process settings_search_engine deep link`() {
|
||||
assertTrue(processorHome.process(testIntent("settings_search_engine"), navController, out))
|
||||
|
||||
verify { activity wasNot Called }
|
||||
verify { navController.navigate(NavGraphDirections.actionGlobalSearchEngineFragment()) }
|
||||
verify { out wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process settings_accessibility deep link`() {
|
||||
assertTrue(processorHome.process(testIntent("settings_accessibility"), navController, out))
|
||||
|
||||
verify { activity wasNot Called }
|
||||
verify { navController.navigate(NavGraphDirections.actionGlobalAccessibilityFragment()) }
|
||||
verify { out wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process settings_delete_browsing_data deep link`() {
|
||||
assertTrue(processorHome.process(testIntent("settings_delete_browsing_data"), navController, out))
|
||||
|
||||
verify { activity wasNot Called }
|
||||
verify { navController.navigate(NavGraphDirections.actionGlobalDeleteBrowsingDataFragment()) }
|
||||
verify { out wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process settings_addon_manager deep link`() {
|
||||
assertTrue(processorHome.process(testIntent("settings_addon_manager"), navController, out))
|
||||
|
||||
verify { navController.navigate(NavGraphDirections.actionGlobalAddonsManagementFragment()) }
|
||||
verify { out wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process settings_logins deep link`() {
|
||||
assertTrue(processorHome.process(testIntent("settings_logins"), navController, out))
|
||||
|
||||
verify { navController.navigate(NavGraphDirections.actionGlobalSavedLoginsAuthFragment()) }
|
||||
verify { out wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process settings_tracking_protection deep link`() {
|
||||
assertTrue(processorHome.process(testIntent("settings_tracking_protection"), navController, out))
|
||||
|
||||
verify { navController.navigate(NavGraphDirections.actionGlobalTrackingProtectionFragment()) }
|
||||
verify { out wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process settings_privacy deep link`() {
|
||||
assertTrue(processorHome.process(testIntent("settings_privacy"), navController, out))
|
||||
|
||||
verify { navController.navigate(NavGraphDirections.actionGlobalSettingsFragment()) }
|
||||
verify { out wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process enable_private_browsing deep link`() {
|
||||
assertTrue(processorHome.process(testIntent("enable_private_browsing"), navController, out))
|
||||
|
||||
verify { activity.browsingModeManager.mode = BrowsingMode.Private }
|
||||
verify { navController.navigate(NavGraphDirections.actionGlobalHome()) }
|
||||
verify { out wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process open deep link`() {
|
||||
assertTrue(processorHome.process(testIntent("open"), navController, out))
|
||||
|
||||
verify { activity wasNot Called }
|
||||
verify { navController wasNot Called }
|
||||
verify { out wasNot Called }
|
||||
|
||||
assertTrue(processorHome.process(testIntent("open?url=test"), navController, out))
|
||||
|
||||
verify { activity wasNot Called }
|
||||
verify { navController wasNot Called }
|
||||
verify { out wasNot Called }
|
||||
|
||||
assertTrue(processorHome.process(testIntent("open?url=https%3A%2F%2Fwww.example.org%2F"), navController, out))
|
||||
|
||||
verify {
|
||||
activity.openToBrowserAndLoad(
|
||||
"https://www.example.org/",
|
||||
newTab = true,
|
||||
from = BrowserDirection.FromGlobal,
|
||||
flags = EngineSession.LoadUrlFlags.external()
|
||||
)
|
||||
}
|
||||
verify { navController wasNot Called }
|
||||
verify { out wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process invalid open deep link`() {
|
||||
val invalidProcessor = HomeDeepLinkIntentProcessor(activity)
|
||||
|
||||
assertTrue(invalidProcessor.process(testIntent("open"), navController, out))
|
||||
|
||||
verify { activity wasNot Called }
|
||||
verify { navController wasNot Called }
|
||||
verify { out wasNot Called }
|
||||
|
||||
assertTrue(invalidProcessor.process(testIntent("open?url=open?url=https%3A%2F%2Fwww.example.org%2F"), navController, out))
|
||||
|
||||
verify { activity wasNot Called }
|
||||
verify { navController wasNot Called }
|
||||
verify { out wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(minSdk = N, maxSdk = P)
|
||||
fun `process make_default_browser deep link for above API 23`() {
|
||||
assertTrue(processorHome.process(testIntent("make_default_browser"), navController, out))
|
||||
|
||||
verify { activity.startActivity(any()) }
|
||||
verify { navController wasNot Called }
|
||||
verify { out wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(maxSdk = M)
|
||||
fun `process make_default_browser deep link for API 23 and below`() {
|
||||
assertTrue(processorHome.process(testIntent("make_default_browser"), navController, out))
|
||||
|
||||
verify {
|
||||
activity.openToBrowserAndLoad(
|
||||
searchTermOrURL = SupportUtils.getSumoURLForTopic(
|
||||
activity,
|
||||
SupportUtils.SumoTopic.SET_AS_DEFAULT_BROWSER
|
||||
),
|
||||
newTab = true,
|
||||
from = BrowserDirection.FromGlobal,
|
||||
flags = EngineSession.LoadUrlFlags.external()
|
||||
)
|
||||
}
|
||||
verify { navController wasNot Called }
|
||||
verify { out wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process settings_notifications deep link`() {
|
||||
assertTrue(processorHome.process(testIntent("settings_notifications"), navController, out))
|
||||
|
||||
verify { navController wasNot Called }
|
||||
verify { out wasNot Called }
|
||||
verify { activity.startActivity(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process settings_wallpapers deep link`() {
|
||||
assertTrue(processorHome.process(testIntent("settings_wallpapers"), navController, out))
|
||||
|
||||
verify { navController.navigate(NavGraphDirections.actionGlobalWallpaperSettingsFragment()) }
|
||||
verify { out wasNot Called }
|
||||
}
|
||||
|
||||
private fun testIntent(uri: String) = Intent("", "$DEEP_LINK_SCHEME://$uri".toUri())
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package org.mozilla.fenix.intent
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import junit.framework.TestCase
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.BuildConfig
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class ExternalDeepLinkIntentProcessorTest : TestCase() {
|
||||
|
||||
@Test
|
||||
fun `GIVEN a deeplink intent WHEN processing the intent THEN add the extra flags`() {
|
||||
val processor = ExternalDeepLinkIntentProcessor()
|
||||
val uri = Uri.parse(BuildConfig.DEEP_LINK_SCHEME + "://settings_wallpapers")
|
||||
val intent = Intent("", uri)
|
||||
|
||||
val result = processor.process(intent)
|
||||
|
||||
assertTrue(result)
|
||||
assertTrue((intent.flags and (Intent.FLAG_ACTIVITY_NEW_TASK) != 0))
|
||||
assertTrue((intent.flags and (Intent.FLAG_ACTIVITY_CLEAR_TOP) != 0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a non-deeplink intent WHEN processing the intent THEN do not add the extra flags`() {
|
||||
val processor = ExternalDeepLinkIntentProcessor()
|
||||
val intent = Intent("")
|
||||
|
||||
val result = processor.process(intent)
|
||||
|
||||
assertFalse(result)
|
||||
assertFalse((intent.flags and (Intent.FLAG_ACTIVITY_NEW_TASK) != 0))
|
||||
assertFalse((intent.flags and (Intent.FLAG_ACTIVITY_CLEAR_TOP) != 0))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue