Bug 1797593 - Add re-engagement dialog for cookie banners.

This commit is contained in:
Arturo Mejia 2023-01-04 20:53:32 -05:00 committed by mergify[bot]
parent 614ba71842
commit 804d286650
24 changed files with 608 additions and 41 deletions

View File

@ -6932,6 +6932,72 @@ cookie_banners:
metadata:
tags:
- Privacy&Security
visited_re_engagement_dialog:
type: event
description: An user visited the cookie banner re-engagement dialog
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1797593
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/28405#issuecomment-1372489596
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 118
metadata:
tags:
- Privacy&Security
opt_out_re_engagement_dialog:
type: event
description: |
An user opt out the cookie banner re-engagement
dialog by clicking the X button
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1797593
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/28405#issuecomment-1372489596
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 118
metadata:
tags:
- Privacy&Security
not_now_re_engagement_dialog:
type: event
description: |
An user clicked the not now button on
the cookie banner re-engagement dialog
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1797593
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/28405#issuecomment-1372489596
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 118
metadata:
tags:
- Privacy&Security
allow_re_engagement_dialog:
type: event
description: |
An user clicked the dismiss banner button
on the cookie banner re-engagement dialog
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1797593
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/28405#issuecomment-1372489596
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 118
metadata:
tags:
- Privacy&Security
site_permissions:
prompt_shown:
type: event

View File

@ -17,6 +17,7 @@ import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.R
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
@ -53,6 +54,7 @@ class SettingsPrivacyTest {
@Before
fun setUp() {
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
appContext.settings().userOptOutOfReEngageCookieBannerDialog = true
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()

View File

@ -13,10 +13,12 @@ import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.Constants.defaultTopSitesList
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.exitMenu
import org.mozilla.fenix.helpers.TestHelper.getSponsoredShortcutTitle
import org.mozilla.fenix.ui.robots.homeScreen
@ -37,6 +39,7 @@ class SponsoredShortcutsTest {
@Before
fun setUp() {
TestHelper.appContext.settings().userOptOutOfReEngageCookieBannerDialog = true
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()

View File

@ -15,11 +15,14 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.thumbnails.BrowserThumbnails
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.concept.engine.permission.SitePermissions
@ -28,9 +31,11 @@ import mozilla.components.feature.contextmenu.ContextMenuCandidate
import mozilla.components.feature.readerview.ReaderViewFeature
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tabs.WindowFeature
import mozilla.components.lib.state.ext.consumeFlow
import mozilla.components.service.glean.private.NoExtras
import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import org.mozilla.fenix.GleanMetrics.ReaderMode
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
@ -43,6 +48,7 @@ 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.settings.quicksettings.protections.cookiebanners.dialog.CookieBannerReEngagementDialogUtils
import org.mozilla.fenix.shortcut.PwaOnboardingObserver
import org.mozilla.fenix.theme.ThemeManager
@ -173,6 +179,9 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
view = view,
)
}
if (!context.settings().shouldUseCookieBanner && !context.settings().userOptOutOfReEngageCookieBannerDialog) {
observeCookieBannerHandlingState(context.components.core.store)
}
}
override fun onUpdateToolbarForConfigurationChange(toolbar: BrowserToolbarView) {
@ -370,12 +379,17 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
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,
)
}
val hasCookieBannerException =
if (requireContext().settings().shouldUseCookieBanner) {
withContext(Dispatchers.IO) {
cookieBannersStorage.hasException(
tab.content.url,
tab.content.private,
)
}
} else {
false
}
runIfFragmentIsAttached {
val isTrackingProtectionEnabled =
tab.trackingProtection.enabled && !hasTrackingProtectionException
@ -476,4 +490,22 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
internal fun updateLastBrowseActivity() {
requireContext().settings().lastBrowseActivity = System.currentTimeMillis()
}
private fun observeCookieBannerHandlingState(store: BrowserStore) {
consumeFlow(store) { flow ->
flow.mapNotNull { state ->
state.findCustomTabOrSelectedTab(customTabSessionId)
}.ifAnyChanged { tab ->
arrayOf(
tab.cookieBanner,
)
}.collect {
CookieBannerReEngagementDialogUtils.tryToShowReEngagementDialog(
settings = requireContext().settings(),
status = it.cookieBanner,
navController = findNavController(),
)
}
}
}
}

