From a61391ef5809caa4c8147d0719d766b8a76f9235 Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Fri, 13 Sep 2019 21:21:50 -0700 Subject: [PATCH] Separates tracker bucket sorting code --- .../trackingprotection/TrackerBuckets.kt | 81 +++++++ .../TrackingProtectionPanelView.kt | 212 +++++++----------- .../trackingprotection/TrackerBucketsTest.kt | 53 +++++ 3 files changed, 216 insertions(+), 130 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/trackingprotection/TrackerBuckets.kt create mode 100644 app/src/test/java/org/mozilla/fenix/trackingprotection/TrackerBucketsTest.kt diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackerBuckets.kt b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackerBuckets.kt new file mode 100644 index 000000000..9aa12a49a --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackerBuckets.kt @@ -0,0 +1,81 @@ +/* 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.trackingprotection + +import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory.AD +import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory.ANALYTICS +import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory.CRYPTOMINING +import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory.FINGERPRINTING +import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory.SOCIAL +import mozilla.components.concept.engine.content.blocking.Tracker +import org.mozilla.fenix.ext.getHostFromUrl +import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.CRYPTOMINERS +import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.FINGERPRINTERS +import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.SOCIAL_MEDIA_TRACKERS +import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.TRACKING_CONTENT +import java.util.EnumMap + +/** + * Sorts [Tracker]s into different buckets and exposes them as a map. + */ +class TrackerBuckets { + + private var trackers = emptyList() + var buckets = emptyMap>() + private set + + /** + * If [newTrackers] has changed since the last call, + * update [buckets] based on the new trackers list. + */ + fun updateIfNeeded(newTrackers: List) { + if (newTrackers != trackers) { + trackers = newTrackers + buckets = putTrackersInBuckets(newTrackers) + } + } + + /** + * Returns true if there are no trackers. + */ + fun isEmpty() = buckets.isEmpty() + + /** + * Gets the tracker URLs for a given category. + */ + operator fun get(key: TrackingProtectionCategory) = buckets[key].orEmpty() + + companion object { + + private fun putTrackersInBuckets( + list: List + ): Map> { + val map = EnumMap>(TrackingProtectionCategory::class.java) + for (item in list) { + when { + CRYPTOMINING in item.trackingCategories -> { + map[CRYPTOMINERS] = map[CRYPTOMINERS].orEmpty() + + (item.url.getHostFromUrl() ?: item.url) + } + FINGERPRINTING in item.trackingCategories -> { + map[FINGERPRINTERS] = map[FINGERPRINTERS].orEmpty() + + (item.url.getHostFromUrl() ?: item.url) + } + SOCIAL in item.trackingCategories -> { + map[SOCIAL_MEDIA_TRACKERS] = map[SOCIAL_MEDIA_TRACKERS].orEmpty() + + (item.url.getHostFromUrl() ?: item.url) + } + AD in item.trackingCategories || + SOCIAL in item.trackingCategories || + ANALYTICS in item.trackingCategories -> { + map[TRACKING_CONTENT] = map[TRACKING_CONTENT].orEmpty() + + (item.url.getHostFromUrl() ?: item.url) + } + } + } + return map + } + } +} 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 d0c834151..7780c91f6 100644 --- a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelView.kt +++ b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelView.kt @@ -9,19 +9,13 @@ import android.view.View import android.view.ViewGroup import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.net.toUri +import androidx.core.view.isGone import kotlinx.android.extensions.LayoutContainer import kotlinx.android.synthetic.main.component_tracking_protection_panel.* import kotlinx.android.synthetic.main.fragment_quick_settings_dialog_sheet.url import kotlinx.android.synthetic.main.switch_with_description.view.* -import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory.CRYPTOMINING -import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory.FINGERPRINTING -import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory.SOCIAL -import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory.AD -import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory.ANALYTICS -import mozilla.components.concept.engine.content.blocking.Tracker import mozilla.components.support.ktx.android.net.hostWithoutCommonPrefixes import org.mozilla.fenix.R -import org.mozilla.fenix.ext.getHostFromUrl import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.CROSS_SITE_TRACKING_COOKIES import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.CRYPTOMINERS import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.FINGERPRINTERS @@ -63,40 +57,24 @@ interface TrackingProtectionPanelViewInteractor { class TrackingProtectionPanelView( override val containerView: ViewGroup, val interactor: TrackingProtectionPanelInteractor -) : LayoutContainer { +) : LayoutContainer, View.OnClickListener { + val view: ConstraintLayout = LayoutInflater.from(containerView.context) .inflate(R.layout.component_tracking_protection_panel, containerView, true) .findViewById(R.id.panel_wrapper) - private val context get() = view.context + private var mode: TrackingProtectionState.Mode = TrackingProtectionState.Mode.Normal - var mode: TrackingProtectionState.Mode = TrackingProtectionState.Mode.Normal - private set - - var trackers: List = listOf() - private set - - var bucketedTrackers: HashMap> = HashMap() - - var loadedTrackers: List = listOf() - private set - - var bucketedLoadedTrackers: HashMap> = HashMap() + private var bucketedTrackers = TrackerBuckets() + private var bucketedLoadedTrackers = TrackerBuckets() fun update(state: TrackingProtectionState) { if (state.mode != mode) { mode = state.mode } - if (state.listTrackers != trackers) { - trackers = state.listTrackers - bucketedTrackers = getHashMapOfTrackersForCategory(state.listTrackers) - } - - if (state.listTrackersLoaded != loadedTrackers) { - loadedTrackers = state.listTrackersLoaded - bucketedLoadedTrackers = getHashMapOfTrackersForCategory(state.listTrackersLoaded) - } + bucketedTrackers.updateIfNeeded(state.listTrackers) + bucketedLoadedTrackers.updateIfNeeded(state.listTrackersLoaded) when (val mode = state.mode) { is TrackingProtectionState.Mode.Normal -> setUIForNormalMode(state) @@ -110,143 +88,84 @@ class TrackingProtectionPanelView( private fun setUIForNormalMode(state: TrackingProtectionState) { details_mode.visibility = View.GONE normal_mode.visibility = View.VISIBLE - protection_settings.visibility = - if (state.session?.customTabConfig != null) View.GONE else View.VISIBLE + protection_settings.isGone = state.session?.customTabConfig != null - not_blocking_header.visibility = - if (bucketedLoadedTrackers.size == 0) View.GONE else View.VISIBLE + not_blocking_header.isGone = bucketedLoadedTrackers.isEmpty() bindUrl(state.url) bindTrackingProtectionInfo(state.isTrackingProtectionEnabled) protection_settings.setOnClickListener { interactor.selectTrackingProtectionSettings() } - blocking_header.visibility = - if (bucketedTrackers.size == 0) View.GONE else View.VISIBLE + blocking_header.isGone = bucketedTrackers.isEmpty() updateCategoryVisibility() setCategoryClickListeners() } - @Suppress("ComplexMethod") private fun updateCategoryVisibility() { - cross_site_tracking.visibility = bucketedTrackers.getVisibility(CROSS_SITE_TRACKING_COOKIES) - social_media_trackers.visibility = bucketedTrackers.getVisibility(SOCIAL_MEDIA_TRACKERS) - fingerprinters.visibility = bucketedTrackers.getVisibility(FINGERPRINTERS) - tracking_content.visibility = bucketedTrackers.getVisibility(TRACKING_CONTENT) - cryptominers.visibility = bucketedTrackers.getVisibility(CRYPTOMINERS) + cross_site_tracking.isGone = bucketedTrackers[CROSS_SITE_TRACKING_COOKIES].isEmpty() + social_media_trackers.isGone = bucketedTrackers[SOCIAL_MEDIA_TRACKERS].isEmpty() + fingerprinters.isGone = bucketedTrackers[FINGERPRINTERS].isEmpty() + tracking_content.isGone = bucketedTrackers[TRACKING_CONTENT].isEmpty() + cryptominers.isGone = bucketedTrackers[CRYPTOMINERS].isEmpty() - cross_site_tracking_loaded.visibility = - bucketedLoadedTrackers.getVisibility(CROSS_SITE_TRACKING_COOKIES) - social_media_trackers_loaded.visibility = - bucketedLoadedTrackers.getVisibility(SOCIAL_MEDIA_TRACKERS) - fingerprinters_loaded.visibility = bucketedLoadedTrackers.getVisibility(FINGERPRINTERS) - tracking_content_loaded.visibility = bucketedLoadedTrackers.getVisibility(TRACKING_CONTENT) - cryptominers_loaded.visibility = bucketedLoadedTrackers.getVisibility(CRYPTOMINERS) + cross_site_tracking_loaded.isGone = + bucketedLoadedTrackers[CROSS_SITE_TRACKING_COOKIES].isEmpty() + social_media_trackers_loaded.isGone = + bucketedLoadedTrackers[SOCIAL_MEDIA_TRACKERS].isEmpty() + fingerprinters_loaded.isGone = bucketedLoadedTrackers[FINGERPRINTERS].isEmpty() + tracking_content_loaded.isGone = bucketedLoadedTrackers[TRACKING_CONTENT].isEmpty() + cryptominers_loaded.isGone = bucketedLoadedTrackers[CRYPTOMINERS].isEmpty() } - private fun HashMap>.getVisibility( - category: TrackingProtectionCategory - ): Int = if (this[category]?.isNotEmpty() == true) View.VISIBLE else View.GONE - private fun setCategoryClickListeners() { - social_media_trackers.setOnClickListener { - interactor.openDetails(SOCIAL_MEDIA_TRACKERS, categoryBlocked = true) - } - fingerprinters.setOnClickListener { - interactor.openDetails(FINGERPRINTERS, categoryBlocked = true) - } - cross_site_tracking.setOnClickListener { - interactor.openDetails(CROSS_SITE_TRACKING_COOKIES, categoryBlocked = true) - } - tracking_content.setOnClickListener { - interactor.openDetails(TRACKING_CONTENT, categoryBlocked = true) - } - cryptominers.setOnClickListener { - interactor.openDetails(CRYPTOMINERS, categoryBlocked = true) - } - social_media_trackers_loaded.setOnClickListener { - interactor.openDetails(SOCIAL_MEDIA_TRACKERS, categoryBlocked = false) - } - fingerprinters_loaded.setOnClickListener { - interactor.openDetails(FINGERPRINTERS, categoryBlocked = false) - } - cross_site_tracking_loaded.setOnClickListener { - interactor.openDetails(CROSS_SITE_TRACKING_COOKIES, categoryBlocked = false) - } - tracking_content_loaded.setOnClickListener { - interactor.openDetails(TRACKING_CONTENT, categoryBlocked = false) - } - cryptominers_loaded.setOnClickListener { - interactor.openDetails(CRYPTOMINERS, categoryBlocked = false) - } + social_media_trackers.setOnClickListener(this) + fingerprinters.setOnClickListener(this) + cross_site_tracking.setOnClickListener(this) + tracking_content.setOnClickListener(this) + cryptominers.setOnClickListener(this) + social_media_trackers_loaded.setOnClickListener(this) + fingerprinters_loaded.setOnClickListener(this) + cross_site_tracking_loaded.setOnClickListener(this) + tracking_content_loaded.setOnClickListener(this) + cryptominers_loaded.setOnClickListener(this) + } + + override fun onClick(v: View) { + val category = getCategory(v) ?: return + interactor.openDetails(category, categoryBlocked = !isLoaded(v)) } private fun setUIForDetailsMode( category: TrackingProtectionCategory, categoryBlocked: Boolean ) { + val context = view.context + normal_mode.visibility = View.GONE details_mode.visibility = View.VISIBLE category_title.text = context.getString(category.title) - val stringList = bucketedTrackers[category]?.joinToString("\n") - blocking_text_list.text = stringList + blocking_text_list.text = bucketedTrackers[category].joinToString("\n") category_description.text = context.getString(category.description) - details_blocking_header.text = - context.getString( - if (categoryBlocked) R.string.enhanced_tracking_protection_blocked else - R.string.enhanced_tracking_protection_allowed - ) + details_blocking_header.text = context.getString( + if (categoryBlocked) { + R.string.enhanced_tracking_protection_blocked + } else { + R.string.enhanced_tracking_protection_allowed + } + ) details_back.setOnClickListener { interactor.onBackPressed() } } - private fun getHashMapOfTrackersForCategory( - list: List - ): HashMap> { - val hashMap = HashMap>() - items@ for (item in list) { - when { - item.trackingCategories.contains(CRYPTOMINING) -> { - hashMap[CRYPTOMINERS] = - (hashMap[CRYPTOMINERS] - ?: listOf()).plus(item.url.getHostFromUrl() ?: item.url) - continue@items - } - item.trackingCategories.contains(FINGERPRINTING) -> { - hashMap[FINGERPRINTERS] = - (hashMap[FINGERPRINTERS] - ?: listOf()).plus(item.url.getHostFromUrl() ?: item.url) - continue@items - } - item.trackingCategories.contains(SOCIAL) -> { - hashMap[SOCIAL_MEDIA_TRACKERS] = - (hashMap[SOCIAL_MEDIA_TRACKERS] ?: listOf()).plus( - item.url.getHostFromUrl() ?: item.url - ) - continue@items - } - item.trackingCategories.contains(AD) || - item.trackingCategories.contains(SOCIAL) || - item.trackingCategories.contains(ANALYTICS) -> { - hashMap[TRACKING_CONTENT] = - (hashMap[TRACKING_CONTENT] ?: listOf()).plus( - item.url.getHostFromUrl() ?: item.url - ) - continue@items - } - } - } - return hashMap - } - private fun bindUrl(url: String) { this.url.text = url.toUri().hostWithoutCommonPrefixes } private fun bindTrackingProtectionInfo(isTrackingProtectionOn: Boolean) { tracking_protection.switchItemDescription.text = - context.getString(if (isTrackingProtectionOn) R.string.etp_panel_on else R.string.etp_panel_off) + view.context.getString(if (isTrackingProtectionOn) R.string.etp_panel_on else R.string.etp_panel_off) tracking_protection.switch_widget.isChecked = isTrackingProtectionOn tracking_protection.switch_widget.setOnCheckedChangeListener { _, isChecked -> @@ -264,4 +183,37 @@ class TrackingProtectionPanelView( else -> false } } + + companion object { + + /** + * Returns the [TrackingProtectionCategory] corresponding to the view ID. + */ + private fun getCategory(v: View) = when (v.id) { + R.id.social_media_trackers, R.id.social_media_trackers_loaded -> SOCIAL_MEDIA_TRACKERS + R.id.fingerprinters, R.id.fingerprinters_loaded -> FINGERPRINTERS + R.id.cross_site_tracking, R.id.cross_site_tracking_loaded -> CROSS_SITE_TRACKING_COOKIES + R.id.tracking_content, R.id.tracking_content_loaded -> TRACKING_CONTENT + R.id.cryptominers, R.id.cryptominers_loaded -> CRYPTOMINERS + else -> null + } + + /** + * Returns true if the view corresponds to a "loaded" category + */ + private fun isLoaded(v: View) = when (v.id) { + R.id.social_media_trackers_loaded, + R.id.fingerprinters_loaded, + R.id.cross_site_tracking_loaded, + R.id.tracking_content_loaded, + R.id.cryptominers_loaded -> true + + R.id.social_media_trackers, + R.id.fingerprinters, + R.id.cross_site_tracking, + R.id.tracking_content, + R.id.cryptominers -> false + else -> false + } + } } diff --git a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackerBucketsTest.kt b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackerBucketsTest.kt new file mode 100644 index 000000000..7efc0c7fe --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackerBucketsTest.kt @@ -0,0 +1,53 @@ +/* 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.trackingprotection + +import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory.AD +import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory.FINGERPRINTING +import mozilla.components.concept.engine.content.blocking.Tracker +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.CRYPTOMINERS +import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.FINGERPRINTERS +import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.TRACKING_CONTENT + +class TrackerBucketsTest { + + @Test + fun `initializes with empty map`() { + assertTrue(TrackerBuckets().isEmpty()) + assertTrue(TrackerBuckets().buckets.isEmpty()) + } + + @Test + fun `getter accesses corresponding bucket`() { + val buckets = TrackerBuckets() + buckets.updateIfNeeded(listOf( + Tracker("http://facebook.com", listOf(FINGERPRINTING, AD)), + Tracker("https://google.com", listOf(AD)), + Tracker("https://mozilla.com") + )) + + assertEquals(listOf("google.com"), buckets[TRACKING_CONTENT]) + assertEquals(listOf("facebook.com"), buckets[FINGERPRINTERS]) + assertEquals(emptyList(), buckets[CRYPTOMINERS]) + } + + @Test + fun `sorts trackers into bucket`() { + val buckets = TrackerBuckets() + buckets.updateIfNeeded(listOf( + Tracker("http://facebook.com", listOf(FINGERPRINTING, AD)), + Tracker("https://google.com", listOf(AD)), + Tracker("https://mozilla.com") + )) + + assertEquals(mapOf( + TRACKING_CONTENT to listOf("google.com"), + FINGERPRINTERS to listOf("facebook.com") + ), buckets.buckets) + } +}