diff --git a/app/metrics.yaml b/app/metrics.yaml index df4ea7bd3a..943076505c 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -8530,7 +8530,38 @@ cookie_banners: metadata: tags: - Privacy&Security - + cfr_shown: + type: event + description: The cookie banner cfr has been shown + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1859393 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1859393#c2 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Privacy&Security + cfr_dismissal: + type: event + description: | + The cookie banners CFR was dismissed by the user by interacting + with the outside of the popup + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1859393 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1859393#c2 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Privacy&Security site_permissions: prompt_shown: type: event diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenter.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenter.kt index 90a5d9d3ab..aa691c1ffd 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenter.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenter.kt @@ -8,9 +8,17 @@ import android.content.Context import android.view.View import androidx.annotation.VisibleForTesting import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.Icon import androidx.compose.material.Text +import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag @@ -34,8 +42,11 @@ import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.compose.cfr.CFRPopup import mozilla.components.compose.cfr.CFRPopup.PopupAlignment.INDICATOR_CENTERED_IN_ANCHOR import mozilla.components.compose.cfr.CFRPopupProperties +import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingStatus import mozilla.components.lib.state.ext.flowScoped import mozilla.components.service.glean.private.NoExtras +import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged +import org.mozilla.fenix.GleanMetrics.CookieBanners import org.mozilla.fenix.GleanMetrics.Shopping import org.mozilla.fenix.GleanMetrics.TrackingProtection import org.mozilla.fenix.R @@ -109,6 +120,24 @@ class BrowserToolbarCFRPresenter( } } } + ToolbarCFR.COOKIE_BANNERS -> { + scope = browserStore.flowScoped { flow -> + flow.mapNotNull { it.findCustomTabOrSelectedTab(sessionId) } + .ifAnyChanged { tab -> + arrayOf( + tab.cookieBanner, + ) + } + .filter { + it.content.private && it.cookieBanner == CookieBannerHandlingStatus.HANDLED + } + .collect { + scope?.cancel() + settings.shouldShowCookieBannersCFR = false + showCookieBannersCFR() + } + } + } ToolbarCFR.SHOPPING, ToolbarCFR.SHOPPING_OPTED_IN -> { scope = browserStore.flowScoped { flow -> @@ -190,6 +219,10 @@ class BrowserToolbarCFRPresenter( settings.openTabsCount >= CFR_MINIMUM_NUMBER_OPENED_TABS ) -> ToolbarCFR.TCP + isPrivate && settings.shouldShowCookieBannersCFR && settings.shouldUseCookieBannerPrivateMode -> { + ToolbarCFR.COOKIE_BANNERS + } + shoppingExperienceFeature.isEnabled && settings.shouldShowReviewQualityCheckCFR -> whichShoppingCFR() @@ -325,6 +358,64 @@ class BrowserToolbarCFRPresenter( } } + @VisibleForTesting + @Suppress("LongMethod") + internal fun showCookieBannersCFR() { + CFRPopup( + anchor = toolbar.findViewById( + R.id.mozac_browser_toolbar_security_indicator, + ), + properties = CFRPopupProperties( + popupAlignment = INDICATOR_CENTERED_IN_ANCHOR, + popupBodyColors = listOf( + getColor(context, R.color.fx_mobile_layer_color_gradient_end), + getColor(context, R.color.fx_mobile_layer_color_gradient_start), + ), + popupVerticalOffset = CFR_TO_ANCHOR_VERTICAL_PADDING.dp, + dismissButtonColor = getColor(context, R.color.fx_mobile_icon_color_oncolor), + indicatorDirection = if (settings.toolbarPosition == ToolbarPosition.TOP) { + CFRPopup.IndicatorDirection.UP + } else { + CFRPopup.IndicatorDirection.DOWN + }, + ), + onDismiss = { + CookieBanners.cfrDismissal.record(NoExtras()) + }, + text = { + FirefoxTheme { + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_cookies_disabled), + contentDescription = null, + tint = FirefoxTheme.colors.iconPrimary, + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = context.getString(R.string.cookie_banner_cfr_title), + color = FirefoxTheme.colors.textOnColorPrimary, + style = FirefoxTheme.typography.subtitle2, + ) + } + Text( + text = context.getString(R.string.cookie_banner_cfr_message), + color = FirefoxTheme.colors.textOnColorPrimary, + style = FirefoxTheme.typography.body2, + modifier = Modifier.padding(top = 2.dp), + ) + } + } + }, + ).run { + popup = this + show() + CookieBanners.cfrShown.record(NoExtras()) + } + } + @VisibleForTesting internal fun showShoppingCFR(shouldShowOptedInCFR: Boolean) { CFRPopup( @@ -395,5 +486,5 @@ class BrowserToolbarCFRPresenter( * The CFR to be shown in the toolbar. */ private enum class ToolbarCFR { - TCP, SHOPPING, SHOPPING_OPTED_IN, ERASE, NONE + TCP, SHOPPING, SHOPPING_OPTED_IN, ERASE, COOKIE_BANNERS, NONE } diff --git a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt index dc38009fd9..1f5aeba0f4 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -778,6 +778,15 @@ class Settings(private val appContext: Context) : PreferencesHolder { default = { feltPrivateBrowsingEnabled }, ) + /** + * Indicates if the cookie banners CRF should be shown. + */ + var shouldShowCookieBannersCFR by lazyFeatureFlagPreference( + appContext.getPreferenceKey(R.string.pref_key_should_show_cookie_banners_action_popup), + featureFlag = true, + default = { shouldShowCookieBannerUI }, + ) + val blockCookiesSelectionInCustomTrackingProtection by stringPreference( key = appContext.getPreferenceKey(R.string.pref_key_tracking_protection_custom_cookies_select), default = if (enabledTotalCookieProtection) { diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml index eaa6c3fc62..d1debd6b16 100644 --- a/app/src/main/res/values/preference_keys.xml +++ b/app/src/main/res/values/preference_keys.xml @@ -275,6 +275,8 @@ pref_key_should_show_total_cookie_protection_popup pref_key_should_show_erase_action_popup + + pref_key_should_show_cookie_banners_action_popup pref_key_debug_settings diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2a89ca1d84..94a0a16eb0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -498,6 +498,10 @@ You’ll see fewer cookie requests Allow + + Firefox just refused cookies for you + + Less distractions, less cookies tracking you on this site. Automatically attempts to connect to sites using HTTPS encryption protocol for increased security. diff --git a/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenterTest.kt b/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenterTest.kt index 2268dd9a0a..c5bfe6124e 100644 --- a/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenterTest.kt +++ b/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenterTest.kt @@ -15,6 +15,7 @@ import io.mockk.verify import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancel import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.action.CookieBannerAction import mozilla.components.browser.state.action.TabListAction import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.CustomTabSessionState @@ -23,6 +24,7 @@ import mozilla.components.browser.state.state.createCustomTab import mozilla.components.browser.state.state.createTab import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.toolbar.BrowserToolbar +import mozilla.components.concept.engine.EngineSession import mozilla.components.support.test.ext.joinBlocking import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.test.rule.MainCoroutineRule @@ -69,6 +71,40 @@ class BrowserToolbarCFRPresenterTest { verify { presenter.showTcpCfr() } } + @Test + fun `GIVEN the cookie banners handling CFR should be shown for a custom tab WHEN the custom tab is fully loaded THEN the TCP CFR is shown`() { + val privateTab = createTab(url = "", private = true) + val browserStore = createBrowserStore(tab = privateTab, selectedTabId = privateTab.id) + val settings: Settings = mockk(relaxed = true) { + every { shouldShowTotalCookieProtectionCFR } returns false + every { shouldShowReviewQualityCheckCFR } returns false + every { reviewQualityCheckOptInTimeInMillis } returns System.currentTimeMillis() + every { shouldShowEraseActionCFR } returns false + every { shouldShowCookieBannersCFR } returns true + every { shouldUseCookieBannerPrivateMode } returns true + every { reviewQualityCheckCfrDisplayTimeInMillis } returns 0L + } + val presenter = createPresenter( + isPrivate = true, + browserStore = browserStore, + settings = settings, + ) + + presenter.start() + + assertNotNull(presenter.scope) + + browserStore.dispatch( + CookieBannerAction.UpdateStatusAction( + privateTab.id, + EngineSession.CookieBannerHandlingStatus.HANDLED, + ), + ).joinBlocking() + + verify { presenter.showCookieBannersCFR() } + verify { settings.shouldShowCookieBannersCFR = false } + } + @Test fun `GIVEN the TCP CFR should be shown WHEN the current normal tab is fully loaded THEN the TCP CFR is shown`() { val normalTab = createTab(url = "", private = false) @@ -457,6 +493,7 @@ class BrowserToolbarCFRPresenterTest { every { shouldShowTotalCookieProtectionCFR } returns true every { shouldShowEraseActionCFR } returns true every { openTabsCount } returns 5 + every { shouldShowCookieBannersCFR } returns true every { shouldShowReviewQualityCheckCFR } returns true }, toolbar: BrowserToolbar = mockk {