View File

@ -11,12 +11,14 @@ import androidx.compose.material.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.navigation.findNavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.transformWhile
import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.lib.state.ext.flowScoped
@ -29,6 +31,7 @@ import org.mozilla.fenix.compose.cfr.CFRPopupProperties
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.settings.SupportUtils.SumoTopic.TOTAL_COOKIE_PROTECTION
import org.mozilla.fenix.settings.quicksettings.protections.cookiebanners.dialog.CookieBannerReEngagementDialogUtils
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.utils.Settings
@ -111,6 +114,7 @@ class BrowserToolbarCFRPresenter(
true -> TrackingProtection.tcpCfrExplicitDismissal.record(NoExtras())
false -> TrackingProtection.tcpCfrImplicitDismissal.record(NoExtras())
}
tryToShowCookieBannerDialogIfNeeded()
},
) {
Text(
@ -137,4 +141,15 @@ class BrowserToolbarCFRPresenter(
TrackingProtection.tcpCfrShown.record(NoExtras())
}
}
@VisibleForTesting
internal fun tryToShowCookieBannerDialogIfNeeded() {
browserStore.state.selectedTab?.let { tab ->
CookieBannerReEngagementDialogUtils.tryToShowReEngagementDialog(
settings = settings,
status = tab.cookieBanner,
navController = toolbar.findNavController(),
)
}
}
}

View File

