Bug 1797577 - Add cookie banner handling panel to the toolbar.
parent
e7a7712a6b
commit
cc666c8887
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
||||||
|
}
|
@ -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") },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
|
@ -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>
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue