1
0
Fork 0

Separates tracker bucket sorting code

master
Tiger Oakes 2019-09-13 21:21:50 -07:00 committed by Emily Kager
parent 7214f40008
commit a61391ef58
3 changed files with 216 additions and 130 deletions

View File

@ -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<Tracker>()
var buckets = emptyMap<TrackingProtectionCategory, List<String>>()
private set
/**
* If [newTrackers] has changed since the last call,
* update [buckets] based on the new trackers list.
*/
fun updateIfNeeded(newTrackers: List<Tracker>) {
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<Tracker>
): Map<TrackingProtectionCategory, List<String>> {
val map = EnumMap<TrackingProtectionCategory, List<String>>(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
}
}
}

View File

@ -9,19 +9,13 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.view.isGone
import kotlinx.android.extensions.LayoutContainer import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.component_tracking_protection_panel.* 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.fragment_quick_settings_dialog_sheet.url
import kotlinx.android.synthetic.main.switch_with_description.view.* 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 mozilla.components.support.ktx.android.net.hostWithoutCommonPrefixes
import org.mozilla.fenix.R 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.CROSS_SITE_TRACKING_COOKIES
import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.CRYPTOMINERS import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.CRYPTOMINERS
import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.FINGERPRINTERS import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.FINGERPRINTERS
@ -63,40 +57,24 @@ interface TrackingProtectionPanelViewInteractor {
class TrackingProtectionPanelView( class TrackingProtectionPanelView(
override val containerView: ViewGroup, override val containerView: ViewGroup,
val interactor: TrackingProtectionPanelInteractor val interactor: TrackingProtectionPanelInteractor
) : LayoutContainer { ) : LayoutContainer, View.OnClickListener {
val view: ConstraintLayout = LayoutInflater.from(containerView.context) val view: ConstraintLayout = LayoutInflater.from(containerView.context)
.inflate(R.layout.component_tracking_protection_panel, containerView, true) .inflate(R.layout.component_tracking_protection_panel, containerView, true)
.findViewById(R.id.panel_wrapper) .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 var bucketedTrackers = TrackerBuckets()
private set private var bucketedLoadedTrackers = TrackerBuckets()
var trackers: List<Tracker> = listOf()
private set
var bucketedTrackers: HashMap<TrackingProtectionCategory, List<String>> = HashMap()
var loadedTrackers: List<Tracker> = listOf()
private set
var bucketedLoadedTrackers: HashMap<TrackingProtectionCategory, List<String>> = HashMap()
fun update(state: TrackingProtectionState) { fun update(state: TrackingProtectionState) {
if (state.mode != mode) { if (state.mode != mode) {
mode = state.mode mode = state.mode
} }
if (state.listTrackers != trackers) { bucketedTrackers.updateIfNeeded(state.listTrackers)
trackers = state.listTrackers bucketedLoadedTrackers.updateIfNeeded(state.listTrackersLoaded)
bucketedTrackers = getHashMapOfTrackersForCategory(state.listTrackers)
}
if (state.listTrackersLoaded != loadedTrackers) {
loadedTrackers = state.listTrackersLoaded
bucketedLoadedTrackers = getHashMapOfTrackersForCategory(state.listTrackersLoaded)
}
when (val mode = state.mode) { when (val mode = state.mode) {
is TrackingProtectionState.Mode.Normal -> setUIForNormalMode(state) is TrackingProtectionState.Mode.Normal -> setUIForNormalMode(state)
@ -110,143 +88,84 @@ class TrackingProtectionPanelView(
private fun setUIForNormalMode(state: TrackingProtectionState) { private fun setUIForNormalMode(state: TrackingProtectionState) {
details_mode.visibility = View.GONE details_mode.visibility = View.GONE
normal_mode.visibility = View.VISIBLE normal_mode.visibility = View.VISIBLE
protection_settings.visibility = protection_settings.isGone = state.session?.customTabConfig != null
if (state.session?.customTabConfig != null) View.GONE else View.VISIBLE
not_blocking_header.visibility = not_blocking_header.isGone = bucketedLoadedTrackers.isEmpty()
if (bucketedLoadedTrackers.size == 0) View.GONE else View.VISIBLE
bindUrl(state.url) bindUrl(state.url)
bindTrackingProtectionInfo(state.isTrackingProtectionEnabled) bindTrackingProtectionInfo(state.isTrackingProtectionEnabled)
protection_settings.setOnClickListener { protection_settings.setOnClickListener {
interactor.selectTrackingProtectionSettings() interactor.selectTrackingProtectionSettings()
} }
blocking_header.visibility = blocking_header.isGone = bucketedTrackers.isEmpty()
if (bucketedTrackers.size == 0) View.GONE else View.VISIBLE
updateCategoryVisibility() updateCategoryVisibility()
setCategoryClickListeners() setCategoryClickListeners()
} }
@Suppress("ComplexMethod")
private fun updateCategoryVisibility() { private fun updateCategoryVisibility() {
cross_site_tracking.visibility = bucketedTrackers.getVisibility(CROSS_SITE_TRACKING_COOKIES) cross_site_tracking.isGone = bucketedTrackers[CROSS_SITE_TRACKING_COOKIES].isEmpty()
social_media_trackers.visibility = bucketedTrackers.getVisibility(SOCIAL_MEDIA_TRACKERS) social_media_trackers.isGone = bucketedTrackers[SOCIAL_MEDIA_TRACKERS].isEmpty()
fingerprinters.visibility = bucketedTrackers.getVisibility(FINGERPRINTERS) fingerprinters.isGone = bucketedTrackers[FINGERPRINTERS].isEmpty()
tracking_content.visibility = bucketedTrackers.getVisibility(TRACKING_CONTENT) tracking_content.isGone = bucketedTrackers[TRACKING_CONTENT].isEmpty()
cryptominers.visibility = bucketedTrackers.getVisibility(CRYPTOMINERS) cryptominers.isGone = bucketedTrackers[CRYPTOMINERS].isEmpty()
cross_site_tracking_loaded.visibility = cross_site_tracking_loaded.isGone =
bucketedLoadedTrackers.getVisibility(CROSS_SITE_TRACKING_COOKIES) bucketedLoadedTrackers[CROSS_SITE_TRACKING_COOKIES].isEmpty()
social_media_trackers_loaded.visibility = social_media_trackers_loaded.isGone =
bucketedLoadedTrackers.getVisibility(SOCIAL_MEDIA_TRACKERS) bucketedLoadedTrackers[SOCIAL_MEDIA_TRACKERS].isEmpty()
fingerprinters_loaded.visibility = bucketedLoadedTrackers.getVisibility(FINGERPRINTERS) fingerprinters_loaded.isGone = bucketedLoadedTrackers[FINGERPRINTERS].isEmpty()
tracking_content_loaded.visibility = bucketedLoadedTrackers.getVisibility(TRACKING_CONTENT) tracking_content_loaded.isGone = bucketedLoadedTrackers[TRACKING_CONTENT].isEmpty()
cryptominers_loaded.visibility = bucketedLoadedTrackers.getVisibility(CRYPTOMINERS) cryptominers_loaded.isGone = bucketedLoadedTrackers[CRYPTOMINERS].isEmpty()
} }
private fun HashMap<TrackingProtectionCategory, List<String>>.getVisibility(
category: TrackingProtectionCategory
): Int = if (this[category]?.isNotEmpty() == true) View.VISIBLE else View.GONE
private fun setCategoryClickListeners() { private fun setCategoryClickListeners() {
social_media_trackers.setOnClickListener { social_media_trackers.setOnClickListener(this)
interactor.openDetails(SOCIAL_MEDIA_TRACKERS, categoryBlocked = true) fingerprinters.setOnClickListener(this)
} cross_site_tracking.setOnClickListener(this)
fingerprinters.setOnClickListener { tracking_content.setOnClickListener(this)
interactor.openDetails(FINGERPRINTERS, categoryBlocked = true) cryptominers.setOnClickListener(this)
} social_media_trackers_loaded.setOnClickListener(this)
cross_site_tracking.setOnClickListener { fingerprinters_loaded.setOnClickListener(this)
interactor.openDetails(CROSS_SITE_TRACKING_COOKIES, categoryBlocked = true) cross_site_tracking_loaded.setOnClickListener(this)
} tracking_content_loaded.setOnClickListener(this)
tracking_content.setOnClickListener { cryptominers_loaded.setOnClickListener(this)
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)
} }
override fun onClick(v: View) {
val category = getCategory(v) ?: return
interactor.openDetails(category, categoryBlocked = !isLoaded(v))
} }
private fun setUIForDetailsMode( private fun setUIForDetailsMode(
category: TrackingProtectionCategory, category: TrackingProtectionCategory,
categoryBlocked: Boolean categoryBlocked: Boolean
) { ) {
val context = view.context
normal_mode.visibility = View.GONE normal_mode.visibility = View.GONE
details_mode.visibility = View.VISIBLE details_mode.visibility = View.VISIBLE
category_title.text = context.getString(category.title) category_title.text = context.getString(category.title)
val stringList = bucketedTrackers[category]?.joinToString("\n") blocking_text_list.text = bucketedTrackers[category].joinToString("\n")
blocking_text_list.text = stringList
category_description.text = context.getString(category.description) category_description.text = context.getString(category.description)
details_blocking_header.text = details_blocking_header.text = context.getString(
context.getString( if (categoryBlocked) {
if (categoryBlocked) R.string.enhanced_tracking_protection_blocked else R.string.enhanced_tracking_protection_blocked
} else {
R.string.enhanced_tracking_protection_allowed R.string.enhanced_tracking_protection_allowed
}
) )
details_back.setOnClickListener { details_back.setOnClickListener {
interactor.onBackPressed() interactor.onBackPressed()
} }
} }
private fun getHashMapOfTrackersForCategory(
list: List<Tracker>
): HashMap<TrackingProtectionCategory, List<String>> {
val hashMap = HashMap<TrackingProtectionCategory, List<String>>()
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) { private fun bindUrl(url: String) {
this.url.text = url.toUri().hostWithoutCommonPrefixes this.url.text = url.toUri().hostWithoutCommonPrefixes
} }
private fun bindTrackingProtectionInfo(isTrackingProtectionOn: Boolean) { private fun bindTrackingProtectionInfo(isTrackingProtectionOn: Boolean) {
tracking_protection.switchItemDescription.text = 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.isChecked = isTrackingProtectionOn
tracking_protection.switch_widget.setOnCheckedChangeListener { _, isChecked -> tracking_protection.switch_widget.setOnCheckedChangeListener { _, isChecked ->
@ -264,4 +183,37 @@ class TrackingProtectionPanelView(
else -> false 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
}
}
} }

View File

@ -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<String>(), 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)
}
}