@ -167,8 +167,11 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
val cookieBannersStorage = requireComponents.core.cookieBannersStorage
requireComponents.useCases.trackingProtectionUseCases.containsException(tab.id) { contains ->
lifecycleScope.launch(Dispatchers.IO) {
val hasException =
val hasException = if (requireContext().settings().shouldUseCookieBanner) {
cookieBannersStorage.hasException(tab.content.url, tab.content.private)
} else {
false
}
withContext(Dispatchers.Main) {
runIfFragmentIsAttached {
val directions = ExternalAppBrowserFragmentDirections

View File

@ -6,6 +6,7 @@ package org.mozilla.fenix.settings
import android.content.Context
import android.util.AttributeSet
import org.mozilla.fenix.ext.settings
/**
* Cookie banners switch preference with a learn more link.
@ -18,4 +19,8 @@ class CookieBannersSwitchPreference(context: Context, attrs: AttributeSet?) :
SupportUtils.SumoTopic.COOKIE_BANNER,
)
}
override fun getSwitchValue(): Boolean {
return context.settings().shouldUseCookieBanner
}
}

View File

@ -7,6 +7,7 @@ package org.mozilla.fenix.settings
import android.content.Context
import android.util.AttributeSet
import android.widget.TextView
import androidx.appcompat.widget.SwitchCompat
import androidx.core.view.isVisible
import androidx.preference.PreferenceViewHolder
import androidx.preference.SwitchPreferenceCompat
@ -28,6 +29,12 @@ abstract class LearnMoreSwitchPreference(context: Context, attrs: AttributeSet?)
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
val switch = holder.findViewById(R.id.learn_more_switch) as SwitchCompat
switch.run {
isChecked = getSwitchValue()
}
getDescription()?.let {
val summaryView = holder.findViewById(android.R.id.summary) as TextView
summaryView.text = it
@ -62,4 +69,9 @@ abstract class LearnMoreSwitchPreference(context: Context, attrs: AttributeSet?)
* Returns the URL that should be used when the learn more link is clicked.
*/
abstract fun getLearnMoreUrl(): String
/**
* Indicates the value which the switch widget should show.
*/
abstract fun getSwitchValue(): Boolean
}

View File

@ -17,6 +17,7 @@ import mozilla.components.concept.engine.permission.SitePermissions
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.settings
/**
* [ConnectionDetailsController] controller.
@ -50,8 +51,11 @@ class DefaultConnectionDetailsController(
getCurrentTab()?.let { tab ->
context.components.useCases.trackingProtectionUseCases.containsException(tab.id) { contains ->
ioScope.launch {
val hasException =
val hasException = if (context.settings().shouldUseCookieBanner) {
cookieBannersStorage.hasException(tab.content.url, tab.content.private)
} else {
false
}
withContext(Dispatchers.Main) {
fragment.runIfFragmentIsAttached {
navController().popBackStack()

View File

@ -26,6 +26,7 @@ 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.ext.settings
import org.mozilla.fenix.trackingprotection.ProtectionsAction
import org.mozilla.fenix.trackingprotection.ProtectionsStore
@ -72,8 +73,11 @@ class DefaultCookieBannerDetailsController(
getCurrentTab()?.let { tab ->
context.components.useCases.trackingProtectionUseCases.containsException(tab.id) { contains ->
ioScope.launch {
val hasException =
val hasException = if (context.settings().shouldUseCookieBanner) {
cookieBannersStorage.hasException(tab.content.url, tab.content.private)
} else {
false
}
withContext(Dispatchers.Main) {
fragment.runIfFragmentIsAttached {
navController().popBackStack()

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.dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.DialogFragment
import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingMode.REJECT_OR_ACCEPT_ALL
import mozilla.components.concept.engine.Settings
import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.GleanMetrics.CookieBanners
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.theme.FirefoxTheme
/**
* Displays a cookie banner dialog fragment that contains the dialog compose and his logic.
*/
class CookieBannerReEngagementDialog : DialogFragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View = ComposeView(requireContext()).apply {
CookieBanners.visitedReEngagementDialog.record(NoExtras())
setContent {
FirefoxTheme {
val cookieBannerDialogSelectedVariant =
CookieBannerReEngagementDialogUtils.getCookieBannerDialogVariants(requireContext())
CookieBannerReEngagementDialogCompose(
dialogTitle = cookieBannerDialogSelectedVariant.title,
dialogText = cookieBannerDialogSelectedVariant.message,
allowButtonText = cookieBannerDialogSelectedVariant.positiveTextButton,
declineButtonText = getString(R.string.reduce_cookie_banner_dialog_not_now_button),
onAllowButtonClicked = {
CookieBanners.allowReEngagementDialog.record(NoExtras())
requireContext().settings().shouldUseCookieBanner = true
getEngineSettings().cookieBannerHandlingModePrivateBrowsing = REJECT_OR_ACCEPT_ALL
getEngineSettings().cookieBannerHandlingMode = REJECT_OR_ACCEPT_ALL
reload()
requireContext().getRootView()?.let {
FenixSnackbar.make(
view = it,
duration = FenixSnackbar.LENGTH_LONG,
isDisplayedWithBrowserToolbar = true,
)
.setText(getString(R.string.reduce_cookie_banner_dialog_snackbar_text))
.show()
}
dismiss()
},
onNotNowButtonClicked = {
CookieBanners.notNowReEngagementDialog.record(NoExtras())
dismiss()
},
onCloseButtonClicked = {
requireContext().settings().userOptOutOfReEngageCookieBannerDialog = true
CookieBanners.optOutReEngagementDialog.record(NoExtras())
dismiss()
},
)
}
}
}
private fun getEngineSettings(): Settings {
return requireContext().components.core.engine.settings
}
private fun reload() {
return requireContext().components.useCases.sessionUseCases.reload()
}
}

View File

@ -0,0 +1,156 @@
/* 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.dialog
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import org.mozilla.fenix.R
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.defaultTypography
@Composable
@Preview(uiMode = UI_MODE_NIGHT_YES)
@Preview(uiMode = UI_MODE_NIGHT_NO)
private fun CookieBannerReEngagementDialogComposePreview() {
FirefoxTheme {
CookieBannerReEngagementDialogCompose(
dialogTitle = "Cookie banners begone!",
dialogText =
"Automatically reject cookie requests, when possible. Otherwise, " +
"accept all cookies to dismiss cookie banners.",
onAllowButtonClicked = {},
onNotNowButtonClicked = {},
onCloseButtonClicked = {},
allowButtonText = "Dismiss banners",
declineButtonText = "NOT NOW",
)
}
}
/**
* Displays the cookie banner reducer dialog
*/
@Suppress("LongParameterList", "LongMethod")
@Composable
fun CookieBannerReEngagementDialogCompose(
dialogTitle: String,
dialogText: String,
allowButtonText: String,
declineButtonText: String,
onCloseButtonClicked: () -> Unit,
onAllowButtonClicked: () -> Unit,
onNotNowButtonClicked: () -> Unit,
) {
Dialog(
properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = false),
onDismissRequest = onNotNowButtonClicked,
) {
Surface(
color = Color.Transparent,
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.clip(RoundedCornerShape(8.dp))
.background(color = FirefoxTheme.colors.layer1),
) {
Column {
Row(
modifier = Modifier
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
modifier = Modifier.padding(
top = 24.dp,
start = 24.dp,
end = 24.dp,
bottom = 8.dp,
),
color = FirefoxTheme.colors.textPrimary,
text = dialogTitle,
style = defaultTypography.headline7,
)
IconButton(
modifier = Modifier
.size(48.dp),
onClick = onCloseButtonClicked,
) {
Icon(
painter = painterResource(R.drawable.mozac_ic_close),
contentDescription = stringResource(R.string.content_description_close_button),
tint = FirefoxTheme.colors.iconPrimary,
)
}
}
Text(
modifier = Modifier.padding(horizontal = 24.dp),
color = FirefoxTheme.colors.textPrimary,
fontSize = 16.sp,
text = dialogText,
style = defaultTypography.body1,
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(end = 24.dp, bottom = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(
space = 8.dp,
alignment = Alignment.End,
),
) {
TextButton(
onClick = onNotNowButtonClicked,
shape = MaterialTheme.shapes.large,
) {
Text(
text = declineButtonText.uppercase(),
fontSize = 14.sp,
style = MaterialTheme.typography.button,
)
}
TextButton(
onClick = onAllowButtonClicked,
shape = MaterialTheme.shapes.large,
) {
Text(
text = allowButtonText.uppercase(),
fontSize = 14.sp,
style = MaterialTheme.typography.button,
)
}
}
}
}
}
}

View File

@ -0,0 +1,105 @@
/* 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.dialog
import android.content.Context
import androidx.navigation.NavController
import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingStatus
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.nimbus.CookieBannersSection
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.utils.Settings
private const val CONTROL_VARIANT = 0
private const val VARIANT_ONE = 1
private const val VARIANT_TWO = 2
/**
* An utility object for interacting with the re-engagement cookie banner dialog.
*/
object CookieBannerReEngagementDialogUtils {
/**
* Returns a the current [CookieBannerDialogVariant] to the given nimbus experiment.
*/
fun getCookieBannerDialogVariants(context: Context): CookieBannerDialogVariant {
val textVariant =
FxNimbus.features.cookieBanners.value().sectionsEnabled[CookieBannersSection.DIALOG_TEXT_VARIANT]
return when (textVariant) {
CONTROL_VARIANT -> CookieBannerDialogVariant(
title = context.getString(R.string.reduce_cookie_banner_control_experiment_dialog_title),
message = context.getString(
R.string.reduce_cookie_banner_control_experiment_dialog_body,
),
positiveTextButton = context.getString(
R.string.reduce_cookie_banner_control_experiment_dialog_change_setting_button,
),
)
VARIANT_ONE -> CookieBannerDialogVariant(
title = context.getString(R.string.reduce_cookie_banner_variant_1_experiment_dialog_title),
message = context.getString(
R.string.reduce_cookie_banner_variant_1_experiment_dialog_body,
context.getString(R.string.app_name),
),
positiveTextButton = context.getString(
R.string.reduce_cookie_banner_variant_1_experiment_dialog_change_setting_button,
),
)
VARIANT_TWO -> CookieBannerDialogVariant(
title = context.getString(R.string.reduce_cookie_banner_variant_2_experiment_dialog_title),
message = context.getString(
R.string.reduce_cookie_banner_variant_2_experiment_dialog_body,
context.getString(R.string.app_name),
),
positiveTextButton = context.getString(
R.string.reduce_cookie_banner_variant_2_experiment_dialog_change_setting_button,
),
)
else -> {
CookieBannerDialogVariant(
title = context.getString(R.string.reduce_cookie_banner_control_experiment_dialog_title),
message = context.getString(
R.string.reduce_cookie_banner_control_experiment_dialog_body,
),
positiveTextButton = context.getString(
R.string.reduce_cookie_banner_control_experiment_dialog_change_setting_button,
),
)
}
}
}
/**
* Tries to show the re-engagement cookie banner dialog, when the right conditions are met, o
* otherwise the dialog won't show.
*/
fun tryToShowReEngagementDialog(
settings: Settings,
status: CookieBannerHandlingStatus,
navController: NavController,
) {
if (status == CookieBannerHandlingStatus.DETECTED &&
settings.shouldCookieBannerReEngagementDialog()
) {
settings.cookieBannerDetectedPreviously = true
val directions =
BrowserFragmentDirections.actionBrowserFragmentToCookieBannerDialogFragment()
navController.nav(R.id.browserFragment, directions)
}
}
/**
* Data class for cookie banner dialog variant
* @property title of the dialog
* @property message of the dialog
* @property positiveTextButton indicates the text of the positive button of the dialog
*/
data class CookieBannerDialogVariant(
val title: String,
val message: String,
val positiveTextButton: String,
)
}

View File

@ -17,6 +17,7 @@ import mozilla.components.concept.engine.permission.SitePermissions
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.settings
/**
* Interactor for the tracking protection panel
@ -53,8 +54,11 @@ class TrackingProtectionPanelInteractor(
getCurrentTab()?.let { tab ->
context.components.useCases.trackingProtectionUseCases.containsException(tab.id) { contains ->
ioScope.launch {
val hasException =
val hasException = if (context.settings().shouldUseCookieBanner) {
cookieBannersStorage.hasException(tab.content.url, tab.content.private)
} else {
false
}
withContext(Dispatchers.Main) {
fragment.runIfFragmentIsAttached {
navController().popBackStack()

View File

@ -544,11 +544,43 @@ class Settings(private val appContext: Context) : PreferencesHolder {
var shouldUseCookieBanner by lazyFeatureFlagPreference(
appContext.getPreferenceKey(R.string.pref_key_cookie_banner_v1),
featureFlag = true,
default = { cookieBannersSection[CookieBannersSection.FEATURE_SETTING_VALUE] == true },
default = { cookieBannersSection[CookieBannersSection.FEATURE_SETTING_VALUE] == 1 },
)
var userOptOutOfReEngageCookieBannerDialog by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_cookie_banner_re_engage_dialog_dismissed),
default = false,
)
var cookieBannerDetectedPreviously by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_cookie_banner_first_banner_detected),
default = false,
)
val shouldShowCookieBannerUI: Boolean
get() = cookieBannersSection[CookieBannersSection.FEATURE_UI] == true
get() = cookieBannersSection[CookieBannersSection.FEATURE_UI] == 1
/**
* Indicates after how many hours a cookie banner dialog should be shown again
*/
@VisibleForTesting
internal val timerForCookieBannerDialog: Long
get() = 60 * 60 * 1000L *
(cookieBannersSection[CookieBannersSection.DIALOG_RE_ENGAGE_TIME] ?: 4)
/**
* Indicates if we should should show the cookie banner dialog that invites the user to turn-on
* the setting.
*/
fun shouldCookieBannerReEngagementDialog(): Boolean {
val shouldShowDialog =
shouldShowCookieBannerUI && !userOptOutOfReEngageCookieBannerDialog && !shouldUseCookieBanner
return if (!shouldShowTotalCookieProtectionCFR && shouldShowDialog) {
!cookieBannerDetectedPreviously || timeNowInMillis() - lastBrowseActivity >= timerForCookieBannerDialog
} else {
false
}
}
/**
* Declared as a function for performance purposes. This could be declared as a variable using
@ -1293,7 +1325,7 @@ class Settings(private val appContext: Context) : PreferencesHolder {
get() =
FxNimbus.features.mr2022.value().sectionsEnabled
private val cookieBannersSection: Map<CookieBannersSection, Boolean>
private val cookieBannersSection: Map<CookieBannersSection, Int>
get() =
FxNimbus.features.cookieBanners.value().sectionsEnabled
@ -1458,7 +1490,11 @@ class Settings(private val appContext: Context) : PreferencesHolder {
fun getCookieBannerHandling(): CookieBannerHandlingMode {
return when (shouldUseCookieBanner) {
true -> CookieBannerHandlingMode.REJECT_OR_ACCEPT_ALL
false -> CookieBannerHandlingMode.DISABLED
false -> if (shouldShowCookieBannerUI && !userOptOutOfReEngageCookieBannerDialog) {
CookieBannerHandlingMode.DETECT_ONLY
} else {
CookieBannerHandlingMode.DISABLED
}
}
}

View File

@ -55,12 +55,14 @@
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:id="@android:id/widget_frame"
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/learn_more_switch"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="end|center_vertical"
android:minWidth="58dp"
android:orientation="vertical" />
android:minHeight="48dp"
android:textColor="@color/state_list_text_color"
android:textOff="@string/studies_off"
android:textOn="@string/studies_on" />
</LinearLayout>

View File

@ -259,6 +259,9 @@
<action
android:id="@+id/action_browserFragment_to_quickSettingsSheetDialogFragment"
app:destination="@id/quickSettingsSheetDialogFragment" />
<action
android:id="@+id/action_browserFragment_to_cookieBannerDialogFragment"
app:destination="@id/cookieBannerDialogFragment" />
</fragment>
<fragment
@ -960,6 +963,9 @@
android:name="isCookieHandlingEnabled"
app:argType="boolean" />
</dialog>
<dialog
android:id="@+id/cookieBannerDialogFragment"
android:name="org.mozilla.fenix.settings.quicksettings.protections.cookiebanners.dialog.CookieBannerReEngagementDialog" />
<fragment
android:id="@+id/accountProblemFragment"
android:name="org.mozilla.fenix.settings.account.AccountProblemFragment">

View File

@ -151,6 +151,8 @@
<!-- Cookie Banner Reduction Settings-->
<string name="pref_key_cookie_banner_settings" translatable="false">pref_key_cookie_banner_settings</string>
<string name="pref_key_cookie_banner_re_engage_dialog_dismissed" translatable="false">pref_key_cookie_banner_re_engage_dialog_dismissed</string>
<string name="pref_key_cookie_banner_first_banner_detected" translatable="false">pref_key_cookie_banner_first_banner_detected</string>
<string name="pref_key_cookie_banner_v1" translatable="false">pref_key_cookie_banner_v1</string>
<!-- Tracking Protection Settings -->

View File

@ -346,21 +346,23 @@
<!-- Body text for the dialog use on the control branch of the experiment to determine which context users engaged the most -->
<string name="reduce_cookie_banner_control_experiment_dialog_body" tools:ignore="UnusedResources">Automatically reject cookie requests, when possible. Otherwise, accept all cookies to dismiss cookie banners.</string>
<!-- Remind me later text button for the onboarding dialog -->
<string name="reduce_cookie_banner_dialog_not_now_button" tools:ignore="UnusedResources">Not Now</string>
<string name="reduce_cookie_banner_dialog_not_now_button">Not Now</string>
<!-- Change setting text button, for the dialog use on the control branch of the experiment to determine which context users engaged the most -->
<string name="reduce_cookie_banner_control_experiment_dialog_change_setting_button" tools:ignore="UnusedResources">Dismiss banners</string>
<string name="reduce_cookie_banner_control_experiment_dialog_change_setting_button">Dismiss banners</string>
<!-- Snack text for the cookie banner dialog, after user hit the dismiss banner button -->
<string name="reduce_cookie_banner_dialog_snackbar_text">Youll see fewer cookie requests</string>
<!-- Title text for the dialog use on the variant 1 branch of the experiment to determine which context users engaged the most -->
<string name="reduce_cookie_banner_variant_1_experiment_dialog_title" tools:ignore="UnusedResources">See fewer cookie pop-ups</string>
<!-- Body text for the dialog use on the variant 1 branch of the experiment to determine which context users engaged the most -->
<string name="reduce_cookie_banner_variant_1_experiment_dialog_body" tools:ignore="UnusedResources">Automatically answer cookie pop-ups for distraction-free browsing. Firefox will reject all requests if possible, or accept all if not.</string>
<string name="reduce_cookie_banner_variant_1_experiment_dialog_title">See fewer cookie pop-ups</string>
<!-- Body text for the dialog use on the variant 1 branch of the experiment to determine which context users engaged the most. The first parameter is the application name. -->
<string name="reduce_cookie_banner_variant_1_experiment_dialog_body">Automatically answer cookie pop-ups for distraction-free browsing. %1$s will reject all requests if possible, or accept all if not.</string>
<!-- Change setting text button, for the onboarding dialog use on the variant 1 branch of the experiment to determine which context users engaged the most -->
<string name="reduce_cookie_banner_variant_1_experiment_dialog_change_setting_button" tools:ignore="UnusedResources">Dismiss Pop-ups</string>
<string name="reduce_cookie_banner_variant_1_experiment_dialog_change_setting_button">Dismiss Pop-ups</string>
<!-- Title text for the dialog use on the variant 2 branch of the experiment to determine which context users engaged the most -->
<string name="reduce_cookie_banner_variant_2_experiment_dialog_title" tools:ignore="UnusedResources">Cookie Banner Reduction</string>
<!-- Body text for the dialog use on the variant 2 branch of the experiment to determine which context users engaged the most -->
<string name="reduce_cookie_banner_variant_2_experiment_dialog_body" tools:ignore="UnusedResources">Allow Firefox to decline a sites cookie consent request if possible or accept cookie access when not possible?</string>
<string name="reduce_cookie_banner_variant_2_experiment_dialog_title">Cookie Banner Reduction</string>
<!-- Body text for the dialog use on the variant 2 branch of the experiment to determine which context users engaged the most. The first parameter is the application name. -->
<string name="reduce_cookie_banner_variant_2_experiment_dialog_body">Allow %1$s to decline a sites cookie consent request if possible or accept cookie access when not possible?</string>
<!-- Change setting text button, for the dialog use on the variant 2 branch of the experiment to determine which context users engaged the most -->
<string name="reduce_cookie_banner_variant_2_experiment_dialog_change_setting_button" tools:ignore="UnusedResources">Allow</string>
<string name="reduce_cookie_banner_variant_2_experiment_dialog_change_setting_button">Allow</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

@ -221,6 +221,7 @@ class BrowserToolbarCFRPresenterTest {
val presenter = createPresenter(
anchor = mockk(relaxed = true),
)
every { presenter.tryToShowCookieBannerDialogIfNeeded() } just Runs
presenter.showTcpCfr()
@ -231,6 +232,9 @@ class BrowserToolbarCFRPresenterTest {
assertNull(TrackingProtection.tcpCfrImplicitDismissal.testGetValue())
presenter.tcpCfrPopup!!.onDismiss.invoke(false)
assertNotNull(TrackingProtection.tcpCfrImplicitDismissal.testGetValue())
verify {
presenter.tryToShowCookieBannerDialogIfNeeded()
}
}
/**
@ -260,12 +264,14 @@ class BrowserToolbarCFRPresenterTest {
every { findViewById<View>(R.id.mozac_browser_toolbar_security_indicator) } returns anchor
},
sessionId: String? = null,
) = BrowserToolbarCFRPresenter(
context = context,
browserStore = browserStore,
settings = settings,
toolbar = toolbar,
sessionId = sessionId,
) = spyk(
BrowserToolbarCFRPresenter(
context = context,
browserStore = browserStore,
settings = settings,
toolbar = toolbar,
sessionId = sessionId,
),
)
private fun createBrowserStore(

View File

@ -27,6 +27,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class)
@ -87,6 +88,8 @@ class DefaultConnectionDetailsControllerTest {
@Test
fun `WHEN handleBackPressed is called THEN should call popBackStack and navigate`() {
every { context.settings().shouldUseCookieBanner } returns true
controller.handleBackPressed()
verify {

View File

@ -43,6 +43,7 @@ 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.ext.settings
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.trackingprotection.ProtectionsAction
import org.mozilla.fenix.trackingprotection.ProtectionsStore
@ -131,6 +132,8 @@ internal class DefaultCookieBannerDetailsControllerTest {
@Test
fun `WHEN handleBackPressed is called THEN should call popBackStack and navigate`() {
every { context.settings().shouldUseCookieBanner } returns true
controller.handleBackPressed()
verify {

View File

@ -30,6 +30,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class)
@ -139,6 +140,8 @@ class TrackingProtectionPanelInteractorTest {
@Test
fun `WHEN onBackPressed is called THEN call popBackStack and navigate`() = runTestOnMain {
every { context.settings().shouldUseCookieBanner } returns true
interactor.onBackPressed()
coVerify {

View File

@ -219,25 +219,31 @@ features:
variables:
sections-enabled:
description: "This property provides a lookup table of whether or not the given section should be enabled."
type: Map<CookieBannersSection, Boolean>
type: Map<CookieBannersSection, Int>
default:
{
"feature-ui": false,
"feature-setting-value": false,
"feature-ui": 0,
"feature-setting-value": 0,
"dialog-re-engage-time": 4,
"dialog-text-variant": 0,
}
defaults:
- channel: developer
value: {
"sections-enabled": {
"feature-ui": true,
"feature-setting-value": false,
"feature-ui": 1,
"feature-setting-value": 0,
"dialog-re-engage-time": 4,
"dialog-text-variant": 0,
}
}
- channel: nightly
value: {
"sections-enabled": {
"feature-ui": true,
"feature-setting-value": false,
"feature-ui": 1,
"feature-setting-value": 0,
"dialog-re-engage-time": 4,
"dialog-text-variant": 0,
}
}
unified-search:
@ -409,3 +415,7 @@ types:
description: Indicates if the user interfaces should be visible.
feature-setting-value:
description: Indicates if the cookie handling setting should be enabled.
dialog-re-engage-time:
description: Indicates after how many hours of interaction, the dialog should show again.
dialog-text-variant:
description: Indicates which text variant should be used in the dialog.