Bug 1797577 - Add cookie banner handling panel to the toolbar.

This commit is contained in:
Arturo Mejia 2022-12-01 09:37:40 -05:00 committed by mergify[bot]
parent e7a7712a6b
commit cc666c8887
51 changed files with 1860 additions and 452 deletions

View File

@ -6883,7 +6883,55 @@ cookie_banners:
metadata:
tags:
- Privacy&Security
exception_added:
type: event
description: |
A user added a cookie banner handling exception through
the toggle in the protections panel.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1797577
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/28044#issuecomment-1334548056
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 118
metadata:
tags:
- Privacy&Security
exception_removed:
type: event
description: |
A user removed a cookie banner handling
exception through the toggle in the protections panel.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1797577
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/28044#issuecomment-1334548056
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 118
metadata:
tags:
- Privacy&Security
visited_panel:
type: event
description: A user visited the cookie banner toolbar panel
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1797577
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/28044#issuecomment-1334548056
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 118
metadata:
tags:
- Privacy&Security
site_permissions:
prompt_shown:
type: event

View File

@ -11,8 +11,12 @@ import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.appcompat.content.res.AppCompatResources
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.TabSessionState
@ -38,6 +42,7 @@ import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.shortcut.PwaOnboardingObserver
import org.mozilla.fenix.theme.ThemeManager
@ -360,22 +365,35 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
}
override fun navToQuickSettingsSheet(tab: SessionState, sitePermissions: SitePermissions?) {
requireComponents.useCases.trackingProtectionUseCases.containsException(tab.id) { contains ->
runIfFragmentIsAttached {
val isTrackingProtectionEnabled = tab.trackingProtection.enabled && !contains
val directions =
BrowserFragmentDirections.actionBrowserFragmentToQuickSettingsSheetDialogFragment(
sessionId = tab.id,
url = tab.content.url,
title = tab.content.title,
isSecured = tab.content.securityInfo.secure,
sitePermissions = sitePermissions,
gravity = getAppropriateLayoutGravity(),
certificateName = tab.content.securityInfo.issuer,
permissionHighlights = tab.content.permissionHighlights,
isTrackingProtectionEnabled = isTrackingProtectionEnabled,
val useCase = requireComponents.useCases.trackingProtectionUseCases
FxNimbus.features.cookieBanners.recordExposure()
useCase.containsException(tab.id) { hasTrackingProtectionException ->
lifecycleScope.launch(Dispatchers.Main) {
val cookieBannersStorage = requireComponents.core.cookieBannersStorage
val hasCookieBannerException = withContext(Dispatchers.IO) {
cookieBannersStorage.hasException(
tab.content.url,
tab.content.private,
)
nav(R.id.browserFragment, directions)
}
runIfFragmentIsAttached {
val isTrackingProtectionEnabled =
tab.trackingProtection.enabled && !hasTrackingProtectionException
val directions =
BrowserFragmentDirections.actionBrowserFragmentToQuickSettingsSheetDialogFragment(
sessionId = tab.id,
url = tab.content.url,
title = tab.content.title,
isSecured = tab.content.securityInfo.secure,
sitePermissions = sitePermissions,
gravity = getAppropriateLayoutGravity(),
certificateName = tab.content.securityInfo.issuer,
permissionHighlights = tab.content.permissionHighlights,
isTrackingProtectionEnabled = isTrackingProtectionEnabled,
isCookieHandlingEnabled = !hasCookieBannerException,
)
nav(R.id.browserFragment, directions)
}
}
}
}

View File

@ -12,6 +12,7 @@ import androidx.appcompat.content.res.AppCompatResources.getDrawable
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import mozilla.components.browser.engine.gecko.GeckoEngine
import mozilla.components.browser.engine.gecko.cookiebanners.GeckoCookieBannersStorage
import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient
import mozilla.components.browser.engine.gecko.permission.GeckoSitePermissionsStorage
import mozilla.components.browser.icons.BrowserIcons
@ -183,6 +184,8 @@ class Core(
)
}
val cookieBannersStorage by lazyMonitored { GeckoCookieBannersStorage(geckoRuntime) }
val geckoSitePermissionsStorage by lazyMonitored {
GeckoSitePermissionsStorage(geckoRuntime, OnDiskSitePermissionsStorage(context))
}

View File

@ -9,7 +9,11 @@ import android.content.Intent
import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.navArgs
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.concept.engine.manifest.WebAppManifestParser
@ -29,6 +33,7 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BaseBrowserFragment
import org.mozilla.fenix.browser.CustomTabContextMenuCandidate
import org.mozilla.fenix.browser.FenixSnackbarDelegate
import org.mozilla.fenix.components.components
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents
@ -159,21 +164,29 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
}
override fun navToQuickSettingsSheet(tab: SessionState, sitePermissions: SitePermissions?) {
val cookieBannersStorage = requireComponents.core.cookieBannersStorage
requireComponents.useCases.trackingProtectionUseCases.containsException(tab.id) { contains ->
runIfFragmentIsAttached {
val directions = ExternalAppBrowserFragmentDirections
.actionGlobalQuickSettingsSheetDialogFragment(
sessionId = tab.id,
url = tab.content.url,
title = tab.content.title,
isSecured = tab.content.securityInfo.secure,
sitePermissions = sitePermissions,
gravity = getAppropriateLayoutGravity(),
certificateName = tab.content.securityInfo.issuer,
permissionHighlights = tab.content.permissionHighlights,
isTrackingProtectionEnabled = tab.trackingProtection.enabled && !contains,
)
nav(R.id.externalAppBrowserFragment, directions)
lifecycleScope.launch(Dispatchers.IO) {
val hasException =
cookieBannersStorage.hasException(tab.content.url, tab.content.private)
withContext(Dispatchers.Main) {
runIfFragmentIsAttached {
val directions = ExternalAppBrowserFragmentDirections
.actionGlobalQuickSettingsSheetDialogFragment(
sessionId = tab.id,
url = tab.content.url,
title = tab.content.title,
isSecured = tab.content.securityInfo.secure,
sitePermissions = sitePermissions,
gravity = getAppropriateLayoutGravity(),
certificateName = tab.content.securityInfo.issuer,
permissionHighlights = tab.content.permissionHighlights,
isTrackingProtectionEnabled = tab.trackingProtection.enabled && !contains,
isCookieHandlingEnabled = !hasException,
)
nav(R.id.externalAppBrowserFragment, directions)
}
}
}
}
}

View File

@ -281,6 +281,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
SettingsFragmentDirections.actionSettingsFragmentToHttpsOnlyFragment()
}
resources.getString(R.string.pref_key_cookie_banner_settings) -> {
FxNimbus.features.cookieBanners.recordExposure()
CookieBanners.visitedSetting.record(mozilla.components.service.glean.private.NoExtras())
SettingsFragmentDirections.actionSettingsFragmentToCookieBannerFragment()
}

View File

@ -7,7 +7,12 @@ package org.mozilla.fenix.settings.quicksettings
import android.content.Context
import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.browser.state.state.SessionState
import mozilla.components.concept.engine.cookiehandling.CookieBannersStorage
import mozilla.components.concept.engine.permission.SitePermissions
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.ext.components
@ -29,9 +34,12 @@ interface ConnectionDetailsController {
/**
* Default behavior of [ConnectionDetailsController].
*/
@Suppress("LongParameterList")
class DefaultConnectionDetailsController(
private val context: Context,
private val fragment: Fragment,
private val ioScope: CoroutineScope,
private val cookieBannersStorage: CookieBannersStorage,
private val navController: () -> NavController,
internal var sitePermissions: SitePermissions?,
private val gravity: Int,
@ -41,22 +49,30 @@ class DefaultConnectionDetailsController(
override fun handleBackPressed() {
getCurrentTab()?.let { tab ->
context.components.useCases.trackingProtectionUseCases.containsException(tab.id) { contains ->
fragment.runIfFragmentIsAttached {
navController().popBackStack()
val isTrackingProtectionEnabled = tab.trackingProtection.enabled && !contains
val directions =
BrowserFragmentDirections.actionGlobalQuickSettingsSheetDialogFragment(
sessionId = tab.id,
url = tab.content.url,
title = tab.content.title,
isSecured = tab.content.securityInfo.secure,
sitePermissions = sitePermissions,
gravity = gravity,
certificateName = tab.content.securityInfo.issuer,
permissionHighlights = tab.content.permissionHighlights,
isTrackingProtectionEnabled = isTrackingProtectionEnabled,
)
navController().navigate(directions)
ioScope.launch {
val hasException =
cookieBannersStorage.hasException(tab.content.url, tab.content.private)
withContext(Dispatchers.Main) {
fragment.runIfFragmentIsAttached {
navController().popBackStack()
val isTrackingProtectionEnabled =
tab.trackingProtection.enabled && !contains
val directions =
BrowserFragmentDirections.actionGlobalQuickSettingsSheetDialogFragment(
sessionId = tab.id,
url = tab.content.url,
title = tab.content.title,
isSecured = tab.content.securityInfo.secure,
sitePermissions = sitePermissions,
gravity = gravity,
certificateName = tab.content.securityInfo.issuer,
permissionHighlights = tab.content.permissionHighlights,
isTrackingProtectionEnabled = isTrackingProtectionEnabled,
isCookieHandlingEnabled = !hasException,
)
navController().navigate(directions)
}
}
}
}
}

View File

@ -9,8 +9,11 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.plus
import mozilla.components.browser.state.selector.findTabOrCustomTab
import mozilla.components.browser.state.state.SessionState
import org.mozilla.fenix.R
@ -39,6 +42,8 @@ class ConnectionPanelDialogFragment : FenixDialogFragment() {
val controller = DefaultConnectionDetailsController(
context = requireContext(),
ioScope = viewLifecycleOwner.lifecycleScope + Dispatchers.IO,
cookieBannersStorage = requireComponents.core.cookieBannersStorage,
fragment = this,
navController = { findNavController() },
sitePermissions = args.sitePermissions,

View File

@ -17,6 +17,7 @@ import mozilla.components.feature.session.SessionUseCases.ReloadUrlUseCase
import mozilla.components.support.base.feature.OnNeedToRequestPermissions
import mozilla.components.support.ktx.kotlin.getOrigin
import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.GleanMetrics.CookieBanners
import org.mozilla.fenix.GleanMetrics.TrackingProtection
import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.components.PermissionStorage
@ -60,14 +61,19 @@ interface QuickSettingsController {
fun handleAndroidPermissionGranted(feature: PhoneFeature)
/**
* @see [TrackingProtectionInteractor.onTrackingProtectionToggled]
* @see [ProtectionsInteractor.onTrackingProtectionToggled]
*/
fun handleTrackingProtectionToggled(isEnabled: Boolean)
/**
* @see [TrackingProtectionInteractor.onDetailsClicked]
* Navigates to the cookie banners details panel.
*/
fun handleDetailsClicked()
fun handleCookieBannerHandlingDetailsClicked()
/**
* Navigates to the tracking protection details panel.
*/
fun handleTrackingProtectionDetailsClicked()
/**
* Navigates to the connection details. Called when a user clicks on the
@ -201,15 +207,34 @@ class DefaultQuickSettingsController(
)
}
override fun handleDetailsClicked() {
override fun handleCookieBannerHandlingDetailsClicked() {
CookieBanners.visitedPanel.record(NoExtras())
navController.popBackStack()
val state = quickSettingsStore.state.trackingProtectionState
val state = quickSettingsStore.state.protectionsState
val directions = NavGraphDirections
.actionGlobalCookieBannerProtectionPanelDialogFragment(
sessionId = sessionId,
url = state.url,
trackingProtectionEnabled = state.isTrackingProtectionEnabled,
cookieBannerHandlingEnabled = state.isCookieBannerHandlingEnabled,
gravity = context.components.settings.toolbarPosition.androidGravity,
sitePermissions = sitePermissions,
)
navController.navigate(directions)
}
override fun handleTrackingProtectionDetailsClicked() {
navController.popBackStack()
val state = quickSettingsStore.state.protectionsState
val directions = NavGraphDirections
.actionGlobalTrackingProtectionPanelDialogFragment(
sessionId = sessionId,
url = state.url,
trackingProtectionEnabled = state.isTrackingProtectionEnabled,
cookieBannerHandlingEnabled = state.isCookieBannerHandlingEnabled,
gravity = context.components.settings.toolbarPosition.androidGravity,
sitePermissions = sitePermissions,
)

View File

@ -6,7 +6,7 @@ package org.mozilla.fenix.settings.quicksettings
import mozilla.components.lib.state.Action
import org.mozilla.fenix.settings.PhoneFeature
import org.mozilla.fenix.trackingprotection.TrackingProtectionState
import org.mozilla.fenix.trackingprotection.ProtectionsState
/**
* Parent [Action] for all the [QuickSettingsFragmentState] changes.
@ -49,7 +49,7 @@ sealed class WebsitePermissionAction(open val updatedFeature: PhoneFeature) : Qu
}
/**
* All possible [TrackingProtectionState] changes as a result oof user / system interactions.
* All possible [ProtectionsState] changes in the quick setting panel.
*/
sealed class TrackingProtectionAction : QuickSettingsFragmentAction() {
/**

View File

@ -4,7 +4,7 @@
package org.mozilla.fenix.settings.quicksettings
import org.mozilla.fenix.trackingprotection.TrackingProtectionState
import org.mozilla.fenix.trackingprotection.ProtectionsState
/**
* Parent Reducer for all [QuickSettingsFragmentState]s of all Views shown in this Fragment.
@ -27,8 +27,8 @@ internal fun quickSettingsFragmentReducer(
),
)
is TrackingProtectionAction -> state.copy(
trackingProtectionState = TrackingProtectionStateReducer.reduce(
state = state.trackingProtectionState,
protectionsState = ProtectionsStateReducer.reduce(
state = state.protectionsState,
action = action,
),
)
@ -67,15 +67,18 @@ object WebsitePermissionsStateReducer {
}
}
object TrackingProtectionStateReducer {
/**
* A reduce for [TrackingProtectionAction]s.
*/
object ProtectionsStateReducer {
/**
* Handles creating a new [TrackingProtectionState] based on the specific
* Handles creating a new [ProtectionsState] based on the specific
* [TrackingProtectionAction].
*/
fun reduce(
state: TrackingProtectionState,
state: ProtectionsState,
action: TrackingProtectionAction,
): TrackingProtectionState {
): ProtectionsState {
return when (action) {
is TrackingProtectionAction.ToggleTrackingProtectionEnabled ->
state.copy(isTrackingProtectionEnabled = action.isTrackingProtectionEnabled)

View File

@ -14,18 +14,18 @@ import mozilla.components.feature.sitepermissions.SitePermissionsRules.AutoplayA
import mozilla.components.lib.state.State
import org.mozilla.fenix.R
import org.mozilla.fenix.settings.PhoneFeature
import org.mozilla.fenix.trackingprotection.TrackingProtectionState
import org.mozilla.fenix.trackingprotection.ProtectionsState
import org.mozilla.fenix.utils.Settings
/**
* [State] containing all data displayed to the user by this Fragment.
*
* Partitioned further to contain mutiple states for each standalone View this Fragment holds.
* Partitioned further to contain multiple states for each standalone View this Fragment holds.
*/
data class QuickSettingsFragmentState(
val webInfoState: WebsiteInfoState,
val websitePermissionsState: WebsitePermissionsState,
val trackingProtectionState: TrackingProtectionState,
val protectionsState: ProtectionsState,
) : State
/**

View File

@ -19,7 +19,7 @@ import org.mozilla.fenix.settings.quicksettings.QuickSettingsFragmentStore.Compa
import org.mozilla.fenix.settings.quicksettings.WebsiteInfoState.Companion.createWebsiteInfoState
import org.mozilla.fenix.settings.quicksettings.ext.shouldBeEnabled
import org.mozilla.fenix.settings.quicksettings.ext.shouldBeVisible
import org.mozilla.fenix.trackingprotection.TrackingProtectionState
import org.mozilla.fenix.trackingprotection.ProtectionsState
import org.mozilla.fenix.utils.Settings
import java.util.EnumMap
@ -69,6 +69,7 @@ class QuickSettingsFragmentStore(
settings: Settings,
sessionId: String,
isTrackingProtectionEnabled: Boolean,
isCookieHandlingEnabled: Boolean,
) = QuickSettingsFragmentStore(
QuickSettingsFragmentState(
webInfoState = createWebsiteInfoState(
@ -83,11 +84,12 @@ class QuickSettingsFragmentStore(
permissionHighlights,
settings,
),
trackingProtectionState = createTrackingProtectionState(
protectionsState = createTrackingProtectionState(
context,
sessionId,
websiteUrl,
isTrackingProtectionEnabled,
isCookieHandlingEnabled,
),
),
)
@ -123,14 +125,16 @@ class QuickSettingsFragmentStore(
}
/**
* Construct an initial [TrackingProtectionState] to be rendered by
* [TrackingProtectionView].
* Construct an initial [ProtectionsState] to be rendered by
* [ProtectionsView].
*
* @param context [Context] used for various Android interactions.
* @param sessionId [String] The current session ID.
* @param websiteUrl [String] the URL of the current web page.
* @param isTrackingProtectionEnabled [Boolean] Current status of tracking protection
* for this session.
* @param isCookieHandlingEnabled [Boolean] Current status of cookie banner handling
* for this session.
*/
@VisibleForTesting
fun createTrackingProtectionState(
@ -138,13 +142,15 @@ class QuickSettingsFragmentStore(
sessionId: String,
websiteUrl: String,
isTrackingProtectionEnabled: Boolean,
): TrackingProtectionState {
return TrackingProtectionState(
isCookieHandlingEnabled: Boolean,
): ProtectionsState {
return ProtectionsState(
tab = context.components.core.store.state.findTabOrCustomTab(sessionId),
url = websiteUrl,
isTrackingProtectionEnabled = isTrackingProtectionEnabled,
isCookieBannerHandlingEnabled = isCookieHandlingEnabled,
listTrackers = listOf(),
mode = TrackingProtectionState.Mode.Normal,
mode = ProtectionsState.Mode.Normal,
lastAccessedCategory = "",
)
}

View File

@ -4,6 +4,8 @@
package org.mozilla.fenix.settings.quicksettings
import org.mozilla.fenix.settings.quicksettings.protections.ProtectionsInteractor
/**
* [QuickSettingsSheetDialogFragment] interactor.
*
@ -15,7 +17,7 @@ package org.mozilla.fenix.settings.quicksettings
*/
class QuickSettingsInteractor(
private val controller: QuickSettingsController,
) : WebsitePermissionInteractor, TrackingProtectionInteractor, WebSiteInfoInteractor, ClearSiteDataViewInteractor {
) : WebsitePermissionInteractor, ProtectionsInteractor, WebSiteInfoInteractor, ClearSiteDataViewInteractor {
override fun onPermissionsShown() {
controller.handlePermissionsShown()
}
@ -32,8 +34,12 @@ class QuickSettingsInteractor(
controller.handleTrackingProtectionToggled(isEnabled)
}
override fun onDetailsClicked() {
controller.handleDetailsClicked()
override fun onCookieBannerHandlingDetailsClicked() {
controller.handleCookieBannerHandlingDetailsClicked()
}
override fun onTrackingProtectionDetailsClicked() {
controller.handleTrackingProtectionDetailsClicked()
}
override fun onConnectionDetailsClicked() {

View File

@ -17,7 +17,6 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.plus
import mozilla.components.browser.state.selector.findTabOrCustomTab
@ -35,6 +34,7 @@ import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.settings.PhoneFeature
import org.mozilla.fenix.settings.quicksettings.protections.ProtectionsView
/**
* Dialog that presents the user with information about
@ -52,7 +52,7 @@ class QuickSettingsSheetDialogFragment : FenixDialogFragment() {
private lateinit var clearSiteDataView: ClearSiteDataView
@VisibleForTesting
internal lateinit var trackingProtectionView: TrackingProtectionView
internal lateinit var protectionsView: ProtectionsView
private lateinit var interactor: QuickSettingsInteractor
@ -91,6 +91,7 @@ class QuickSettingsSheetDialogFragment : FenixDialogFragment() {
permissionHighlights = args.permissionHighlights,
sessionId = args.sessionId,
isTrackingProtectionEnabled = args.isTrackingProtectionEnabled,
isCookieHandlingEnabled = args.isCookieHandlingEnabled,
)
quickSettingsController = DefaultQuickSettingsController(
@ -115,8 +116,8 @@ class QuickSettingsSheetDialogFragment : FenixDialogFragment() {
websiteInfoView = WebsiteInfoView(binding.websiteInfoLayout, interactor = interactor)
websitePermissionsView =
WebsitePermissionsView(binding.websitePermissionsLayout, interactor)
trackingProtectionView =
TrackingProtectionView(binding.trackingProtectionLayout, interactor, context.settings())
protectionsView =
ProtectionsView(binding.trackingProtectionLayout, interactor, context.settings())
clearSiteDataView = ClearSiteDataView(
context = context,
ioScope = viewLifecycleOwner.lifecycleScope + Dispatchers.IO,
@ -135,7 +136,7 @@ class QuickSettingsSheetDialogFragment : FenixDialogFragment() {
consumeFrom(quickSettingsStore) {
websiteInfoView.update(it.webInfoState)
websitePermissionsView.update(it.websitePermissionsState)
trackingProtectionView.update(it.trackingProtectionState)
protectionsView.update(it.protectionsState)
clearSiteDataView.update(it.webInfoState)
}
}
@ -210,7 +211,7 @@ class QuickSettingsSheetDialogFragment : FenixDialogFragment() {
provideTrackingProtectionUseCases().fetchTrackingLogs(
tab.id,
onSuccess = { trackers ->
trackingProtectionView.updateDetailsSection(trackers.isNotEmpty())
protectionsView.updateDetailsSection(trackers.isNotEmpty())
},
onError = {
Logger.error("QuickSettingsSheetDialogFragment - fetchTrackingLogs onError", it)

View File

@ -1,79 +0,0 @@
/* 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.settings.quicksettings
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.core.view.isVisible
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.QuicksettingsTrackingProtectionBinding
import org.mozilla.fenix.trackingprotection.TrackingProtectionState
import org.mozilla.fenix.utils.Settings
/**
* Contract declaring all possible user interactions with [TrackingProtectionView].
*/
interface TrackingProtectionInteractor {
/**
* Called whenever the tracking protection toggle for this site is toggled.
*
* @param isEnabled Whether or not tracking protection is enabled.
*/
fun onTrackingProtectionToggled(isEnabled: Boolean)
/**
* Navigates to the tracking protection preferences. Called when a user clicks on the
* "Details" button.
*/
fun onDetailsClicked()
}
/**
* MVI View that displays the tracking protection toggle and navigation to additional tracking
* protection details.
*
* @param containerView [ViewGroup] in which this View will inflate itself.
* @param interactor [TrackingProtectionInteractor] which will have delegated to all user
* @param settings [Settings] application settings.
* interactions.
*/
class TrackingProtectionView(
val containerView: ViewGroup,
val interactor: TrackingProtectionInteractor,
val settings: Settings,
) {
private val context = containerView.context
@VisibleForTesting
internal val binding = QuicksettingsTrackingProtectionBinding.inflate(
LayoutInflater.from(containerView.context),
containerView,
true,
)
fun update(state: TrackingProtectionState) {
bindTrackingProtectionInfo(state.isTrackingProtectionEnabled)
binding.root.isVisible = settings.shouldUseTrackingProtection
binding.trackingProtectionDetails.setOnClickListener {
interactor.onDetailsClicked()
}
}
fun updateDetailsSection(show: Boolean) {
binding.trackingProtectionDetails.isVisible = show
}
private fun bindTrackingProtectionInfo(isTrackingProtectionEnabled: Boolean) {
binding.trackingProtectionSwitch.trackingProtectionCategoryItemDescription.text =
context.getString(if (isTrackingProtectionEnabled) R.string.etp_panel_on else R.string.etp_panel_off)
binding.trackingProtectionSwitch.switchWidget.isChecked = isTrackingProtectionEnabled
binding.trackingProtectionSwitch.switchWidget.jumpDrawablesToCurrentState()
binding.trackingProtectionSwitch.switchWidget.setOnCheckedChangeListener { _, isChecked ->
interactor.onTrackingProtectionToggled(isChecked)
}
}
}

View File

@ -0,0 +1,29 @@
/* 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.settings.quicksettings.protections
/**
* Contract declaring all possible user interactions with [ProtectionsView].
*/
interface ProtectionsInteractor {
/**
* Called whenever the tracking protection toggle for this site is toggled.
*
* @param isEnabled Whether or not tracking protection is enabled.
*/
fun onTrackingProtectionToggled(isEnabled: Boolean)
/**
* Navigates to the tracking protection details panel.
*/
fun onCookieBannerHandlingDetailsClicked()
/**
* Navigates to the tracking protection preferences. Called when a user clicks on the
* "Details" button.
*/
fun onTrackingProtectionDetailsClicked()
}

View File

@ -0,0 +1,185 @@
/* 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.settings.quicksettings.protections
import android.content.res.Configuration
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.view.isVisible
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.QuicksettingsProtectionsPanelBinding
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.trackingprotection.ProtectionsState
import org.mozilla.fenix.utils.Settings
/**
* MVI View that displays the tracking protection, cookie banner handling toggles and the navigation
* to additional tracking protection details.
*
* @param containerView [ViewGroup] in which this View will inflate itself.
* @param interactor [ProtectionsInteractor] which will have delegated to all user
* @param settings [Settings] application settings.
* interactions.
*/
class ProtectionsView(
val containerView: ViewGroup,
val interactor: ProtectionsInteractor,
val settings: Settings,
) {
/**
* Allows changing what this View displays.
*/
fun update(state: ProtectionsState) {
bindTrackingProtectionInfo(state.isTrackingProtectionEnabled)
bindCookieBannerProtection(state.isCookieBannerHandlingEnabled)
binding.trackingProtectionSwitch.isVisible = settings.shouldUseTrackingProtection
binding.cookieBannerItem.isVisible = shouldShowCookieBanner
binding.trackingProtectionDetails.setOnClickListener {
interactor.onTrackingProtectionDetailsClicked()
}
}
@VisibleForTesting
internal fun updateDetailsSection(show: Boolean) {
binding.trackingProtectionDetails.isVisible = show
}
private fun bindTrackingProtectionInfo(isTrackingProtectionEnabled: Boolean) {
binding.trackingProtectionSwitch.isChecked = isTrackingProtectionEnabled
binding.trackingProtectionSwitch.setOnCheckedChangeListener { _, isChecked ->
interactor.onTrackingProtectionToggled(isChecked)
}
}
@VisibleForTesting
internal val binding = QuicksettingsProtectionsPanelBinding.inflate(
LayoutInflater.from(containerView.context),
containerView,
true,
)
private val shouldShowCookieBanner: Boolean
get() = settings.shouldShowCookieBannerUI && settings.shouldUseCookieBanner
private fun bindCookieBannerProtection(isCookieBannerHandlingEnabled: Boolean) {
val context = binding.cookieBannerItem.context
val label = context.getString(R.string.preferences_cookie_banner_reduction)
val description = context.getString(
if (isCookieBannerHandlingEnabled) {
R.string.reduce_cookie_banner_on_for_site
} else {
R.string.reduce_cookie_banner_off_for_site
},
)
val icon = if (isCookieBannerHandlingEnabled) {
R.drawable.ic_cookies_enabled
} else {
R.drawable.ic_cookies_disabled
}
binding.cookieBannerItem.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
FirefoxTheme {
CookieBannerItem(
label = label,
description = description,
startIconPainter = painterResource(icon),
endIconPainter = painterResource(R.drawable.ic_arrowhead_right),
onClick = { interactor.onCookieBannerHandlingDetailsClicked() },
)
}
}
}
}
}
@Composable
private fun CookieBannerItem(
label: String,
description: String,
startIconPainter: Painter,
endIconPainter: Painter,
onClick: () -> Unit,
) {
Row(
modifier = Modifier
.clickable { onClick() }
.defaultMinSize(minHeight = 48.dp)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = startIconPainter,
contentDescription = null,
modifier = Modifier.padding(horizontal = 0.dp),
tint = FirefoxTheme.colors.iconPrimary,
)
Column(
modifier = Modifier
.padding(horizontal = 8.dp, vertical = 6.dp)
.weight(1f),
) {
Text(
text = label,
color = FirefoxTheme.colors.textPrimary,
style = FirefoxTheme.typography.subtitle1,
maxLines = 1,
)
Text(
text = description,
color = FirefoxTheme.colors.textSecondary,
style = FirefoxTheme.typography.body2,
maxLines = 1,
)
}
Icon(
modifier = Modifier
.padding(end = 0.dp)
.size(24.dp),
painter = endIconPainter,
contentDescription = null,
tint = FirefoxTheme.colors.iconPrimary,
)
}
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun CookieBannerItemPreview() {
FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer1)) {
CookieBannerItem(
label = "Cookie Banner Reduction",
description = "On for this site",
startIconPainter = painterResource(R.drawable.ic_cookies_enabled),
endIconPainter = painterResource(R.drawable.ic_arrowhead_right),
onClick = { println("list item click") },
)
}
}
}

View File

@ -0,0 +1,119 @@
/* 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.settings.quicksettings.protections.cookiebanners
import android.content.Context
import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.browser.state.selector.findTabOrCustomTab
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.cookiehandling.CookieBannersStorage
import mozilla.components.concept.engine.permission.SitePermissions
import mozilla.components.feature.session.SessionUseCases
import mozilla.components.service.glean.private.NoExtras
import org.mozilla.fenix.GleanMetrics.CookieBanners
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.trackingprotection.ProtectionsAction
import org.mozilla.fenix.trackingprotection.ProtectionsStore
/**
* [CookieBannerDetailsController] controller.
*
* Delegated by View Interactors, handles container business logic and operates changes on it,
* complex Android interactions or communication with other features.
*/
interface CookieBannerDetailsController {
/**
* @see [CookieBannerDetailsInteractor.onBackPressed]
*/
fun handleBackPressed()
/**
* @see [CookieBannerDetailsInteractor.onTogglePressed]
*/
fun handleTogglePressed(isEnabled: Boolean)
}
/**
* Default behavior of [CookieBannerDetailsController].
*/
@Suppress("LongParameterList")
class DefaultCookieBannerDetailsController(
private val context: Context,
private val fragment: Fragment,
private val ioScope: CoroutineScope,
internal val sessionId: String,
private val browserStore: BrowserStore,
internal val protectionsStore: ProtectionsStore,
private val cookieBannersStorage: CookieBannersStorage,
private val navController: () -> NavController,
internal var sitePermissions: SitePermissions?,
private val gravity: Int,
private val getCurrentTab: () -> SessionState?,
private val reload: SessionUseCases.ReloadUrlUseCase,
) : CookieBannerDetailsController {
override fun handleBackPressed() {
getCurrentTab()?.let { tab ->
context.components.useCases.trackingProtectionUseCases.containsException(tab.id) { contains ->
ioScope.launch {
val hasException =
cookieBannersStorage.hasException(tab.content.url, tab.content.private)
withContext(Dispatchers.Main) {
fragment.runIfFragmentIsAttached {
navController().popBackStack()
val isTrackingProtectionEnabled = tab.trackingProtection.enabled && !contains
val directions =
BrowserFragmentDirections.actionGlobalQuickSettingsSheetDialogFragment(
sessionId = tab.id,
url = tab.content.url,
title = tab.content.title,
isSecured = tab.content.securityInfo.secure,
sitePermissions = sitePermissions,
gravity = gravity,
certificateName = tab.content.securityInfo.issuer,
permissionHighlights = tab.content.permissionHighlights,
isTrackingProtectionEnabled = isTrackingProtectionEnabled,
isCookieHandlingEnabled = !hasException,
)
navController().navigate(directions)
}
}
}
}
}
}
override fun handleTogglePressed(isEnabled: Boolean) {
val tab = requireNotNull(browserStore.state.findTabOrCustomTab(sessionId)) {
"A session is required to update the cookie banner mode"
}
ioScope.launch {
if (isEnabled) {
cookieBannersStorage.removeException(
uri = tab.content.url,
privateBrowsing = tab.content.private,
)
CookieBanners.exceptionRemoved.record(NoExtras())
} else {
cookieBannersStorage.addException(uri = tab.content.url, privateBrowsing = tab.content.private)
CookieBanners.exceptionAdded.record(NoExtras())
}
protectionsStore.dispatch(
ProtectionsAction.ToggleCookieBannerHandlingProtectionEnabled(
isEnabled,
),
)
reload(tab.id)
}
}
}

View File

@ -0,0 +1,42 @@
/* 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.settings.quicksettings.protections.cookiebanners
/**
* Contract declaring all possible user interactions with [CookieBannerHandlingDetailsView].
*/
interface CookieBannerDetailsInteractor {
/**
* Called whenever back is pressed.
*/
fun onBackPressed() = Unit
/**
* Called whenever the user press the toggle widget.
*/
fun onTogglePressed(vale: Boolean) = Unit
}
/**
* [CookieBannerPanelDialogFragment] interactor.
*
* Implements callbacks for each of [CookieBannerPanelDialogFragment]'s Views declared possible user interactions,
* delegates all such user events to the [CookieBannerDetailsController].
*
* @param controller [CookieBannerDetailsController] which will be delegated for all users interactions,
* it expected to contain all business logic for how to act in response.
*/
class DefaultCookieBannerDetailsInteractor(
private val controller: CookieBannerDetailsController,
) : CookieBannerDetailsInteractor {
override fun onBackPressed() {
controller.handleBackPressed()
}
override fun onTogglePressed(vale: Boolean) {
controller.handleTogglePressed(vale)
}
}

View File

@ -0,0 +1,83 @@
/* 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.settings.quicksettings.protections.cookiebanners
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.support.ktx.kotlin.toShortUrl
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.ComponentCookieBannerDetailsPanelBinding
import org.mozilla.fenix.trackingprotection.ProtectionsState
/**
* MVI View that knows how to display cookie banner handling details for a site.
*
* @param container [ViewGroup] in which this View will inflate itself.
* @param publicSuffixList To show short url.
* @param interactor [CookieBannerDetailsInteractor] which will have delegated to all user interactions.
*/
class CookieBannerHandlingDetailsView(
container: ViewGroup,
private val context: Context,
private val publicSuffixList: PublicSuffixList,
val interactor: CookieBannerDetailsInteractor,
) {
val binding = ComponentCookieBannerDetailsPanelBinding.inflate(
LayoutInflater.from(container.context),
container,
true,
)
/**
* Allows changing what this View displays.
*/
fun update(state: ProtectionsState) {
bindTitle(state.url, state.isCookieBannerHandlingEnabled)
bindBackButtonListener()
bindDescription(state.isCookieBannerHandlingEnabled)
bindSwitch(state.isCookieBannerHandlingEnabled)
}
@VisibleForTesting
internal fun bindTitle(url: String, isCookieBannerHandlingEnabled: Boolean) {
val stringID =
if (isCookieBannerHandlingEnabled) {
R.string.reduce_cookie_banner_details_panel_title_off_for_site
} else {
R.string.reduce_cookie_banner_details_panel_title_on_for_site
}
val shortUrl = url.toShortUrl(publicSuffixList)
binding.title.text = context.getString(stringID, shortUrl)
}
@VisibleForTesting
internal fun bindDescription(isCookieBannerHandlingEnabled: Boolean) {
val stringID =
if (isCookieBannerHandlingEnabled) {
R.string.reduce_cookie_banner_details_panel_description_off_for_site
} else {
R.string.reduce_cookie_banner_details_panel_description_on_for_site
}
binding.details.text = context.getString(stringID, context.getString(R.string.app_name))
}
@VisibleForTesting
internal fun bindBackButtonListener() {
binding.navigateBack.setOnClickListener {
interactor.onBackPressed()
}
}
@VisibleForTesting
internal fun bindSwitch(isCookieBannerHandlingEnabled: Boolean) {
binding.cookieBannerSwitch.isChecked = isCookieBannerHandlingEnabled
binding.cookieBannerSwitch.setOnCheckedChangeListener { _, isChecked ->
interactor.onTogglePressed(isChecked)
}
}
}

View File

@ -0,0 +1,114 @@
/* 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.settings.quicksettings.protections.cookiebanners
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.plus
import mozilla.components.browser.state.selector.findTabOrCustomTab
import mozilla.components.browser.state.state.SessionState
import mozilla.components.lib.state.ext.consumeFrom
import org.mozilla.fenix.R
import org.mozilla.fenix.android.FenixDialogFragment
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.databinding.FragmentCookieBannerHandlingDetailsDialogBinding
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.trackingprotection.ProtectionsState
import org.mozilla.fenix.trackingprotection.ProtectionsStore
/**
* A [FenixDialogFragment] that contains all the cookie banner details for a given tab.
*/
class CookieBannerPanelDialogFragment : FenixDialogFragment() {
@VisibleForTesting
private lateinit var cookieBannersView: CookieBannerHandlingDetailsView
private val args by navArgs<CookieBannerPanelDialogFragmentArgs>()
private var _binding: FragmentCookieBannerHandlingDetailsDialogBinding? = null
override val gravity: Int get() = args.gravity
override val layoutId: Int = R.layout.fragment_cookie_banner_handling_details_dialog
@VisibleForTesting
internal lateinit var protectionsStore: ProtectionsStore
// This property is only valid between onCreateView and onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
val store = requireComponents.core.store
val rootView = inflateRootView(container)
val tab = store.state.findTabOrCustomTab(provideCurrentTabId())
protectionsStore = StoreProvider.get(this) {
ProtectionsStore(
ProtectionsState(
tab = tab,
url = args.url,
isTrackingProtectionEnabled = args.trackingProtectionEnabled,
isCookieBannerHandlingEnabled = args.cookieBannerHandlingEnabled,
listTrackers = listOf(),
mode = ProtectionsState.Mode.Normal,
lastAccessedCategory = "",
),
)
}
val controller = DefaultCookieBannerDetailsController(
context = requireContext(),
ioScope = viewLifecycleOwner.lifecycleScope + Dispatchers.IO,
cookieBannersStorage = requireComponents.core.cookieBannersStorage,
protectionsStore = protectionsStore,
browserStore = requireComponents.core.store,
fragment = this,
sessionId = args.sessionId,
reload = requireComponents.useCases.sessionUseCases.reload,
navController = { findNavController() },
sitePermissions = args.sitePermissions,
gravity = args.gravity,
getCurrentTab = ::getCurrentTab,
)
_binding = FragmentCookieBannerHandlingDetailsDialogBinding.bind(rootView)
cookieBannersView = CookieBannerHandlingDetailsView(
context = requireContext(),
container = binding.cookieBannerDetailsInfoLayout,
publicSuffixList = requireComponents.publicSuffixList,
interactor = DefaultCookieBannerDetailsInteractor(controller),
)
return rootView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
consumeFrom(protectionsStore) { state ->
cookieBannersView.update(state)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
@VisibleForTesting
internal fun provideCurrentTabId(): String = args.sessionId
private fun getCurrentTab(): SessionState? {
return requireComponents.core.store.state.findTabOrCustomTab(args.sessionId)
}
}

View File

@ -13,57 +13,95 @@ import mozilla.components.lib.state.Store
import org.mozilla.fenix.R
/**
* The [Store] for holding the [TrackingProtectionState] and applying [TrackingProtectionAction]s.
* The [Store] for holding the [ProtectionsState] and applying [ProtectionsAction]s.
*/
class TrackingProtectionStore(initialState: TrackingProtectionState) :
Store<TrackingProtectionState, TrackingProtectionAction>(
class ProtectionsStore(initialState: ProtectionsState) :
Store<ProtectionsState, ProtectionsAction>(
initialState,
::trackingProtectionStateReducer,
::protectionsStateReducer,
)
/**
* Actions to dispatch through the `TrackingProtectionStore` to modify `TrackingProtectionState` through the reducer.
* Actions to dispatch through the `TrackingProtectionStore` to modify `ProtectionsState` through the reducer.
*/
sealed class TrackingProtectionAction : Action {
sealed class ProtectionsAction : Action {
/**
* The values of the tracking protection view has been changed.
*/
data class Change(
val url: String,
val isTrackingProtectionEnabled: Boolean,
val isCookieBannerHandlingEnabled: Boolean,
val listTrackers: List<TrackerLog>,
val mode: TrackingProtectionState.Mode,
) : TrackingProtectionAction()
val mode: ProtectionsState.Mode,
) : ProtectionsAction()
data class UrlChange(val url: String) : TrackingProtectionAction()
data class TrackerLogChange(val listTrackers: List<TrackerLog>) : TrackingProtectionAction()
/**
* Toggles the enabled state of cookie banner handling protection.
*
* @param isEnabled Whether or not cookie banner protection is enabled.
*/
data class ToggleCookieBannerHandlingProtectionEnabled(val isEnabled: Boolean) :
ProtectionsAction()
object ExitDetailsMode : TrackingProtectionAction()
/**
* Indicates the url has changed.
*/
data class UrlChange(val url: String) : ProtectionsAction()
/**
* Indicates the url has the list of trackers has been updated.
*/
data class TrackerLogChange(val listTrackers: List<TrackerLog>) : ProtectionsAction()
/**
* Indicates the user is leaving the detailed view.
*/
object ExitDetailsMode : ProtectionsAction()
/**
* Holds the data to show a detailed tracking protection view.
*/
data class EnterDetailsMode(
val category: TrackingProtectionCategory,
val categoryBlocked: Boolean,
) :
TrackingProtectionAction()
) : ProtectionsAction()
}
/**
* The state for the Tracking Protection Panel
* The state for the Protections Panel
* @property tab Current session to display
* @property url Current URL to display
* @property isTrackingProtectionEnabled Current status of tracking protection for this session
* (ie is an exception)
* @property isCookieBannerHandlingEnabled Current status of cookie banner handling protection
* for this session (ie is an exception).
* @property listTrackers Current Tracker Log list of blocked and loaded tracker categories
* @property mode Current Mode of TrackingProtection
* @property lastAccessedCategory Remembers the last accessed details category, used to move
* accessibly focus after returning from details_mode
*/
data class TrackingProtectionState(
data class ProtectionsState(
val tab: SessionState?,
val url: String,
val isTrackingProtectionEnabled: Boolean,
val isCookieBannerHandlingEnabled: Boolean,
val listTrackers: List<TrackerLog>,
val mode: Mode,
val lastAccessedCategory: String,
) : State {
/**
* Indicates the modes in which a tracking protection view could be in.
*/
sealed class Mode {
/**
* Indicates that tracking protection view should not be in detail mode.
*/
object Normal : Mode()
/**
* Indicates that tracking protection view in detailed mode.
*/
data class Details(
val selectedCategory: TrackingProtectionCategory,
val categoryBlocked: Boolean,
@ -105,32 +143,36 @@ enum class TrackingProtectionCategory(
}
/**
* The TrackingProtectionState Reducer.
* The [ProtectionsState] reducer.
*/
fun trackingProtectionStateReducer(
state: TrackingProtectionState,
action: TrackingProtectionAction,
): TrackingProtectionState {
fun protectionsStateReducer(
state: ProtectionsState,
action: ProtectionsAction,
): ProtectionsState {
return when (action) {
is TrackingProtectionAction.Change -> state.copy(
is ProtectionsAction.Change -> state.copy(
url = action.url,
isTrackingProtectionEnabled = action.isTrackingProtectionEnabled,
isCookieBannerHandlingEnabled = action.isCookieBannerHandlingEnabled,
listTrackers = action.listTrackers,
mode = action.mode,
)
is TrackingProtectionAction.UrlChange -> state.copy(
is ProtectionsAction.UrlChange -> state.copy(
url = action.url,
)
is TrackingProtectionAction.TrackerLogChange -> state.copy(listTrackers = action.listTrackers)
TrackingProtectionAction.ExitDetailsMode -> state.copy(
mode = TrackingProtectionState.Mode.Normal,
is ProtectionsAction.TrackerLogChange -> state.copy(listTrackers = action.listTrackers)
ProtectionsAction.ExitDetailsMode -> state.copy(
mode = ProtectionsState.Mode.Normal,
)
is TrackingProtectionAction.EnterDetailsMode -> state.copy(
mode = TrackingProtectionState.Mode.Details(
is ProtectionsAction.EnterDetailsMode -> state.copy(
mode = ProtectionsState.Mode.Details(
action.category,
action.categoryBlocked,
),
lastAccessedCategory = action.category.name,
)
is ProtectionsAction.ToggleCookieBannerHandlingProtectionEnabled -> state.copy(
isCookieBannerHandlingEnabled = action.isEnabled,
)
}
}

View File

@ -7,7 +7,9 @@ package org.mozilla.fenix.trackingprotection
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.CompoundButton
import android.widget.TextView
import androidx.annotation.VisibleForTesting
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.SwitchCompat
import androidx.constraintlayout.widget.ConstraintLayout
@ -15,42 +17,114 @@ import androidx.core.content.withStyledAttributes
import mozilla.components.support.ktx.android.view.putCompoundDrawablesRelativeWithIntrinsicBounds
import org.mozilla.fenix.R
private const val DEFAULT_DRAWABLE: Int = 0
/**
* Add a [SwitchCompat] widget with description that will vary depending on switch status.
*/
class SwitchWithDescription @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : ConstraintLayout(context, attrs, defStyleAttr) {
lateinit var switchWidget: SwitchCompat
lateinit var trackingProtectionCategoryTitle: TextView
lateinit var trackingProtectionCategoryItemDescription: TextView
private lateinit var switchWidget: SwitchCompat
private lateinit var titleWidget: TextView
private lateinit var descriptionWidget: TextView
private lateinit var descriptionOn: String
private lateinit var descriptionOff: String
private var iconOn: Int = 0
private var iconOff: Int = 0
private var shouldShowIcons: Boolean = true
init {
LayoutInflater.from(context).inflate(R.layout.switch_with_description, this, true)
context.withStyledAttributes(attrs, R.styleable.SwitchWithDescription, defStyleAttr, 0) {
val id = getResourceId(
R.styleable.SwitchWithDescription_switchIcon,
R.drawable.ic_tracking_protection,
)
switchWidget = findViewById(R.id.switch_widget)
trackingProtectionCategoryTitle = findViewById(R.id.trackingProtectionCategoryTitle)
trackingProtectionCategoryItemDescription = findViewById(R.id.trackingProtectionCategoryItemDescription)
switchWidget.putCompoundDrawablesRelativeWithIntrinsicBounds(
start = AppCompatResources.getDrawable(context, id),
titleWidget = findViewById(R.id.switch_with_description_title)
descriptionWidget = findViewById(R.id.switch_with_description_description)
switchWidget.setOnCheckedChangeListener { _, isChecked ->
onSwitchChange(isChecked)
}
iconOn = getResourceId(
R.styleable.SwitchWithDescription_switchIconOn,
DEFAULT_DRAWABLE,
)
trackingProtectionCategoryTitle.text = resources.getString(
iconOff = getResourceId(
R.styleable.SwitchWithDescription_switchIconOff,
DEFAULT_DRAWABLE,
)
shouldShowIcons = getBoolean(
R.styleable.SwitchWithDescription_switchShowIcon,
true,
)
descriptionOn = resources.getString(
getResourceId(
R.styleable.SwitchWithDescription_switchDescriptionOn,
R.string.empty_string,
),
)
descriptionOff = resources.getString(
getResourceId(
R.styleable.SwitchWithDescription_switchDescriptionOff,
R.string.empty_string,
),
)
switchWidget.textOn = descriptionOn
switchWidget.textOff = descriptionOff
titleWidget.text = resources.getString(
getResourceId(
R.styleable.SwitchWithDescription_switchTitle,
R.string.preference_enhanced_tracking_protection,
),
)
trackingProtectionCategoryItemDescription.text = resources.getString(
getResourceId(
R.styleable.SwitchWithDescription_switchDescription,
R.string.preference_enhanced_tracking_protection_explanation,
R.string.empty_string,
),
)
if (shouldShowIcons) {
switchWidget.putCompoundDrawablesRelativeWithIntrinsicBounds(
start = AppCompatResources.getDrawable(context, iconOn),
)
}
}
}
/**
* Add a [CompoundButton.OnCheckedChangeListener] listener to the switch view.
*/
fun setOnCheckedChangeListener(listener: CompoundButton.OnCheckedChangeListener) {
switchWidget.setOnCheckedChangeListener { item, isChecked ->
onSwitchChange(isChecked)
listener.onCheckedChanged(item, isChecked)
}
}
/**
* Allows to query switch view isChecked state.
*/
var isChecked: Boolean
get() = switchWidget.isChecked
set(value) {
switchWidget.isChecked = value
onSwitchChange(value)
}
@VisibleForTesting
internal fun onSwitchChange(isChecked: Boolean) {
val newDescription = if (isChecked) descriptionOn else descriptionOff
val newIcon = if (isChecked) iconOn else iconOff
if (shouldShowIcons) {
switchWidget.putCompoundDrawablesRelativeWithIntrinsicBounds(
start = AppCompatResources.getDrawable(context, newIcon),
)
}
descriptionWidget.text = newDescription
switchWidget.jumpDrawablesToCurrentState()
}
}

View File

@ -23,9 +23,10 @@ import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import mozilla.components.browser.state.selector.findTabOrCustomTab
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.store.BrowserStore
@ -65,7 +66,7 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), UserInt
}
@VisibleForTesting
internal lateinit var trackingProtectionStore: TrackingProtectionStore
internal lateinit var protectionsStore: ProtectionsStore
private lateinit var trackingProtectionView: TrackingProtectionPanelView
private lateinit var trackingProtectionInteractor: TrackingProtectionPanelInteractor
private lateinit var trackingProtectionUseCases: TrackingProtectionUseCases
@ -84,14 +85,15 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), UserInt
val view = inflateRootView(container)
val tab = store.state.findTabOrCustomTab(provideCurrentTabId())
trackingProtectionStore = StoreProvider.get(this) {
TrackingProtectionStore(
TrackingProtectionState(
protectionsStore = StoreProvider.get(this) {
ProtectionsStore(
ProtectionsState(
tab = tab,
url = args.url,
isTrackingProtectionEnabled = args.trackingProtectionEnabled,
isCookieBannerHandlingEnabled = args.cookieBannerHandlingEnabled,
listTrackers = listOf(),
mode = TrackingProtectionState.Mode.Normal,
mode = ProtectionsState.Mode.Normal,
lastAccessedCategory = "",
),
)
@ -99,7 +101,9 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), UserInt
trackingProtectionInteractor = TrackingProtectionPanelInteractor(
context = requireContext(),
fragment = this,
store = trackingProtectionStore,
store = protectionsStore,
ioScope = viewLifecycleOwner.lifecycleScope + Dispatchers.IO,
cookieBannersStorage = requireComponents.core.cookieBannersStorage,
navController = { findNavController() },
openTrackingProtectionSettings = ::openTrackingProtectionSettings,
openLearnMoreLink = ::handleLearnMoreClicked,
@ -119,7 +123,7 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), UserInt
trackingProtectionUseCases.fetchTrackingLogs(
tab.id,
onSuccess = {
trackingProtectionStore.dispatch(TrackingProtectionAction.TrackerLogChange(it))
protectionsStore.dispatch(ProtectionsAction.TrackerLogChange(it))
},
onError = {
Logger.error("TrackingProtectionUseCases - fetchTrackingLogs onError", it)
@ -133,7 +137,7 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), UserInt
observeUrlChange(store)
observeTrackersChange(store)
trackingProtectionStore.observe(view) {
protectionsStore.observe(view) {
viewLifecycleOwner.lifecycleScope.launch {
whenStarted {
trackingProtectionView.update(it)
@ -217,7 +221,7 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), UserInt
state.findTabOrCustomTab(provideCurrentTabId())
}.ifChanged { tab -> tab.content.url }
.collect {
trackingProtectionStore.dispatch(TrackingProtectionAction.UrlChange(it.content.url))
protectionsStore.dispatch(ProtectionsAction.UrlChange(it.content.url))
}
}
}

View File

@ -7,7 +7,12 @@ package org.mozilla.fenix.trackingprotection
import android.content.Context
import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.browser.state.state.SessionState
import mozilla.components.concept.engine.cookiehandling.CookieBannersStorage
import mozilla.components.concept.engine.permission.SitePermissions
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.ext.components
@ -21,7 +26,9 @@ import org.mozilla.fenix.ext.runIfFragmentIsAttached
class TrackingProtectionPanelInteractor(
private val context: Context,
private val fragment: Fragment,
private val store: TrackingProtectionStore,
private val store: ProtectionsStore,
private val ioScope: CoroutineScope,
private val cookieBannersStorage: CookieBannersStorage,
private val navController: () -> NavController,
private val openTrackingProtectionSettings: () -> Unit,
private val openLearnMoreLink: () -> Unit,
@ -31,7 +38,7 @@ class TrackingProtectionPanelInteractor(
) : TrackingProtectionPanelViewInteractor {
override fun openDetails(category: TrackingProtectionCategory, categoryBlocked: Boolean) {
store.dispatch(TrackingProtectionAction.EnterDetailsMode(category, categoryBlocked))
store.dispatch(ProtectionsAction.EnterDetailsMode(category, categoryBlocked))
}
override fun onLearnMoreClicked() {
@ -45,28 +52,36 @@ class TrackingProtectionPanelInteractor(
override fun onBackPressed() {
getCurrentTab()?.let { tab ->
context.components.useCases.trackingProtectionUseCases.containsException(tab.id) { contains ->
fragment.runIfFragmentIsAttached {
navController().popBackStack()
val isTrackingProtectionEnabled = tab.trackingProtection.enabled && !contains
val directions =
BrowserFragmentDirections.actionGlobalQuickSettingsSheetDialogFragment(
sessionId = tab.id,
url = tab.content.url,
title = tab.content.title,
isSecured = tab.content.securityInfo.secure,
sitePermissions = sitePermissions,
gravity = gravity,
certificateName = tab.content.securityInfo.issuer,
permissionHighlights = tab.content.permissionHighlights,
isTrackingProtectionEnabled = isTrackingProtectionEnabled,
)
navController().navigate(directions)
ioScope.launch {
val hasException =
cookieBannersStorage.hasException(tab.content.url, tab.content.private)
withContext(Dispatchers.Main) {
fragment.runIfFragmentIsAttached {
navController().popBackStack()
val isTrackingProtectionEnabled =
tab.trackingProtection.enabled && !contains
val directions =
BrowserFragmentDirections.actionGlobalQuickSettingsSheetDialogFragment(
sessionId = tab.id,
url = tab.content.url,
title = tab.content.title,
isSecured = tab.content.securityInfo.secure,
sitePermissions = sitePermissions,
gravity = gravity,
certificateName = tab.content.securityInfo.issuer,
permissionHighlights = tab.content.permissionHighlights,
isTrackingProtectionEnabled = isTrackingProtectionEnabled,
isCookieHandlingEnabled = !hasException,
)
navController().navigate(directions)
}
}
}
}
}
}
override fun onExitDetailMode() {
store.dispatch(TrackingProtectionAction.ExitDetailsMode)
store.dispatch(ProtectionsAction.ExitDetailsMode)
}
}

View File

@ -84,7 +84,7 @@ class TrackingProtectionPanelView(
val view: ConstraintLayout = binding.panelWrapper
private var mode: TrackingProtectionState.Mode = TrackingProtectionState.Mode.Normal
private var mode: ProtectionsState.Mode = ProtectionsState.Mode.Normal
private var bucketedTrackers = TrackerBuckets()
@ -106,13 +106,16 @@ class TrackingProtectionPanelView(
setCategoryClickListeners()
}
fun update(state: TrackingProtectionState) {
/**
* Updates the display mode of the Protection view.
*/
fun update(state: ProtectionsState) {
mode = state.mode
bucketedTrackers.updateIfNeeded(state.listTrackers)
when (val mode = state.mode) {
is TrackingProtectionState.Mode.Normal -> setUIForNormalMode(state)
is TrackingProtectionState.Mode.Details -> setUIForDetailsMode(
is ProtectionsState.Mode.Normal -> setUIForNormalMode(state)
is ProtectionsState.Mode.Details -> setUIForDetailsMode(
mode.selectedCategory,
mode.categoryBlocked,
)
@ -121,7 +124,7 @@ class TrackingProtectionPanelView(
setAccessibilityViewHierarchy(binding.detailsBack, binding.categoryTitle)
}
private fun setUIForNormalMode(state: TrackingProtectionState) {
private fun setUIForNormalMode(state: ProtectionsState) {
binding.detailsMode.visibility = View.GONE
binding.normalMode.visibility = View.VISIBLE
@ -280,8 +283,8 @@ class TrackingProtectionPanelView(
fun onBackPressed(): Boolean {
return when (mode) {
is TrackingProtectionState.Mode.Details -> {
mode = TrackingProtectionState.Mode.Normal
is ProtectionsState.Mode.Details -> {
mode = ProtectionsState.Mode.Normal
interactor.onBackPressed()
true
}

View File

@ -4,12 +4,12 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="16"
android:viewportHeight="16">
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/textPrimary"
android:pathData="M15.379,8a0.142,0.142 0,0 0,0.02 0,7.978 7.978,0 0,1 -1.858,-0.356 0.981,0.981 0,0 1,-0.054 0.847,1 1,0 1,1 -1.735,-0.994 0.981,0.981 0,0 1,0.481 -0.407c-0.069,-0.036 -0.13,-0.083 -0.2,-0.121l-5.42,5.418a0.977,0.977 0,0 1,-0.4 0.476,0.85 0.85,0 0,1 -0.117,0.04l-2.065,2.066c0.509,0.219 1,0.24 1.161,-0.147 -0.424,1.025 2.823,1.668 2.822,0.558 0,1.11 3.246,0.461 2.821,-0.564 0.425,1.025 3.175,-0.816 2.39,-1.6 0.785,0.784 2.621,-1.97 1.6,-2.394 1.021,0.424 1.664,-2.822 0.554,-2.822zM10.306,13a1,1 0,1 1,1 -1,1 1,0 0,1 -1,1zM14.707,1.293a1,1 0,0 0,-1.414 0L9.679,4.907A7.942,7.942 0,0 1,8 0.61v0.025C8,-0.474 4.753,0.174 5.179,1.2 4.753,0.174 2,2.016 2.788,2.8 2,2.016 0.167,4.77 1.193,5.193 0.167,4.77 -0.476,8.016 0.634,8.015c-1.11,0 -0.461,3.247 0.564,2.821 -0.639,0.265 -0.163,1.428 0.475,2.077l-0.38,0.38a1,1 0,1 0,1.414 1.414l12,-12a1,1 0,0 0,0 -1.414zM5.707,2.993a1,1 0,1 1,-1 1A1,1 0,0 1,5.706 3zM2.524,7.508a1,1 0,1 1,0.37 1.364,1 1,0 0,1 -0.37,-1.364zM7.293,7.293z"/>
android:pathData="M7,6V6.404L5.558,4.962C6.903,3.723 8.601,2.861 10.489,2.56L11.054,3.058C11.022,3.344 10.999,3.633 10.999,3.928C10.999,7.474 13.307,10.477 16.5,11.529V12.751L17,13.251H18.5L19,12.751V11.934H19.006C19.649,11.934 20.264,11.833 20.862,11.689L21.492,12.244C21.416,14.61 20.47,16.752 18.972,18.376L13.25,12.654V11.25L12.75,10.75H11.346L8.596,8H9L9.5,7.5V6L9,5.5H7.5L7,6ZM18.159,21.78C18.305,21.926 18.497,22 18.689,22C18.881,22 19.073,21.927 19.219,21.78C19.512,21.487 19.512,21.012 19.219,20.719L3.134,4.634C2.841,4.341 2.366,4.341 2.073,4.634C1.78,4.927 1.78,5.402 2.073,5.695L3.697,7.319C2.931,8.684 2.492,10.256 2.492,11.934C2.492,17.185 6.749,21.442 12,21.442C13.679,21.442 15.253,21.005 16.619,20.241L18.159,21.78ZM5,12.75V11.25L5.5,10.75H7L7.5,11.25V12.75L7,13.25H5.5L5,12.75ZM9.5,18L9,18.5H7.5L7,18V16.5L7.5,16H9L9.5,16.5V18Z"/>
</vector>

View File

@ -4,12 +4,12 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="16"
android:viewportHeight="16">
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/textPrimary"
android:pathData="M15.379,8a0.142,0.142 0,0 0,0.02 0,7.978 7.978,0 0,1 -1.858,-0.356 0.981,0.981 0,0 1,-0.054 0.847,1 1,0 1,1 -1.735,-0.994 0.981,0.981 0,0 1,0.481 -0.407A8.02,8.02 0,0 1,8 0.61v0.025C8,-0.474 4.753,0.174 5.179,1.2 4.753,0.174 2,2.016 2.788,2.8 2,2.016 0.167,4.77 1.193,5.193 0.167,4.77 -0.476,8.016 0.634,8.015c-1.11,0 -0.461,3.247 0.564,2.821 -1.025,0.426 0.817,3.175 1.6,2.39 -0.784,0.785 1.969,2.621 2.393,1.6 -0.424,1.025 2.823,1.668 2.822,0.558 0,1.11 3.246,0.461 2.821,-0.564 0.425,1.025 3.175,-0.816 2.39,-1.6 0.785,0.784 2.621,-1.97 1.6,-2.394 1.022,0.42 1.665,-2.826 0.555,-2.826zM4.259,8.5a1,1 0,1 1,-0.37 -1.365,1 1,0 0,1 0.37,1.365zM6.214,12.861a1,1 0,1 1,0.36 -1.367,1 1,0 0,1 -0.36,1.369zM5.706,5a1,1 0,1 1,1 -1,1 1,0 0,1 -1,1zM8,9a1,1 0,1 1,1 -1,1 1,0 0,1 -1,1zM10.306,13a1,1 0,1 1,1 -1,1 1,0 0,1 -1,1z" />
android:pathData="M20.862,11.69a7.886,7.886 0,0 1,-1.856 0.244L19,11.934v0.817l-0.5,0.5L17,13.251l-0.5,-0.5L16.5,11.53c-3.193,-1.052 -5.501,-4.055 -5.501,-7.6 0,-0.296 0.023,-0.585 0.055,-0.87l-0.565,-0.499c-4.531,0.725 -7.997,4.64 -7.997,9.375A9.508,9.508 0,0 0,12 21.443c5.146,0 9.327,-4.09 9.492,-9.198l-0.63,-0.556ZM5,12.75v-1.5l0.5,-0.5L7,10.75l0.5,0.5v1.5l-0.5,0.5L5.5,13.25l-0.5,-0.5ZM9.5,18l-0.5,0.5L7.5,18.5L7,18v-1.5l0.5,-0.5L9,16l0.5,0.5L9.5,18ZM9.5,7.5L9,8L7.5,8L7,7.5L7,6l0.5,-0.5L9,5.5l0.5,0.5v1.5ZM13.25,12.75 L12.75,13.25h-1.5l-0.5,-0.5v-1.5l0.5,-0.5h1.5l0.5,0.5v1.5ZM17,18l-0.5,0.5L15,18.5l-0.5,-0.5v-1.5l0.5,-0.5h1.5l0.5,0.5L17,18Z" />
</vector>

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/enabled"
android:drawable="@drawable/ic_tracking_protection_enabled"
android:state_checked="true" />
<item
android:id="@+id/disabled"
android:drawable="@drawable/ic_tracking_protection_disabled" />
</animated-selector>

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/panel_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/layer1">
<ImageView
android:id="@+id/navigate_back"
android:layout_width="@dimen/tracking_protection_item_height"
android:layout_height="@dimen/tracking_protection_item_height"
android:contentDescription="@string/etp_back_button_content_description"
android:scaleType="center"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/mozac_ic_back"
app:tint="?attr/textPrimary" />
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:clickable="false"
android:focusable="false"
android:paddingHorizontal="16dp"
android:paddingTop="16dp"
android:textColor="?attr/textPrimary"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/navigate_back"
app:layout_constraintTop_toTopOf="parent"
tools:text="Turn off Cookize Banner Reduction for [domain.com]? " />
<TextView
android:id="@+id/details"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:textColor="?attr/textSecondary"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/navigate_back"
app:layout_constraintTop_toBottomOf="@id/title"
tools:text="Firefox will clear this sites cookies and refresh the page. Clearing all cookies may sign you out or empty shopping carts." />
<org.mozilla.fenix.trackingprotection.SwitchWithDescription
android:id="@+id/cookieBannerSwitch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/tracking_protection_item_height"
android:paddingHorizontal="16dp"
android:paddingTop="23dp"
app:layout_constraintStart_toEndOf="@+id/navigate_back"
app:layout_constraintTop_toBottomOf="@id/details"
app:switchDescriptionOff="@string/reduce_cookie_banner_off_for_site"
app:switchDescriptionOn="@string/reduce_cookie_banner_on_for_site"
app:switchShowIcon="false"
app:switchTitle="@string/preferences_cookie_banner_reduction" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/quick_settings_sheet"
android:fillViewport="true">
<FrameLayout
android:id="@+id/cookieBannerDetailsInfoLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</androidx.core.widget.NestedScrollView>

View File

@ -1,24 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
<?xml version="1.0" encoding="utf-8"?><!-- 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/. -->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.compose.ui.platform.ComposeView
android:id="@+id/cookieBannerItem"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/tracking_protection_item_height"
app:layout_constraintBottom_toTopOf="@id/trackingProtectionSwitch"
app:layout_constraintTop_toTopOf="parent" />
<org.mozilla.fenix.trackingprotection.SwitchWithDescription
android:id="@+id/trackingProtectionSwitch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:minHeight="@dimen/tracking_protection_item_height"
android:text="@string/preference_enhanced_tracking_protection"
app:layout_constraintBottom_toTopOf="@id/trackingProtectionDetails"
app:layout_constraintTop_toTopOf="parent"
app:switchDescription="@string/etp_panel_on"
app:switchIcon="@drawable/ic_tracking_protection"
app:layout_constraintTop_toBottomOf="@id/cookieBannerItem"
app:switchDescriptionOff="@string/etp_panel_off"
app:switchDescriptionOn="@string/etp_panel_on"
app:switchIconOff="@drawable/ic_tracking_protection_disabled"
app:switchIconOn="@drawable/ic_tracking_protection_enabled"
app:switchTitle="@string/preference_enhanced_tracking_protection" />
<TextView
@ -26,8 +35,8 @@
style="@style/QuickSettingsText.Icon"
android:layout_width="0dp"
android:layout_height="@dimen/quicksettings_item_height"
android:gravity="end|center_vertical"
android:layout_alignParentEnd="true"
android:gravity="end|center_vertical"
android:text="@string/enhanced_tracking_protection_details"
android:visibility="gone"
app:drawableEndCompat="@drawable/ic_arrowhead_right"

View File

@ -10,7 +10,7 @@
<TextView
android:layout_marginTop="4dp"
android:id="@+id/trackingProtectionCategoryTitle"
android:id="@+id/switch_with_description_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="48dp"
@ -20,7 +20,7 @@
android:importantForAccessibility="no"
android:textAppearance="@style/ListItemTextStyle"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@id/trackingProtectionCategoryItemDescription"
app:layout_constraintBottom_toTopOf="@id/switch_with_description_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
@ -29,7 +29,7 @@
tools:text="@tools:sample/lorem" />
<TextView
android:id="@+id/trackingProtectionCategoryItemDescription"
android:id="@+id/switch_with_description_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:clickable="false"
@ -38,10 +38,10 @@
android:textColor="?attr/textSecondary"
android:layout_marginBottom="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/trackingProtectionCategoryTitle"
app:layout_constraintEnd_toEndOf="@id/switch_with_description_title"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="@id/trackingProtectionCategoryTitle"
app:layout_constraintTop_toBottomOf="@id/trackingProtectionCategoryTitle"
app:layout_constraintStart_toStartOf="@id/switch_with_description_title"
app:layout_constraintTop_toBottomOf="@id/switch_with_description_title"
app:layout_constraintVertical_chainStyle="packed"
tools:text="@tools:sample/lorem" />
@ -51,10 +51,7 @@
android:minHeight="@dimen/tracking_protection_item_height"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textOff="@string/etp_panel_off"
android:textOn="@string/etp_panel_on"
app:drawableStartCompat="@drawable/ic_tracking_protection"
app:layout_constraintBottom_toBottomOf="@id/trackingProtectionCategoryItemDescription"
app:layout_constraintBottom_toBottomOf="@id/switch_with_description_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -130,6 +130,9 @@
<action
android:id="@+id/action_global_trackingProtectionPanelDialogFragment"
app:destination="@id/trackingProtectionPanelDialogFragment" />
<action
android:id="@+id/action_global_cookieBannerProtectionPanelDialogFragment"
app:destination="@id/cookieBannerPanelDialogFragment" />
<action
android:id="@+id/action_global_quickSettingsSheetDialogFragment"
app:destination="@id/quickSettingsSheetDialogFragment" />
@ -953,6 +956,9 @@
<argument
android:name="isTrackingProtectionEnabled"
app:argType="boolean" />
<argument
android:name="isCookieHandlingEnabled"
app:argType="boolean" />
</dialog>
<fragment
android:id="@+id/accountProblemFragment"
@ -978,6 +984,9 @@
<argument
android:name="trackingProtectionEnabled"
app:argType="boolean" />
<argument
android:name="cookieBannerHandlingEnabled"
app:argType="boolean" />
<argument
android:name="gravity"
android:defaultValue="80"
@ -1016,6 +1025,33 @@
android:defaultValue="80"
app:argType="integer" />
</dialog>
<dialog
android:id="@+id/cookieBannerPanelDialogFragment"
android:name="org.mozilla.fenix.settings.quicksettings.protections.cookiebanners.CookieBannerPanelDialogFragment"
tools:layout="@layout/quicksettings_website_info">
<argument
android:name="sessionId"
app:argType="string" />
<argument
android:name="url"
app:argType="string" />
<argument
android:name="trackingProtectionEnabled"
app:argType="boolean" />
<argument
android:name="cookieBannerHandlingEnabled"
app:argType="boolean" />
<argument
android:name="gravity"
android:defaultValue="80"
app:argType="integer" />
<argument
android:name="sitePermissions"
app:argType="mozilla.components.concept.engine.permission.SitePermissions"
app:nullable="true" />
</dialog>
<fragment
android:id="@+id/trackingProtectionBlockingFragment"
android:name="org.mozilla.fenix.trackingprotection.TrackingProtectionBlockingFragment"

View File

@ -90,8 +90,11 @@
<declare-styleable name="SwitchWithDescription">
<attr name="switchTitle" format="reference" />
<attr name="switchDescription" format="reference" />
<attr name="switchIcon" format="reference" />
<attr name="switchDescriptionOn" format="reference" />
<attr name="switchDescriptionOff" format="reference" />
<attr name="switchIconOn" format="reference" />
<attr name="switchIconOff" format="reference" />
<attr name="switchShowIcon" format="boolean" />
</declare-styleable>
<declare-styleable name="DeleteBrowsingDataItem">

View File

@ -329,6 +329,18 @@
<string name="reduce_cookie_banner_option">Reduce cookie banners</string>
<!-- Summary for the preference for rejecting all cookies whenever possible. -->
<string name="reduce_cookie_banner_summary">Firefox automatically tries to reject cookie requests on cookie banners. If a reject option isnt available, Firefox may accept all cookies to dismiss the banner.</string>
<!-- Text for indicating cookie banner handling is off this site, this is shown as part of the protections panel with the tracking protection toggle -->
<string name="reduce_cookie_banner_off_for_site">Off for this site</string>
<!-- Text for indicating cookie banner handling is on this site, this is shown as part of the protections panel with the tracking protection toggle -->
<string name="reduce_cookie_banner_on_for_site">On for this site</string>
<!-- Title text for a detail explanation indicating cookie banner handling is on this site, this is shown as part of the cookie banner panel in the toolbar. The first parameter is a shortened URL of the current site-->
<string name="reduce_cookie_banner_details_panel_title_on_for_site">Turn on Cookie Banner Reduction for %1$s?</string>
<!-- Title text for a detail explanation indicating cookie banner handling is off this site, this is shown as part of the cookie banner panel in the toolbar. The first parameter is a shortened URL of the current site-->
<string name="reduce_cookie_banner_details_panel_title_off_for_site">Turn off Cookie Banner Reduction for %1$s?</string>
<!-- Long text for a detail explanation indicating what will happen if cookie banner handling is off for a site, this is shown as part of the cookie banner panel in the toolbar. The first parameter is the application name -->
<string name="reduce_cookie_banner_details_panel_description_off_for_site">%1$s will clear this sites cookies and refresh the page. Clearing all cookies may sign you out or empty shopping carts.</string>
<!-- Long text for a detail explanation indicating what will happen if cookie banner handling is on for a site, this is shown as part of the cookie banner panel in the toolbar -->
<string name="reduce_cookie_banner_details_panel_description_on_for_site">Firefox can try to automatically reject cookie requests. If a reject option isnt available, Firefox may accept all cookies to dismiss the banner.</string>
<!-- Description of the preference to enable "HTTPS-Only" mode. -->
<string name="preferences_https_only_summary">Automatically attempts to connect to sites using HTTPS encryption protocol for increased security.</string>

View File

@ -17,10 +17,13 @@ import io.mockk.spyk
import io.mockk.verify
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.createTab
import mozilla.components.concept.engine.cookiehandling.CookieBannersStorage
import mozilla.components.concept.engine.permission.SitePermissions
import mozilla.components.feature.session.TrackingProtectionUseCases
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.ext.components
@ -40,12 +43,19 @@ class DefaultConnectionDetailsControllerTest {
@MockK(relaxed = true)
private lateinit var sitePermissions: SitePermissions
@MockK(relaxed = true)
private lateinit var cookieBannersStorage: CookieBannersStorage
private lateinit var controller: DefaultConnectionDetailsController
private lateinit var tab: TabSessionState
private var gravity = 54
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
private val scope = coroutinesTestRule.scope
@Before
fun setUp() {
MockKAnnotations.init(this)
@ -55,6 +65,8 @@ class DefaultConnectionDetailsControllerTest {
controller = DefaultConnectionDetailsController(
fragment = fragment,
context = context,
ioScope = scope,
cookieBannersStorage = cookieBannersStorage,
navController = { navController },
sitePermissions = sitePermissions,
gravity = gravity,

View File

@ -39,6 +39,7 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.GleanMetrics.CookieBanners
import org.mozilla.fenix.GleanMetrics.TrackingProtection
import org.mozilla.fenix.components.PermissionStorage
import org.mozilla.fenix.ext.components
@ -303,6 +304,23 @@ class DefaultQuickSettingsControllerTest {
}
}
@Test
fun `handleCookieBannerHandlingDetailsClicked should call popBackStack and navigate to details page`() {
every { context.components.core.store } returns browserStore
every { store.state.protectionsState } returns mockk(relaxed = true)
every { context.components.settings } returns appSettings
every { context.components.settings.toolbarPosition.androidGravity } returns mockk(relaxed = true)
controller.handleCookieBannerHandlingDetailsClicked()
verify {
navController.popBackStack()
navController.navigate(any<NavDirections>())
}
assertNotNull(CookieBanners.visitedPanel.testGetValue())
}
@Test
fun `handleTrackingProtectionToggled should call the right use cases`() = runTestOnMain {
val trackingProtectionUseCases: TrackingProtectionUseCases = mockk(relaxed = true)
@ -348,11 +366,12 @@ class DefaultQuickSettingsControllerTest {
websiteUrl = tab.content.url,
sessionId = tab.id,
isTrackingProtectionEnabled = isTrackingProtectionEnabled,
isCookieHandlingEnabled = isTrackingProtectionEnabled,
)
every { store.state.trackingProtectionState } returns state
every { store.state.protectionsState } returns state
controller.handleDetailsClicked()
controller.handleTrackingProtectionDetailsClicked()
verify {
navController.popBackStack()

View File

@ -0,0 +1,141 @@
/* 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.settings.quicksettings
import android.widget.FrameLayout
import androidx.core.view.isVisible
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import io.mockk.spyk
import mozilla.components.browser.state.state.createTab
import mozilla.components.support.test.robolectric.testContext
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.databinding.QuicksettingsProtectionsPanelBinding
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.settings.quicksettings.protections.ProtectionsInteractor
import org.mozilla.fenix.settings.quicksettings.protections.ProtectionsView
import org.mozilla.fenix.trackingprotection.ProtectionsState
import org.mozilla.fenix.utils.Settings
@RunWith(FenixRobolectricTestRunner::class)
class ProtectionsViewTest {
private lateinit var view: ProtectionsView
private lateinit var binding: QuicksettingsProtectionsPanelBinding
private lateinit var interactor: ProtectionsInteractor
@MockK(relaxed = true)
private lateinit var settings: Settings
@Before
fun setup() {
MockKAnnotations.init(this)
interactor = mockk(relaxed = true)
view = spyk(ProtectionsView(FrameLayout(testContext), interactor, settings))
binding = view.binding
}
@Test
fun `WHEN updating THEN bind checkbox`() {
val websiteUrl = "https://mozilla.org"
val state = ProtectionsState(
tab = createTab(url = websiteUrl),
url = websiteUrl,
isTrackingProtectionEnabled = true,
isCookieBannerHandlingEnabled = true,
listTrackers = listOf(),
mode = ProtectionsState.Mode.Normal,
lastAccessedCategory = "",
)
every { settings.shouldUseTrackingProtection } returns true
view.update(state)
assertTrue(binding.root.isVisible)
assertTrue(binding.trackingProtectionSwitch.isChecked)
}
@Test
fun `GIVEN TP is globally off WHEN updating THEN hide the TP section`() {
val websiteUrl = "https://mozilla.org"
val state = ProtectionsState(
tab = createTab(url = websiteUrl),
url = websiteUrl,
isTrackingProtectionEnabled = true,
isCookieBannerHandlingEnabled = true,
listTrackers = listOf(),
mode = ProtectionsState.Mode.Normal,
lastAccessedCategory = "",
)
every { settings.shouldUseTrackingProtection } returns false
view.update(state)
assertFalse(binding.trackingProtectionSwitch.isVisible)
}
@Test
fun `GIVEN cookie banners handling is globally off WHEN updating THEN hide the cookie banner section`() {
val websiteUrl = "https://mozilla.org"
val state = ProtectionsState(
tab = createTab(url = websiteUrl),
url = websiteUrl,
isTrackingProtectionEnabled = true,
isCookieBannerHandlingEnabled = true,
listTrackers = listOf(),
mode = ProtectionsState.Mode.Normal,
lastAccessedCategory = "",
)
every { settings.shouldShowCookieBannerUI } returns true
every { settings.shouldUseCookieBanner } returns false
view.update(state)
assertFalse(binding.cookieBannerItem.isVisible)
}
@Test
fun `GIVEN cookie banners handling UI feature flag is off WHEN updating THEN hide the cookie banner section`() {
val websiteUrl = "https://mozilla.org"
val state = ProtectionsState(
tab = createTab(url = websiteUrl),
url = websiteUrl,
isTrackingProtectionEnabled = true,
isCookieBannerHandlingEnabled = true,
listTrackers = listOf(),
mode = ProtectionsState.Mode.Normal,
lastAccessedCategory = "",
)
every { settings.shouldShowCookieBannerUI } returns false
every { settings.shouldUseCookieBanner } returns false
view.update(state)
assertFalse(binding.cookieBannerItem.isVisible)
}
@Test
fun `WHEN updateDetailsSection is called THEN update the visibility of the section`() {
every { settings.shouldUseTrackingProtection } returns false
view.updateDetailsSection(false)
assertFalse(binding.trackingProtectionDetails.isVisible)
view.updateDetailsSection(true)
assertTrue(binding.trackingProtectionDetails.isVisible)
}
}

View File

@ -12,8 +12,8 @@ import org.junit.Assert.assertNotSame
import org.junit.Assert.assertTrue
import org.junit.Test
import org.mozilla.fenix.settings.PhoneFeature
import org.mozilla.fenix.trackingprotection.TrackingProtectionState
import org.mozilla.fenix.trackingprotection.TrackingProtectionState.Mode.Normal
import org.mozilla.fenix.trackingprotection.ProtectionsState
import org.mozilla.fenix.trackingprotection.ProtectionsState.Mode.Normal
class QuickSettingsFragmentReducerTest {
@ -30,13 +30,14 @@ class QuickSettingsFragmentReducerTest {
val map =
mapOf<PhoneFeature, WebsitePermission>(PhoneFeature.CAMERA to toggleablePermission)
val infoState = WebsiteInfoState("", "", WebsiteSecurityUiValues.SECURE, "")
val tpState = TrackingProtectionState(
val tpState = ProtectionsState(
null,
"",
false,
emptyList(),
Normal,
"",
isTrackingProtectionEnabled = false,
isCookieBannerHandlingEnabled = false,
listTrackers = emptyList(),
mode = Normal,
lastAccessedCategory = "",
)
val state = QuickSettingsFragmentState(infoState, map, tpState)
val newState = quickSettingsFragmentReducer(
@ -67,13 +68,14 @@ class QuickSettingsFragmentReducerTest {
val map =
mapOf<PhoneFeature, WebsitePermission>(PhoneFeature.AUTOPLAY to permissionPermission)
val infoState = WebsiteInfoState("", "", WebsiteSecurityUiValues.SECURE, "")
val tpState = TrackingProtectionState(
val tpState = ProtectionsState(
null,
"",
false,
emptyList(),
Normal,
"",
isTrackingProtectionEnabled = false,
isCookieBannerHandlingEnabled = false,
listTrackers = emptyList(),
mode = Normal,
lastAccessedCategory = "",
)
val state = QuickSettingsFragmentState(infoState, map, tpState)
val autoplayValue = AutoplayValue.AllowAll(
@ -92,14 +94,15 @@ class QuickSettingsFragmentReducerTest {
}
@Test
fun `TrackingProtectionAction - ToggleTrackingProtectionEnabled`() = runTest {
fun `ProtectionsAction - ToggleTrackingProtectionEnabled`() = runTest {
val state = QuickSettingsFragmentState(
webInfoState = WebsiteInfoState("", "", WebsiteSecurityUiValues.SECURE, ""),
websitePermissionsState = emptyMap(),
trackingProtectionState = TrackingProtectionState(
protectionsState = ProtectionsState(
tab = null,
url = "https://www.firefox.com",
isTrackingProtectionEnabled = true,
isCookieBannerHandlingEnabled = true,
listTrackers = listOf(),
mode = Normal,
lastAccessedCategory = "",
@ -112,7 +115,7 @@ class QuickSettingsFragmentReducerTest {
)
assertNotSame(state, newState)
assertFalse(newState.trackingProtectionState.isTrackingProtectionEnabled)
assertFalse(newState.protectionsState.isTrackingProtectionEnabled)
}
private fun createTestRule() = SitePermissionsRules(

View File

@ -39,7 +39,7 @@ import org.mozilla.fenix.settings.quicksettings.WebsiteInfoState.Companion.creat
import org.mozilla.fenix.settings.quicksettings.ext.shouldBeEnabled
import org.mozilla.fenix.settings.quicksettings.ext.shouldBeVisible
import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_BLOCK_ALL
import org.mozilla.fenix.trackingprotection.TrackingProtectionState
import org.mozilla.fenix.trackingprotection.ProtectionsState
import org.mozilla.fenix.utils.Settings
@RunWith(FenixRobolectricTestRunner::class)
@ -83,13 +83,14 @@ class QuickSettingsFragmentStoreTest {
settings = appSettings,
sessionId = tab.id,
isTrackingProtectionEnabled = true,
isCookieHandlingEnabled = true,
)
assertNotNull(store)
assertNotNull(store.state)
assertNotNull(store.state.webInfoState)
assertNotNull(store.state.websitePermissionsState)
assertNotNull(store.state.trackingProtectionState)
assertNotNull(store.state.protectionsState)
}
@Test
@ -286,7 +287,7 @@ class QuickSettingsFragmentStoreTest {
val initialState = QuickSettingsFragmentState(
webInfoState = websiteInfoState,
websitePermissionsState = initialWebsitePermissionsState,
trackingProtectionState = mockk(),
protectionsState = mockk(),
)
val store = QuickSettingsFragmentStore(initialState)
@ -340,6 +341,7 @@ class QuickSettingsFragmentStoreTest {
val tab = createTab("https://www.firefox.com")
val browserStore = BrowserStore(BrowserState(tabs = listOf(tab)))
val isTrackingProtectionEnabled = true
val isCookieHandlingEnabled = true
every { context.components.core.store } returns browserStore
@ -348,14 +350,16 @@ class QuickSettingsFragmentStoreTest {
websiteUrl = tab.content.url,
sessionId = tab.id,
isTrackingProtectionEnabled = isTrackingProtectionEnabled,
isCookieHandlingEnabled = isCookieHandlingEnabled,
)
assertNotNull(state)
assertEquals(tab, state.tab)
assertEquals(tab.content.url, state.url)
assertEquals(isTrackingProtectionEnabled, state.isTrackingProtectionEnabled)
assertEquals(isCookieHandlingEnabled, state.isCookieBannerHandlingEnabled)
assertEquals(0, state.listTrackers.size)
assertEquals(TrackingProtectionState.Mode.Normal, state.mode)
assertEquals(ProtectionsState.Mode.Normal, state.mode)
assertEquals("", state.lastAccessedCategory)
}

View File

@ -66,11 +66,20 @@ class QuickSettingsInteractorTest {
}
@Test
fun `onBlockedItemsClicked should delegate the controller`() {
interactor.onDetailsClicked()
fun `onCookieBannerHandlingClicked should delegate the controller`() {
interactor.onCookieBannerHandlingDetailsClicked()
verify {
controller.handleDetailsClicked()
controller.handleCookieBannerHandlingDetailsClicked()
}
}
@Test
fun `onBlockedItemsClicked should delegate the controller`() {
interactor.onTrackingProtectionDetailsClicked()
verify {
controller.handleTrackingProtectionDetailsClicked()
}
}

View File

@ -29,6 +29,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.settings.quicksettings.protections.ProtectionsView
@RunWith(FenixRobolectricTestRunner::class)
class QuickSettingsSheetDialogFragmentTest {
@ -107,11 +108,11 @@ class QuickSettingsSheetDialogFragmentTest {
fun `GIVEN no trackers WHEN calling updateTrackers THEN hide the details section`() {
val tab = createTab("mozilla.org")
val trackingProtectionUseCases: TrackingProtectionUseCases = mockk(relaxed = true)
val trackingProtectionView: TrackingProtectionView = mockk(relaxed = true)
val protectionsView: ProtectionsView = mockk(relaxed = true)
val onComplete = slot<(List<TrackerLog>) -> Unit>()
every { fragment.trackingProtectionView } returns trackingProtectionView
every { fragment.protectionsView } returns protectionsView
every {
trackingProtectionUseCases.fetchTrackingLogs.invoke(
@ -126,7 +127,7 @@ class QuickSettingsSheetDialogFragmentTest {
fragment.updateTrackers(tab)
verify {
trackingProtectionView.updateDetailsSection(false)
protectionsView.updateDetailsSection(false)
}
}
@ -134,11 +135,11 @@ class QuickSettingsSheetDialogFragmentTest {
fun `GIVEN trackers WHEN calling updateTrackers THEN show the details section`() {
val tab = createTab("mozilla.org")
val trackingProtectionUseCases: TrackingProtectionUseCases = mockk(relaxed = true)
val trackingProtectionView: TrackingProtectionView = mockk(relaxed = true)
val protectionsView: ProtectionsView = mockk(relaxed = true)
val onComplete = slot<(List<TrackerLog>) -> Unit>()
every { fragment.trackingProtectionView } returns trackingProtectionView
every { fragment.protectionsView } returns protectionsView
every {
trackingProtectionUseCases.fetchTrackingLogs.invoke(
@ -153,7 +154,7 @@ class QuickSettingsSheetDialogFragmentTest {
fragment.updateTrackers(tab)
verify {
trackingProtectionView.updateDetailsSection(true)
protectionsView.updateDetailsSection(true)
}
}

View File

@ -1,95 +0,0 @@
/* 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.settings.quicksettings
import android.widget.FrameLayout
import androidx.core.view.isVisible
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import io.mockk.spyk
import mozilla.components.browser.state.state.createTab
import mozilla.components.support.test.robolectric.testContext
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.databinding.QuicksettingsTrackingProtectionBinding
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.trackingprotection.TrackingProtectionState
import org.mozilla.fenix.utils.Settings
@RunWith(FenixRobolectricTestRunner::class)
class TrackingProtectionViewTest {
private lateinit var view: TrackingProtectionView
private lateinit var binding: QuicksettingsTrackingProtectionBinding
private lateinit var interactor: TrackingProtectionInteractor
@MockK(relaxed = true)
private lateinit var settings: Settings
@Before
fun setup() {
MockKAnnotations.init(this)
interactor = mockk(relaxed = true)
view = spyk(TrackingProtectionView(FrameLayout(testContext), interactor, settings))
binding = view.binding
}
@Test
fun `WHEN updating THEN bind checkbox`() {
val websiteUrl = "https://mozilla.org"
val state = TrackingProtectionState(
tab = createTab(url = websiteUrl),
url = websiteUrl,
isTrackingProtectionEnabled = true,
listTrackers = listOf(),
mode = TrackingProtectionState.Mode.Normal,
lastAccessedCategory = "",
)
every { settings.shouldUseTrackingProtection } returns true
view.update(state)
assertTrue(binding.root.isVisible)
assertTrue(binding.trackingProtectionSwitch.switchWidget.isChecked)
}
@Test
fun `GIVEN TP is globally off WHEN updating THEN hide the TP section`() {
val websiteUrl = "https://mozilla.org"
val state = TrackingProtectionState(
tab = createTab(url = websiteUrl),
url = websiteUrl,
isTrackingProtectionEnabled = true,
listTrackers = listOf(),
mode = TrackingProtectionState.Mode.Normal,
lastAccessedCategory = "",
)
every { settings.shouldUseTrackingProtection } returns false
view.update(state)
assertFalse(binding.root.isVisible)
}
@Test
fun `WHEN updateDetailsSection is called THEN update the visibility of the section`() {
every { settings.shouldUseTrackingProtection } returns false
view.updateDetailsSection(false)
assertFalse(binding.trackingProtectionDetails.isVisible)
view.updateDetailsSection(true)
assertTrue(binding.trackingProtectionDetails.isVisible)
}
}

View File

@ -0,0 +1,155 @@
/* 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.settings.quicksettings.protections.cookiebanners
import android.widget.FrameLayout
import io.mockk.MockKAnnotations
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import mozilla.components.browser.state.state.createTab
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.support.ktx.kotlin.toShortUrl
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
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.R
import org.mozilla.fenix.databinding.ComponentCookieBannerDetailsPanelBinding
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.trackingprotection.ProtectionsState
@RunWith(FenixRobolectricTestRunner::class)
class CookieBannerHandlingDetailsViewTest {
private lateinit var view: CookieBannerHandlingDetailsView
private lateinit var binding: ComponentCookieBannerDetailsPanelBinding
private lateinit var interactor: CookieBannerDetailsInteractor
@MockK(relaxed = true)
private lateinit var publicSuffixList: PublicSuffixList
@Before
fun setup() {
MockKAnnotations.init(this)
interactor = mockk(relaxed = true)
view = spyk(
CookieBannerHandlingDetailsView(
container = FrameLayout(testContext),
context = testContext,
publicSuffixList = publicSuffixList,
interactor = interactor,
),
)
binding = view.binding
}
@Test
fun `WHEN updating THEN bind title,back button, description and switch`() {
val websiteUrl = "https://mozilla.org"
val state = ProtectionsState(
tab = createTab(url = websiteUrl),
url = websiteUrl,
isTrackingProtectionEnabled = true,
isCookieBannerHandlingEnabled = true,
listTrackers = listOf(),
mode = ProtectionsState.Mode.Normal,
lastAccessedCategory = "",
)
view.update(state)
verify {
view.bindTitle(state.url, state.isCookieBannerHandlingEnabled)
view.bindBackButtonListener()
view.bindDescription(state.isCookieBannerHandlingEnabled)
view.bindSwitch(state.isCookieBannerHandlingEnabled)
}
}
@Test
fun `GIVEN cookie banner handling is enabled WHEN biding title THEN title view must have the expected string`() {
val websiteUrl = "https://mozilla.org"
view.bindTitle(url = websiteUrl, isCookieBannerHandlingEnabled = true)
val expectedText =
testContext.getString(
R.string.reduce_cookie_banner_details_panel_title_off_for_site,
websiteUrl.toShortUrl(publicSuffixList),
)
assertEquals(expectedText, view.binding.title.text)
}
@Test
fun `GIVEN cookie banner handling is disabled WHEN biding title THEN title view must have the expected string`() {
val websiteUrl = "https://mozilla.org"
view.bindTitle(url = websiteUrl, isCookieBannerHandlingEnabled = false)
val expectedText =
testContext.getString(
R.string.reduce_cookie_banner_details_panel_title_on_for_site,
websiteUrl.toShortUrl(publicSuffixList),
)
assertEquals(expectedText, view.binding.title.text)
}
@Test
fun `WHEN clicking the back button THEN view must delegate to the interactor#onBackPressed()`() {
view.bindBackButtonListener()
view.binding.navigateBack.performClick()
verify {
interactor.onBackPressed()
}
}
@Test
fun `GIVEN cookie banner handling is enabled WHEN biding description THEN description view must have the expected string`() {
view.bindDescription(isCookieBannerHandlingEnabled = true)
val expectedText =
testContext.getString(
R.string.reduce_cookie_banner_details_panel_description_off_for_site,
testContext.getString(R.string.app_name),
)
assertEquals(expectedText, view.binding.details.text)
}
@Test
fun `GIVEN cookie banner handling is disabled WHEN biding description THEN description view must have the expected string`() {
view.bindDescription(isCookieBannerHandlingEnabled = false)
val expectedText =
testContext.getString(
R.string.reduce_cookie_banner_details_panel_description_on_for_site,
)
assertEquals(expectedText, view.binding.details.text)
}
@Test
fun `GIVEN cookie banner handling is disabled WHEN biding switch THEN switch view must have the expected isChecked status`() {
view.bindSwitch(isCookieBannerHandlingEnabled = false)
assertFalse(view.binding.cookieBannerSwitch.isChecked)
}
@Test
fun `GIVEN cookie banner handling is enabled WHEN biding switch THEN switch view must have the expected isChecked status`() {
view.bindSwitch(isCookieBannerHandlingEnabled = true)
assertTrue(view.binding.cookieBannerSwitch.isChecked)
}
}

View File

@ -0,0 +1,182 @@
/* 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.settings.quicksettings.protections.cookiebanners
import android.content.Context
import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import io.mockk.MockKAnnotations
import io.mockk.coVerifyOrder
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import io.mockk.slot
import io.mockk.spyk
import io.mockk.verify
import kotlinx.coroutines.test.advanceUntilIdle
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.cookiehandling.CookieBannersStorage
import mozilla.components.concept.engine.permission.SitePermissions
import mozilla.components.feature.session.SessionUseCases
import mozilla.components.feature.session.TrackingProtectionUseCases
import mozilla.components.service.glean.testing.GleanTestRule
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.rule.MainCoroutineRule
import mozilla.components.support.test.rule.runTestOnMain
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.GleanMetrics.CookieBanners
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.trackingprotection.ProtectionsAction
import org.mozilla.fenix.trackingprotection.ProtectionsStore
@RunWith(FenixRobolectricTestRunner::class)
internal class DefaultCookieBannerDetailsControllerTest {
private lateinit var context: Context
@MockK(relaxed = true)
private lateinit var navController: NavController
@MockK(relaxed = true)
private lateinit var fragment: Fragment
@MockK(relaxed = true)
private lateinit var sitePermissions: SitePermissions
@MockK(relaxed = true)
private lateinit var cookieBannersStorage: CookieBannersStorage
private lateinit var controller: DefaultCookieBannerDetailsController
private lateinit var tab: TabSessionState
private lateinit var browserStore: BrowserStore
@MockK(relaxed = true)
private lateinit var protectionsStore: ProtectionsStore
@MockK(relaxed = true)
private lateinit var reload: SessionUseCases.ReloadUrlUseCase
private var gravity = 54
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
private val scope = coroutinesTestRule.scope
@get:Rule
val gleanRule = GleanTestRule(testContext)
@Before
fun setUp() {
MockKAnnotations.init(this)
val trackingProtectionUseCases: TrackingProtectionUseCases = mockk(relaxed = true)
context = spyk(testContext)
tab = createTab("https://mozilla.org")
browserStore = BrowserStore(BrowserState(tabs = listOf(tab)))
controller = DefaultCookieBannerDetailsController(
fragment = fragment,
context = context,
ioScope = scope,
cookieBannersStorage = cookieBannersStorage,
navController = { navController },
sitePermissions = sitePermissions,
gravity = gravity,
getCurrentTab = { tab },
sessionId = tab.id,
browserStore = browserStore,
protectionsStore = protectionsStore,
reload = reload,
)
every { fragment.context } returns context
every { context.components.useCases.trackingProtectionUseCases } returns trackingProtectionUseCases
val onComplete = slot<(Boolean) -> Unit>()
every {
trackingProtectionUseCases.containsException.invoke(
any(),
capture(onComplete),
)
}.answers { onComplete.captured.invoke(true) }
}
@Test
fun `WHEN handleBackPressed is called THEN should call popBackStack and navigate`() {
controller.handleBackPressed()
verify {
navController.popBackStack()
navController.navigate(any<NavDirections>())
}
}
@Test
fun `GIVEN cookie banner is enabled WHEN handleTogglePressed THEN remove from the storage, send telemetry and reload the tab`() =
runTestOnMain {
val isEnabled = true
assertNull(CookieBanners.exceptionRemoved.testGetValue())
every { protectionsStore.dispatch(any()) } returns mockk()
controller.handleTogglePressed(isEnabled)
advanceUntilIdle()
coVerifyOrder {
cookieBannersStorage.removeException(
uri = tab.content.url,
privateBrowsing = tab.content.private,
)
protectionsStore.dispatch(
ProtectionsAction.ToggleCookieBannerHandlingProtectionEnabled(
isEnabled,
),
)
reload(tab.id)
}
assertNotNull(CookieBanners.exceptionRemoved.testGetValue())
}
@Test
fun `GIVEN cookie banner is disabled WHEN handleTogglePressed THEN remove from the storage, send telemetry and reload the tab`() =
runTestOnMain {
val isEnabled = false
assertNull(CookieBanners.exceptionRemoved.testGetValue())
every { protectionsStore.dispatch(any()) } returns mockk()
controller.handleTogglePressed(isEnabled)
advanceUntilIdle()
coVerifyOrder {
cookieBannersStorage.addException(
uri = tab.content.url,
privateBrowsing = tab.content.private,
)
protectionsStore.dispatch(
ProtectionsAction.ToggleCookieBannerHandlingProtectionEnabled(
isEnabled,
),
)
reload(tab.id)
}
assertNotNull(CookieBanners.exceptionAdded.testGetValue())
}
}

View File

@ -0,0 +1,42 @@
/* 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.settings.quicksettings.protections.cookiebanners
import io.mockk.mockk
import io.mockk.verify
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class)
class DefaultCookieBannerDetailsInteractorTest {
private lateinit var controller: CookieBannerDetailsController
private lateinit var interactor: DefaultCookieBannerDetailsInteractor
@Before
fun setUp() {
controller = mockk(relaxed = true)
interactor = DefaultCookieBannerDetailsInteractor(controller)
}
@Test
fun `WHEN onBackPressed is called THEN delegate the controller`() {
interactor.onBackPressed()
verify {
controller.handleBackPressed()
}
}
@Test
fun `WHEN onTogglePressed is called THEN delegate the controller`() {
interactor.onTogglePressed(true)
verify {
controller.handleTogglePressed(true)
}
}
}

View File

@ -10,19 +10,20 @@ import mozilla.components.browser.state.state.SessionState
import mozilla.components.concept.engine.content.blocking.TrackerLog
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotSame
import org.junit.Assert.assertTrue
import org.junit.Test
class TrackingProtectionStoreTest {
class ProtectionsStoreTest {
val tab: SessionState = mockk(relaxed = true)
@Test
fun enterDetailsMode() = runTest {
val initialState = defaultState()
val store = TrackingProtectionStore(initialState)
val store = ProtectionsStore(initialState)
store.dispatch(
TrackingProtectionAction.EnterDetailsMode(
ProtectionsAction.EnterDetailsMode(
TrackingProtectionCategory.FINGERPRINTERS,
true,
),
@ -31,7 +32,7 @@ class TrackingProtectionStoreTest {
assertNotSame(initialState, store.state)
assertEquals(
store.state.mode,
TrackingProtectionState.Mode.Details(TrackingProtectionCategory.FINGERPRINTERS, true),
ProtectionsState.Mode.Details(TrackingProtectionCategory.FINGERPRINTERS, true),
)
assertEquals(store.state.lastAccessedCategory, TrackingProtectionCategory.FINGERPRINTERS.name)
}
@ -39,13 +40,13 @@ class TrackingProtectionStoreTest {
@Test
fun exitDetailsMode() = runTest {
val initialState = detailsState()
val store = TrackingProtectionStore(initialState)
val store = ProtectionsStore(initialState)
store.dispatch(TrackingProtectionAction.ExitDetailsMode).join()
store.dispatch(ProtectionsAction.ExitDetailsMode).join()
assertNotSame(initialState, store.state)
assertEquals(
store.state.mode,
TrackingProtectionState.Mode.Normal,
ProtectionsState.Mode.Normal,
)
assertEquals(store.state.lastAccessedCategory, initialState.lastAccessedCategory)
}
@ -53,10 +54,10 @@ class TrackingProtectionStoreTest {
@Test
fun trackerListChanged() = runTest {
val initialState = defaultState()
val store = TrackingProtectionStore(initialState)
val store = ProtectionsStore(initialState)
val tracker = TrackerLog("url", listOf())
store.dispatch(TrackingProtectionAction.TrackerLogChange(listOf(tracker))).join()
store.dispatch(ProtectionsAction.TrackerLogChange(listOf(tracker))).join()
assertNotSame(initialState, store.state)
assertEquals(
listOf(tracker),
@ -67,9 +68,9 @@ class TrackingProtectionStoreTest {
@Test
fun urlChanged() = runTest {
val initialState = defaultState()
val store = TrackingProtectionStore(initialState)
val store = ProtectionsStore(initialState)
store.dispatch(TrackingProtectionAction.UrlChange("newURL")).join()
store.dispatch(ProtectionsAction.UrlChange("newURL")).join()
assertNotSame(initialState, store.state)
assertEquals(
"newURL",
@ -80,15 +81,16 @@ class TrackingProtectionStoreTest {
@Test
fun onChange() = runTest {
val initialState = defaultState()
val store = TrackingProtectionStore(initialState)
val store = ProtectionsStore(initialState)
val tracker = TrackerLog("url", listOf(), listOf(), cookiesHasBeenBlocked = false)
store.dispatch(
TrackingProtectionAction.Change(
ProtectionsAction.Change(
"newURL",
false,
listOf(tracker),
TrackingProtectionState.Mode.Details(
isTrackingProtectionEnabled = false,
isCookieBannerHandlingEnabled = false,
listTrackers = listOf(tracker),
mode = ProtectionsState.Mode.Details(
TrackingProtectionCategory.FINGERPRINTERS,
true,
),
@ -103,31 +105,51 @@ class TrackingProtectionStoreTest {
false,
store.state.isTrackingProtectionEnabled,
)
assertEquals(
false,
store.state.isCookieBannerHandlingEnabled,
)
assertEquals(
listOf(tracker),
store.state.listTrackers,
)
assertEquals(
TrackingProtectionState.Mode.Details(TrackingProtectionCategory.FINGERPRINTERS, true),
ProtectionsState.Mode.Details(TrackingProtectionCategory.FINGERPRINTERS, true),
store.state.mode,
)
}
private fun defaultState(): TrackingProtectionState = TrackingProtectionState(
@Test
fun `ProtectionsAction - ToggleCookieBannerHandlingProtectionEnabled`() = runTest {
val initialState = defaultState()
val store = ProtectionsStore(initialState)
store.dispatch(
ProtectionsAction.ToggleCookieBannerHandlingProtectionEnabled(
isEnabled = true,
),
).join()
assertTrue(store.state.isCookieBannerHandlingEnabled)
}
private fun defaultState(): ProtectionsState = ProtectionsState(
tab = tab,
url = "www.mozilla.org",
isTrackingProtectionEnabled = true,
isCookieBannerHandlingEnabled = false,
listTrackers = listOf(),
mode = TrackingProtectionState.Mode.Normal,
mode = ProtectionsState.Mode.Normal,
lastAccessedCategory = "",
)
private fun detailsState(): TrackingProtectionState = TrackingProtectionState(
private fun detailsState(): ProtectionsState = ProtectionsState(
tab = tab,
url = "www.mozilla.org",
isTrackingProtectionEnabled = true,
isCookieBannerHandlingEnabled = false,
listTrackers = listOf(),
mode = TrackingProtectionState.Mode.Details(TrackingProtectionCategory.CRYPTOMINERS, true),
mode = ProtectionsState.Mode.Details(TrackingProtectionCategory.CRYPTOMINERS, true),
lastAccessedCategory = TrackingProtectionCategory.CRYPTOMINERS.name,
)
}

View File

@ -50,32 +50,32 @@ class TrackingProtectionPanelDialogFragmentTest {
@Test
fun `WHEN the url is updated THEN the url view is updated`() {
val trackingProtectionStore: TrackingProtectionStore = mockk(relaxed = true)
val protectionsStore: ProtectionsStore = mockk(relaxed = true)
val tab = createTab("mozilla.org")
every { fragment.trackingProtectionStore } returns trackingProtectionStore
every { fragment.protectionsStore } returns protectionsStore
every { fragment.provideCurrentTabId() } returns tab.id
fragment.observeUrlChange(store)
addAndSelectTab(tab)
verify(exactly = 1) {
trackingProtectionStore.dispatch(TrackingProtectionAction.UrlChange("mozilla.org"))
protectionsStore.dispatch(ProtectionsAction.UrlChange("mozilla.org"))
}
store.dispatch(ContentAction.UpdateUrlAction(tab.id, "wikipedia.org")).joinBlocking()
verify(exactly = 1) {
trackingProtectionStore.dispatch(TrackingProtectionAction.UrlChange("wikipedia.org"))
protectionsStore.dispatch(ProtectionsAction.UrlChange("wikipedia.org"))
}
}
@Test
fun `WHEN a tracker is loaded THEN trackers view is updated`() {
val trackingProtectionStore: TrackingProtectionStore = mockk(relaxed = true)
val protectionsStore: ProtectionsStore = mockk(relaxed = true)
val tab = createTab("mozilla.org")
every { fragment.trackingProtectionStore } returns trackingProtectionStore
every { fragment.protectionsStore } returns protectionsStore
every { fragment.provideCurrentTabId() } returns tab.id
every { fragment.updateTrackers(any()) } returns Unit
@ -99,10 +99,10 @@ class TrackingProtectionPanelDialogFragmentTest {
@Test
fun `WHEN a tracker is blocked THEN trackers view is updated`() {
val trackingProtectionStore: TrackingProtectionStore = mockk(relaxed = true)
val protectionsStore: ProtectionsStore = mockk(relaxed = true)
val tab = createTab("mozilla.org")
every { fragment.trackingProtectionStore } returns trackingProtectionStore
every { fragment.protectionsStore } returns protectionsStore
every { fragment.provideCurrentTabId() } returns tab.id
every { fragment.updateTrackers(any()) } returns Unit

View File

@ -9,6 +9,7 @@ import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import io.mockk.MockKAnnotations
import io.mockk.coVerify
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
@ -17,11 +18,15 @@ import io.mockk.spyk
import io.mockk.verify
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.createTab
import mozilla.components.concept.engine.cookiehandling.CookieBannersStorage
import mozilla.components.concept.engine.permission.SitePermissions
import mozilla.components.feature.session.TrackingProtectionUseCases
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.rule.MainCoroutineRule
import mozilla.components.support.test.rule.runTestOnMain
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.ext.components
@ -42,12 +47,16 @@ class TrackingProtectionPanelInteractorTest {
private lateinit var sitePermissions: SitePermissions
@MockK(relaxed = true)
private lateinit var store: TrackingProtectionStore
private lateinit var store: ProtectionsStore
private lateinit var interactor: TrackingProtectionPanelInteractor
private lateinit var tab: TabSessionState
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
private val scope = coroutinesTestRule.scope
private var learnMoreClicked = false
private var openSettings = false
private var gravity = 54
@ -59,11 +68,14 @@ class TrackingProtectionPanelInteractorTest {
context = spyk(testContext)
tab = createTab("https://mozilla.org")
val cookieBannersStorage: CookieBannersStorage = mockk(relaxed = true)
interactor = TrackingProtectionPanelInteractor(
context = context,
fragment = fragment,
store = store,
ioScope = scope,
cookieBannersStorage = cookieBannersStorage,
navController = { navController },
openTrackingProtectionSettings = { openSettings = true },
openLearnMoreLink = { learnMoreClicked = true },
@ -92,7 +104,7 @@ class TrackingProtectionPanelInteractorTest {
verify {
store.dispatch(
TrackingProtectionAction.EnterDetailsMode(
ProtectionsAction.EnterDetailsMode(
TrackingProtectionCategory.FINGERPRINTERS,
true,
),
@ -103,7 +115,7 @@ class TrackingProtectionPanelInteractorTest {
verify {
store.dispatch(
TrackingProtectionAction.EnterDetailsMode(
ProtectionsAction.EnterDetailsMode(
TrackingProtectionCategory.REDIRECT_TRACKERS,
true,
),
@ -126,10 +138,10 @@ class TrackingProtectionPanelInteractorTest {
}
@Test
fun `WHEN onBackPressed is called THEN call popBackStack and navigate`() {
fun `WHEN onBackPressed is called THEN call popBackStack and navigate`() = runTestOnMain {
interactor.onBackPressed()
verify {
coVerify {
navController.popBackStack()
navController.navigate(any<NavDirections>())
@ -140,6 +152,6 @@ class TrackingProtectionPanelInteractorTest {
fun `WHEN onExitDetailMode is called THEN store should dispatch ExitDetailsMode action`() {
interactor.onExitDetailMode()
verify { store.dispatch(TrackingProtectionAction.ExitDetailsMode) }
verify { store.dispatch(ProtectionsAction.ExitDetailsMode) }
}
}

View File

@ -37,12 +37,13 @@ class TrackingProtectionPanelViewTest {
private lateinit var container: ViewGroup
private lateinit var interactor: TrackingProtectionPanelInteractor
private lateinit var view: TrackingProtectionPanelView
private val baseState = TrackingProtectionState(
private val baseState = ProtectionsState(
tab = null,
url = "",
isTrackingProtectionEnabled = false,
isCookieBannerHandlingEnabled = false,
listTrackers = emptyList(),
mode = TrackingProtectionState.Mode.Normal,
mode = ProtectionsState.Mode.Normal,
lastAccessedCategory = "",
)
@ -61,7 +62,7 @@ class TrackingProtectionPanelViewTest {
mockkStatic("org.mozilla.fenix.ext.ContextKt") {
every { any<Context>().settings() } returns mockk(relaxed = true)
view.update(baseState.copy(mode = TrackingProtectionState.Mode.Normal))
view.update(baseState.copy(mode = ProtectionsState.Mode.Normal))
assertFalse(view.binding.detailsMode.isVisible)
assertTrue(view.binding.normalMode.isVisible)
assertTrue(view.binding.protectionSettings.isVisible)
@ -78,7 +79,7 @@ class TrackingProtectionPanelViewTest {
}
val expectedTitle = testContext.getString(R.string.etp_cookies_title_2)
view.update(baseState.copy(mode = TrackingProtectionState.Mode.Normal))
view.update(baseState.copy(mode = ProtectionsState.Mode.Normal))
assertEquals(expectedTitle, view.binding.crossSiteTracking.text)
assertEquals(expectedTitle, view.binding.crossSiteTrackingLoaded.text)
@ -93,7 +94,7 @@ class TrackingProtectionPanelViewTest {
}
val expectedTitle = testContext.getString(R.string.etp_cookies_title)
view.update(baseState.copy(mode = TrackingProtectionState.Mode.Normal))
view.update(baseState.copy(mode = ProtectionsState.Mode.Normal))
assertEquals(expectedTitle, view.binding.crossSiteTracking.text)
assertEquals(expectedTitle, view.binding.crossSiteTrackingLoaded.text)
@ -104,7 +105,7 @@ class TrackingProtectionPanelViewTest {
fun testPrivateModeUi() {
view.update(
baseState.copy(
mode = TrackingProtectionState.Mode.Details(
mode = ProtectionsState.Mode.Details(
selectedCategory = TrackingProtectionCategory.TRACKING_CONTENT,
categoryBlocked = false,
),
@ -137,7 +138,7 @@ class TrackingProtectionPanelViewTest {
view.update(
baseState.copy(
mode = TrackingProtectionState.Mode.Details(
mode = ProtectionsState.Mode.Details(
selectedCategory = CROSS_SITE_TRACKING_COOKIES,
categoryBlocked = false,
),
@ -160,7 +161,7 @@ class TrackingProtectionPanelViewTest {
view.update(
baseState.copy(
mode = TrackingProtectionState.Mode.Details(
mode = ProtectionsState.Mode.Details(
selectedCategory = CROSS_SITE_TRACKING_COOKIES,
categoryBlocked = false,
),