From cbb8f808c1daf730e9650ac85c134840ba7a806c Mon Sep 17 00:00:00 2001 From: Arturo Mejia Date: Thu, 26 Aug 2021 20:02:36 -0400 Subject: [PATCH] For #18629: add support for SmartBlock exceptions --- .../org/mozilla/fenix/BrowserDirection.kt | 1 + .../java/org/mozilla/fenix/HomeActivity.kt | 3 + .../mozilla/fenix/settings/SupportUtils.kt | 3 +- .../trackingprotection/TrackerBuckets.kt | 13 ++-- .../TrackingProtectionPanelDialogFragment.kt | 13 ++++ .../TrackingProtectionPanelInteractor.kt | 5 ++ .../TrackingProtectionPanelView.kt | 39 +++++++++- .../component_tracking_protection_panel.xml | 72 +++++++++++++------ app/src/main/res/values/strings.xml | 4 ++ .../trackingprotection/TrackerBucketsTest.kt | 56 +++++++-------- .../TrackingProtectionPanelInteractorTest.kt | 10 +++ 11 files changed, 160 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt b/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt index bd50ba9fd1cd..23cafc54b3ba 100644 --- a/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt +++ b/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt @@ -23,6 +23,7 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) { FromTrackingProtectionExceptions(R.id.trackingProtectionExceptionsFragment), FromAbout(R.id.aboutFragment), FromTrackingProtection(R.id.trackingProtectionFragment), + FromTrackingProtectionDialog(R.id.trackingProtectionPanelDialogFragment), FromSavedLoginsFragment(R.id.savedLoginsFragment), FromAddNewDeviceFragment(R.id.addNewDeviceFragment), FromAddSearchEngineFragment(R.id.addSearchEngineFragment), diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index 2eafea132492..add0c8d3f030 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -116,6 +116,7 @@ import org.mozilla.fenix.tabstray.TabsTrayFragment import org.mozilla.fenix.tabstray.TabsTrayFragmentDirections import org.mozilla.fenix.theme.DefaultThemeManager import org.mozilla.fenix.theme.ThemeManager +import org.mozilla.fenix.trackingprotection.TrackingProtectionPanelDialogFragmentDirections import org.mozilla.fenix.utils.BrowsersCache import org.mozilla.fenix.utils.Settings import java.lang.ref.WeakReference @@ -746,6 +747,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { AboutFragmentDirections.actionGlobalBrowser(customTabSessionId) BrowserDirection.FromTrackingProtection -> TrackingProtectionFragmentDirections.actionGlobalBrowser(customTabSessionId) + BrowserDirection.FromTrackingProtectionDialog -> + TrackingProtectionPanelDialogFragmentDirections.actionGlobalBrowser(customTabSessionId) BrowserDirection.FromSavedLoginsFragment -> SavedLoginsAuthFragmentDirections.actionGlobalBrowser(customTabSessionId) BrowserDirection.FromAddNewDeviceFragment -> diff --git a/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt b/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt index b0ca33f43f4e..58c989352952 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt @@ -50,7 +50,8 @@ object SupportUtils { SEARCH_SUGGESTION("how-search-firefox-preview"), CUSTOM_SEARCH_ENGINES("custom-search-engines"), SYNC_SETUP("how-set-firefox-sync-firefox-android"), - QR_CAMERA_ACCESS("qr-camera-access") + QR_CAMERA_ACCESS("qr-camera-access"), + SMARTBLOCK("smartblock-enhanced-tracking-protection") } enum class MozillaPage(internal val path: String) { diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackerBuckets.kt b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackerBuckets.kt index aaf11831ac6e..cccd6cd7e21a 100644 --- a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackerBuckets.kt +++ b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackerBuckets.kt @@ -7,7 +7,6 @@ package org.mozilla.fenix.trackingprotection import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory import mozilla.components.concept.engine.content.blocking.Tracker import mozilla.components.concept.engine.content.blocking.TrackerLog -import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.CROSS_SITE_TRACKING_COOKIES import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.CRYPTOMINERS import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.FINGERPRINTERS @@ -15,7 +14,7 @@ import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.SOCIAL_ME import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.TRACKING_CONTENT import java.util.EnumMap -typealias BucketMap = Map> +typealias BucketMap = Map> /** * Sorts [Tracker]s into different buckets and exposes them as a map. @@ -85,14 +84,14 @@ class TrackerBuckets { * Create an empty mutable map of [TrackingProtectionCategory] to hostnames. */ private fun createMap() = - EnumMap>(TrackingProtectionCategory::class.java) + EnumMap>(TrackingProtectionCategory::class.java) /** * Add the hostname of the [TrackerLog.url] into the map for the given category * from Android Components. The category is transformed into a corresponding Fenix bucket, * and the item is discarded if the category doesn't have a match. */ - private fun MutableMap>.addTrackerHost( + private fun MutableMap>.addTrackerHost( category: TrackingCategory, tracker: TrackerLog ) { @@ -107,13 +106,13 @@ class TrackerBuckets { } /** - * Add the hostname of the [TrackerLog.url] into the map for the given [TrackingProtectionCategory]. + * Add the hostname of the [TrackerLog] into the map for the given [TrackingProtectionCategory]. */ - private fun MutableMap>.addTrackerHost( + private fun MutableMap>.addTrackerHost( key: TrackingProtectionCategory, tracker: TrackerLog ) { - getOrPut(key) { mutableListOf() }.add(tracker.url.tryGetHostFromUrl()) + getOrPut(key) { mutableListOf() }.add(tracker) } } } diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragment.kt index ec2c53afc0ae..9a6476323dc8 100644 --- a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragment.kt @@ -37,6 +37,7 @@ import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.log.logger.Logger import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged +import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.components.StoreProvider @@ -45,6 +46,7 @@ import org.mozilla.fenix.databinding.FragmentTrackingProtectionBinding import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.requireComponents +import org.mozilla.fenix.settings.SupportUtils @ExperimentalCoroutinesApi @Suppress("TooManyFunctions") @@ -102,6 +104,7 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), UserInt store = trackingProtectionStore, navController = { findNavController() }, openTrackingProtectionSettings = ::openTrackingProtectionSettings, + openLearnMoreLink = ::handleLearnMoreClicked, sitePermissions = args.sitePermissions, gravity = args.gravity, getCurrentTab = ::getCurrentTab @@ -149,6 +152,16 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), UserInt ) } + private fun handleLearnMoreClicked() { + (activity as HomeActivity).openToBrowserAndLoad( + searchTermOrURL = SupportUtils.getGenericSumoURLForTopic( + SupportUtils.SumoTopic.SMARTBLOCK + ), + newTab = true, + from = BrowserDirection.FromTrackingProtectionDialog + ) + } + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return if (args.gravity == Gravity.BOTTOM) { object : BottomSheetDialog(requireContext(), this.theme) { diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelInteractor.kt b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelInteractor.kt index 02cbe9728a78..9f48b7172227 100644 --- a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelInteractor.kt @@ -24,6 +24,7 @@ class TrackingProtectionPanelInteractor( private val store: TrackingProtectionStore, private val navController: () -> NavController, private val openTrackingProtectionSettings: () -> Unit, + private val openLearnMoreLink: () -> Unit, internal var sitePermissions: SitePermissions?, private val gravity: Int, private val getCurrentTab: () -> SessionState? @@ -33,6 +34,10 @@ class TrackingProtectionPanelInteractor( store.dispatch(TrackingProtectionAction.EnterDetailsMode(category, categoryBlocked)) } + override fun onLearnMoreClicked() { + openLearnMoreLink() + } + override fun selectTrackingProtectionSettings() { openTrackingProtectionSettings.invoke() } diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelView.kt b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelView.kt index 97943122c592..0c7570be03bf 100644 --- a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelView.kt +++ b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelView.kt @@ -4,21 +4,26 @@ package org.mozilla.fenix.trackingprotection +import android.text.method.LinkMovementMethod import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.annotation.VisibleForTesting import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.text.HtmlCompat import androidx.core.view.AccessibilityDelegateCompat import androidx.core.view.ViewCompat import androidx.core.view.accessibility.AccessibilityNodeInfoCompat import androidx.core.view.isGone import androidx.core.view.isVisible import mozilla.components.browser.state.state.CustomTabSessionState +import mozilla.components.concept.engine.content.blocking.TrackerLog +import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.databinding.ComponentTrackingProtectionPanelBinding +import org.mozilla.fenix.ext.addUnderline import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.CROSS_SITE_TRACKING_COOKIES import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.CRYPTOMINERS @@ -53,6 +58,11 @@ interface TrackingProtectionPanelViewInteractor { * @param categoryBlocked The trackers from this category were blocked */ fun openDetails(category: TrackingProtectionCategory, categoryBlocked: Boolean) + + /** + * Called when the Learn more link for SmartBlock is clicked. + */ + fun onLearnMoreClicked() } /** @@ -126,10 +136,28 @@ class TrackingProtectionPanelView( category: TrackingProtectionCategory, categoryBlocked: Boolean ) { + val containASmartBlockItem = bucketedTrackers.get(category, categoryBlocked).any { it.unBlockedBySmartBlock } binding.normalMode.visibility = View.GONE binding.detailsMode.visibility = View.VISIBLE binding.categoryTitle.setText(category.title) - binding.blockingTextList.text = bucketedTrackers.get(category, categoryBlocked).joinToString("\n") + + binding.smartblockDescription.isVisible = containASmartBlockItem + binding.smartblockLearnMore.isVisible = containASmartBlockItem + + val trackersList = bucketedTrackers.get(category, categoryBlocked).joinToString("
") { + createTrackerItem(it, containASmartBlockItem) + } + + binding.blockingTextList.text = HtmlCompat.fromHtml(trackersList, HtmlCompat.FROM_HTML_MODE_COMPACT) + + // show description for SmartBlock tracking content in details + if (containASmartBlockItem) { + with(binding.smartblockLearnMore) { + movementMethod = LinkMovementMethod.getInstance() + addUnderline() + setOnClickListener { interactor.onLearnMoreClicked() } + } + } binding.categoryDescription.setText(category.description) binding.detailsBlockingHeader.setText( if (categoryBlocked) { @@ -143,6 +171,15 @@ class TrackingProtectionPanelView( binding.detailsBack.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) } + private fun createTrackerItem(tracker: TrackerLog, isUnblockedSection: Boolean): String { + val space = if (isUnblockedSection) "  " else "" + return if (tracker.unBlockedBySmartBlock) { + "*${tracker.url.tryGetHostFromUrl()}" + } else { + "$space${tracker.url.tryGetHostFromUrl()}" + } + } + /** * Will force accessibility focus to last entered details category. * Called when user returns from details_mode. diff --git a/app/src/main/res/layout/component_tracking_protection_panel.xml b/app/src/main/res/layout/component_tracking_protection_panel.xml index 7d9121715775..b49786a1fac6 100644 --- a/app/src/main/res/layout/component_tracking_protection_panel.xml +++ b/app/src/main/res/layout/component_tracking_protection_panel.xml @@ -242,6 +242,34 @@ app:layout_constraintTop_toBottomOf="@id/category_title" tools:text="@tools:sample/lorem" /> + + + + - - + app:layout_constraintTop_toBottomOf="@id/smartblock_learn_more" /> - + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/line_divider_details"> + + - + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 189a812b7043..a2c6a1757b3e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1439,6 +1439,10 @@ Redirect Trackers Clears cookies set by redirects to known tracking websites. + + Some trackers marked below have been partially unblocked on this page because you interacted with them *. + + Learn more Support diff --git a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackerBucketsTest.kt b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackerBucketsTest.kt index a834f355263a..4c4fad6296a9 100644 --- a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackerBucketsTest.kt +++ b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackerBucketsTest.kt @@ -27,21 +27,22 @@ class TrackerBucketsTest { @Test fun `getter accesses corresponding bucket`() { val buckets = TrackerBuckets() + val google = TrackerLog("https://google.com", listOf(), listOf(FINGERPRINTING)) + val facebook = TrackerLog("http://facebook.com", listOf(MOZILLA_SOCIAL)) + buckets.updateIfNeeded( listOf( - TrackerLog( - "http://facebook.com", - listOf(MOZILLA_SOCIAL) - ), - TrackerLog("https://google.com", listOf(), listOf(FINGERPRINTING)), + google, + facebook, TrackerLog("https://mozilla.com") ) ) - assertEquals(listOf("google.com"), buckets.buckets.blockedBucketMap[FINGERPRINTERS]) + assertEquals(google, buckets.buckets.blockedBucketMap[FINGERPRINTERS]!!.first()) assertEquals( - listOf("facebook.com"), + facebook, buckets.buckets.loadedBucketMap[FenixTrackingProtectionCategory.SOCIAL_MEDIA_TRACKERS] + !!.first() ) assertTrue(buckets.buckets.blockedBucketMap[CRYPTOMINERS].isNullOrEmpty()) assertTrue(buckets.buckets.loadedBucketMap[CRYPTOMINERS].isNullOrEmpty()) @@ -50,27 +51,27 @@ class TrackerBucketsTest { @Test fun `sorts trackers into bucket`() { val buckets = TrackerBuckets() + val google = TrackerLog("https://google.com", listOf(), listOf(FINGERPRINTING)) + val facebook = TrackerLog("http://facebook.com", listOf(MOZILLA_SOCIAL)) + val mozilla = TrackerLog("https://mozilla.com") buckets.updateIfNeeded( listOf( - TrackerLog( - "http://facebook.com", - listOf(MOZILLA_SOCIAL) - ), - TrackerLog("https://google.com", listOf(), listOf(FINGERPRINTING)), - TrackerLog("https://mozilla.com") + facebook, + google, + mozilla ) ) assertEquals( mapOf( - FenixTrackingProtectionCategory.SOCIAL_MEDIA_TRACKERS to listOf("facebook.com") + FenixTrackingProtectionCategory.SOCIAL_MEDIA_TRACKERS to listOf(facebook) ), buckets.buckets.loadedBucketMap ) assertEquals( mapOf( - FINGERPRINTERS to listOf("google.com") + FINGERPRINTERS to listOf(google) ), buckets.buckets.blockedBucketMap ) @@ -86,24 +87,21 @@ class TrackerBucketsTest { SCRIPTS_AND_SUB_RESOURCES ) - buckets.updateIfNeeded( - listOf( - TrackerLog( - url = "http://facebook.com", - cookiesHasBeenBlocked = true, - blockedCategories = acCategories, - loadedCategories = acCategories - ) - ) + val trackerLog = TrackerLog( + url = "http://facebook.com", + cookiesHasBeenBlocked = true, + blockedCategories = acCategories, + loadedCategories = acCategories ) + buckets.updateIfNeeded(listOf(trackerLog)) val expectedBlockedMap = mapOf( - FenixTrackingProtectionCategory.SOCIAL_MEDIA_TRACKERS to listOf("facebook.com"), - FenixTrackingProtectionCategory.TRACKING_CONTENT to listOf("facebook.com"), - FenixTrackingProtectionCategory.FINGERPRINTERS to listOf("facebook.com"), - FenixTrackingProtectionCategory.CRYPTOMINERS to listOf("facebook.com"), - FenixTrackingProtectionCategory.CROSS_SITE_TRACKING_COOKIES to listOf("facebook.com") + FenixTrackingProtectionCategory.SOCIAL_MEDIA_TRACKERS to listOf(trackerLog), + FenixTrackingProtectionCategory.TRACKING_CONTENT to listOf(trackerLog), + FenixTrackingProtectionCategory.FINGERPRINTERS to listOf(trackerLog), + FenixTrackingProtectionCategory.CRYPTOMINERS to listOf(trackerLog), + FenixTrackingProtectionCategory.CROSS_SITE_TRACKING_COOKIES to listOf(trackerLog) ) val expectedLoadedMap = expectedBlockedMap - FenixTrackingProtectionCategory.CROSS_SITE_TRACKING_COOKIES diff --git a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelInteractorTest.kt index ea821c3f961d..d043fca88097 100644 --- a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelInteractorTest.kt +++ b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelInteractorTest.kt @@ -48,12 +48,14 @@ class TrackingProtectionPanelInteractorTest { private lateinit var tab: TabSessionState + private var learnMoreClicked = false private var openSettings = false private var gravity = 54 @Before fun setup() { MockKAnnotations.init(this) + learnMoreClicked = false context = spyk(testContext) tab = createTab("https://mozilla.org") @@ -64,6 +66,7 @@ class TrackingProtectionPanelInteractorTest { store = store, navController = { navController }, openTrackingProtectionSettings = { openSettings = true }, + openLearnMoreLink = { learnMoreClicked = true }, sitePermissions = sitePermissions, gravity = gravity, getCurrentTab = { tab } @@ -115,6 +118,13 @@ class TrackingProtectionPanelInteractorTest { assertEquals(true, openSettings) } + @Test + fun `WHEN on the learn more link is clicked THEN onLearnMoreClicked should be invoked`() { + interactor.onLearnMoreClicked() + + assertEquals(true, learnMoreClicked) + } + @Test fun `WHEN onBackPressed is called THEN call popBackStack and navigate`() { interactor.onBackPressed()