From cc666c8887e2758325cd0a4208e48c804f90b4bd Mon Sep 17 00:00:00 2001 From: Arturo Mejia Date: Thu, 1 Dec 2022 09:37:40 -0500 Subject: [PATCH] Bug 1797577 - Add cookie banner handling panel to the toolbar. --- app/metrics.yaml | 50 ++++- .../mozilla/fenix/browser/BrowserFragment.kt | 48 +++-- .../java/org/mozilla/fenix/components/Core.kt | 3 + .../customtabs/ExternalAppBrowserFragment.kt | 41 ++-- .../fenix/settings/SettingsFragment.kt | 1 + .../ConnectionDetailsController.kt | 48 +++-- .../ConnectionPanelDialogFragment.kt | 5 + .../quicksettings/QuickSettingsController.kt | 35 +++- .../QuickSettingsFragmentAction.kt | 4 +- .../QuickSettingsFragmentReducer.kt | 17 +- .../QuickSettingsFragmentState.kt | 6 +- .../QuickSettingsFragmentStore.kt | 20 +- .../quicksettings/QuickSettingsInteractor.kt | 12 +- .../QuickSettingsSheetDialogFragment.kt | 13 +- .../quicksettings/TrackingProtectionView.kt | 79 -------- .../protections/ProtectionsInteractor.kt | 29 +++ .../protections/ProtectionsView.kt | 185 ++++++++++++++++++ .../CookieBannerDetailsController.kt | 119 +++++++++++ .../CookieBannerDetailsInteractor.kt | 42 ++++ .../CookieBannerHandlingDetailsView.kt | 83 ++++++++ .../CookieBannerPanelDialogFragment.kt | 114 +++++++++++ ...ProtectionStore.kt => ProtectionsStore.kt} | 96 ++++++--- .../SwitchWithDescription.kt | 108 ++++++++-- .../TrackingProtectionPanelDialogFragment.kt | 24 ++- .../TrackingProtectionPanelInteractor.kt | 53 +++-- .../TrackingProtectionPanelView.kt | 17 +- .../main/res/drawable/ic_cookies_disabled.xml | 10 +- .../main/res/drawable/ic_cookies_enabled.xml | 10 +- .../res/drawable/ic_tracking_protection.xml | 14 -- .../component_cookie_banner_details_panel.xml | 67 +++++++ ..._cookie_banner_handling_details_dialog.xml | 14 ++ ...ml => quicksettings_protections_panel.xml} | 25 ++- .../res/layout/switch_with_description.xml | 17 +- app/src/main/res/navigation/nav_graph.xml | 36 ++++ app/src/main/res/values/attrs.xml | 7 +- app/src/main/res/values/strings.xml | 12 ++ .../DefaultConnectionDetailsControllerTest.kt | 12 ++ .../DefaultQuickSettingsControllerTest.kt | 23 ++- .../quicksettings/ProtectionsViewTest.kt | 141 +++++++++++++ .../QuickSettingsFragmentReducerTest.kt | 33 ++-- .../QuickSettingsFragmentStoreTest.kt | 12 +- .../QuickSettingsInteractorTest.kt | 13 +- .../QuickSettingsSheetDialogFragmentTest.kt | 13 +- .../TrackingProtectionViewTest.kt | 95 --------- .../CookieBannerHandlingDetailsViewTest.kt | 155 +++++++++++++++ ...efaultCookieBannerDetailsControllerTest.kt | 182 +++++++++++++++++ ...efaultCookieBannerDetailsInteractorTest.kt | 42 ++++ ...onStoreTest.kt => ProtectionsStoreTest.kt} | 64 ++++-- ...ackingProtectionPanelDialogFragmentTest.kt | 16 +- .../TrackingProtectionPanelInteractorTest.kt | 24 ++- .../TrackingProtectionPanelViewTest.kt | 17 +- 51 files changed, 1857 insertions(+), 449 deletions(-) delete mode 100644 app/src/main/java/org/mozilla/fenix/settings/quicksettings/TrackingProtectionView.kt create mode 100644 app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/ProtectionsInteractor.kt create mode 100644 app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/ProtectionsView.kt create mode 100644 app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerDetailsController.kt create mode 100644 app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerDetailsInteractor.kt create mode 100644 app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerHandlingDetailsView.kt create mode 100644 app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerPanelDialogFragment.kt rename app/src/main/java/org/mozilla/fenix/trackingprotection/{TrackingProtectionStore.kt => ProtectionsStore.kt} (55%) delete mode 100644 app/src/main/res/drawable/ic_tracking_protection.xml create mode 100644 app/src/main/res/layout/component_cookie_banner_details_panel.xml create mode 100644 app/src/main/res/layout/fragment_cookie_banner_handling_details_dialog.xml rename app/src/main/res/layout/{quicksettings_tracking_protection.xml => quicksettings_protections_panel.xml} (60%) create mode 100644 app/src/test/java/org/mozilla/fenix/settings/quicksettings/ProtectionsViewTest.kt delete mode 100644 app/src/test/java/org/mozilla/fenix/settings/quicksettings/TrackingProtectionViewTest.kt create mode 100644 app/src/test/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerHandlingDetailsViewTest.kt create mode 100644 app/src/test/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/DefaultCookieBannerDetailsControllerTest.kt create mode 100644 app/src/test/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/DefaultCookieBannerDetailsInteractorTest.kt rename app/src/test/java/org/mozilla/fenix/trackingprotection/{TrackingProtectionStoreTest.kt => ProtectionsStoreTest.kt} (60%) diff --git a/app/metrics.yaml b/app/metrics.yaml index 15f13b09c..18050c1b6 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -6883,7 +6883,55 @@ cookie_banners: metadata: tags: - Privacy&Security - + exception_added: + type: event + description: | + A user added a cookie banner handling exception through + the toggle in the protections panel. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1797577 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/28044#issuecomment-1334548056 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: 118 + metadata: + tags: + - Privacy&Security + exception_removed: + type: event + description: | + A user removed a cookie banner handling + exception through the toggle in the protections panel. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1797577 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/28044#issuecomment-1334548056 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: 118 + metadata: + tags: + - Privacy&Security + visited_panel: + type: event + description: A user visited the cookie banner toolbar panel + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1797577 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/28044#issuecomment-1334548056 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: 118 + metadata: + tags: + - Privacy&Security site_permissions: prompt_shown: type: event diff --git a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt index c2760bd9e..a76cb5a64 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt @@ -11,8 +11,12 @@ import android.view.ViewGroup import androidx.annotation.VisibleForTesting import androidx.appcompat.content.res.AppCompatResources import androidx.lifecycle.Observer +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import com.google.android.material.snackbar.Snackbar +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.state.TabSessionState @@ -38,6 +42,7 @@ import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.runIfFragmentIsAttached import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.nimbus.FxNimbus import org.mozilla.fenix.shortcut.PwaOnboardingObserver import org.mozilla.fenix.theme.ThemeManager @@ -360,22 +365,35 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { } override fun navToQuickSettingsSheet(tab: SessionState, sitePermissions: SitePermissions?) { - requireComponents.useCases.trackingProtectionUseCases.containsException(tab.id) { contains -> - runIfFragmentIsAttached { - val isTrackingProtectionEnabled = tab.trackingProtection.enabled && !contains - val directions = - BrowserFragmentDirections.actionBrowserFragmentToQuickSettingsSheetDialogFragment( - sessionId = tab.id, - url = tab.content.url, - title = tab.content.title, - isSecured = tab.content.securityInfo.secure, - sitePermissions = sitePermissions, - gravity = getAppropriateLayoutGravity(), - certificateName = tab.content.securityInfo.issuer, - permissionHighlights = tab.content.permissionHighlights, - isTrackingProtectionEnabled = isTrackingProtectionEnabled, + val useCase = requireComponents.useCases.trackingProtectionUseCases + FxNimbus.features.cookieBanners.recordExposure() + useCase.containsException(tab.id) { hasTrackingProtectionException -> + lifecycleScope.launch(Dispatchers.Main) { + val cookieBannersStorage = requireComponents.core.cookieBannersStorage + val hasCookieBannerException = withContext(Dispatchers.IO) { + cookieBannersStorage.hasException( + tab.content.url, + tab.content.private, ) - nav(R.id.browserFragment, directions) + } + runIfFragmentIsAttached { + val isTrackingProtectionEnabled = + tab.trackingProtection.enabled && !hasTrackingProtectionException + val directions = + BrowserFragmentDirections.actionBrowserFragmentToQuickSettingsSheetDialogFragment( + sessionId = tab.id, + url = tab.content.url, + title = tab.content.title, + isSecured = tab.content.securityInfo.secure, + sitePermissions = sitePermissions, + gravity = getAppropriateLayoutGravity(), + certificateName = tab.content.securityInfo.issuer, + permissionHighlights = tab.content.permissionHighlights, + isTrackingProtectionEnabled = isTrackingProtectionEnabled, + isCookieHandlingEnabled = !hasCookieBannerException, + ) + nav(R.id.browserFragment, directions) + } } } } diff --git a/app/src/main/java/org/mozilla/fenix/components/Core.kt b/app/src/main/java/org/mozilla/fenix/components/Core.kt index a92775a8e..fd3b644c3 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Core.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Core.kt @@ -12,6 +12,7 @@ import androidx.appcompat.content.res.AppCompatResources.getDrawable import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.toBitmap import mozilla.components.browser.engine.gecko.GeckoEngine +import mozilla.components.browser.engine.gecko.cookiebanners.GeckoCookieBannersStorage import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient import mozilla.components.browser.engine.gecko.permission.GeckoSitePermissionsStorage import mozilla.components.browser.icons.BrowserIcons @@ -183,6 +184,8 @@ class Core( ) } + val cookieBannersStorage by lazyMonitored { GeckoCookieBannersStorage(geckoRuntime) } + val geckoSitePermissionsStorage by lazyMonitored { GeckoSitePermissionsStorage(geckoRuntime, OnDiskSitePermissionsStorage(context)) } diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt index 07a9b46d8..7657256f6 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt @@ -9,7 +9,11 @@ import android.content.Intent import android.view.View import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.navArgs +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.concept.engine.manifest.WebAppManifestParser @@ -29,6 +33,7 @@ import org.mozilla.fenix.R import org.mozilla.fenix.browser.BaseBrowserFragment import org.mozilla.fenix.browser.CustomTabContextMenuCandidate import org.mozilla.fenix.browser.FenixSnackbarDelegate +import org.mozilla.fenix.components.components import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.requireComponents @@ -159,21 +164,29 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler } override fun navToQuickSettingsSheet(tab: SessionState, sitePermissions: SitePermissions?) { + val cookieBannersStorage = requireComponents.core.cookieBannersStorage requireComponents.useCases.trackingProtectionUseCases.containsException(tab.id) { contains -> - runIfFragmentIsAttached { - val directions = ExternalAppBrowserFragmentDirections - .actionGlobalQuickSettingsSheetDialogFragment( - sessionId = tab.id, - url = tab.content.url, - title = tab.content.title, - isSecured = tab.content.securityInfo.secure, - sitePermissions = sitePermissions, - gravity = getAppropriateLayoutGravity(), - certificateName = tab.content.securityInfo.issuer, - permissionHighlights = tab.content.permissionHighlights, - isTrackingProtectionEnabled = tab.trackingProtection.enabled && !contains, - ) - nav(R.id.externalAppBrowserFragment, directions) + lifecycleScope.launch(Dispatchers.IO) { + val hasException = + cookieBannersStorage.hasException(tab.content.url, tab.content.private) + withContext(Dispatchers.Main) { + runIfFragmentIsAttached { + val directions = ExternalAppBrowserFragmentDirections + .actionGlobalQuickSettingsSheetDialogFragment( + sessionId = tab.id, + url = tab.content.url, + title = tab.content.title, + isSecured = tab.content.securityInfo.secure, + sitePermissions = sitePermissions, + gravity = getAppropriateLayoutGravity(), + certificateName = tab.content.securityInfo.issuer, + permissionHighlights = tab.content.permissionHighlights, + isTrackingProtectionEnabled = tab.trackingProtection.enabled && !contains, + isCookieHandlingEnabled = !hasException, + ) + nav(R.id.externalAppBrowserFragment, directions) + } + } } } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt index c77b3976f..99f30c763 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt @@ -281,6 +281,7 @@ class SettingsFragment : PreferenceFragmentCompat() { SettingsFragmentDirections.actionSettingsFragmentToHttpsOnlyFragment() } resources.getString(R.string.pref_key_cookie_banner_settings) -> { + FxNimbus.features.cookieBanners.recordExposure() CookieBanners.visitedSetting.record(mozilla.components.service.glean.private.NoExtras()) SettingsFragmentDirections.actionSettingsFragmentToCookieBannerFragment() } diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionDetailsController.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionDetailsController.kt index c33bd7eb2..94969e246 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionDetailsController.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionDetailsController.kt @@ -7,7 +7,12 @@ package org.mozilla.fenix.settings.quicksettings import android.content.Context import androidx.fragment.app.Fragment import androidx.navigation.NavController +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import mozilla.components.browser.state.state.SessionState +import mozilla.components.concept.engine.cookiehandling.CookieBannersStorage import mozilla.components.concept.engine.permission.SitePermissions import org.mozilla.fenix.browser.BrowserFragmentDirections import org.mozilla.fenix.ext.components @@ -29,9 +34,12 @@ interface ConnectionDetailsController { /** * Default behavior of [ConnectionDetailsController]. */ +@Suppress("LongParameterList") class DefaultConnectionDetailsController( private val context: Context, private val fragment: Fragment, + private val ioScope: CoroutineScope, + private val cookieBannersStorage: CookieBannersStorage, private val navController: () -> NavController, internal var sitePermissions: SitePermissions?, private val gravity: Int, @@ -41,22 +49,30 @@ class DefaultConnectionDetailsController( override fun handleBackPressed() { getCurrentTab()?.let { tab -> context.components.useCases.trackingProtectionUseCases.containsException(tab.id) { contains -> - fragment.runIfFragmentIsAttached { - navController().popBackStack() - val isTrackingProtectionEnabled = tab.trackingProtection.enabled && !contains - val directions = - BrowserFragmentDirections.actionGlobalQuickSettingsSheetDialogFragment( - sessionId = tab.id, - url = tab.content.url, - title = tab.content.title, - isSecured = tab.content.securityInfo.secure, - sitePermissions = sitePermissions, - gravity = gravity, - certificateName = tab.content.securityInfo.issuer, - permissionHighlights = tab.content.permissionHighlights, - isTrackingProtectionEnabled = isTrackingProtectionEnabled, - ) - navController().navigate(directions) + ioScope.launch { + val hasException = + cookieBannersStorage.hasException(tab.content.url, tab.content.private) + withContext(Dispatchers.Main) { + fragment.runIfFragmentIsAttached { + navController().popBackStack() + val isTrackingProtectionEnabled = + tab.trackingProtection.enabled && !contains + val directions = + BrowserFragmentDirections.actionGlobalQuickSettingsSheetDialogFragment( + sessionId = tab.id, + url = tab.content.url, + title = tab.content.title, + isSecured = tab.content.securityInfo.secure, + sitePermissions = sitePermissions, + gravity = gravity, + certificateName = tab.content.securityInfo.issuer, + permissionHighlights = tab.content.permissionHighlights, + isTrackingProtectionEnabled = isTrackingProtectionEnabled, + isCookieHandlingEnabled = !hasException, + ) + navController().navigate(directions) + } + } } } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionPanelDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionPanelDialogFragment.kt index 730623c95..1654af980 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionPanelDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionPanelDialogFragment.kt @@ -9,8 +9,11 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.annotation.VisibleForTesting +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.plus import mozilla.components.browser.state.selector.findTabOrCustomTab import mozilla.components.browser.state.state.SessionState import org.mozilla.fenix.R @@ -39,6 +42,8 @@ class ConnectionPanelDialogFragment : FenixDialogFragment() { val controller = DefaultConnectionDetailsController( context = requireContext(), + ioScope = viewLifecycleOwner.lifecycleScope + Dispatchers.IO, + cookieBannersStorage = requireComponents.core.cookieBannersStorage, fragment = this, navController = { findNavController() }, sitePermissions = args.sitePermissions, diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt index 98dba9fad..b9c714a4d 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt @@ -17,6 +17,7 @@ import mozilla.components.feature.session.SessionUseCases.ReloadUrlUseCase import mozilla.components.support.base.feature.OnNeedToRequestPermissions import mozilla.components.support.ktx.kotlin.getOrigin import mozilla.telemetry.glean.private.NoExtras +import org.mozilla.fenix.GleanMetrics.CookieBanners import org.mozilla.fenix.GleanMetrics.TrackingProtection import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.components.PermissionStorage @@ -60,14 +61,19 @@ interface QuickSettingsController { fun handleAndroidPermissionGranted(feature: PhoneFeature) /** - * @see [TrackingProtectionInteractor.onTrackingProtectionToggled] + * @see [ProtectionsInteractor.onTrackingProtectionToggled] */ fun handleTrackingProtectionToggled(isEnabled: Boolean) /** - * @see [TrackingProtectionInteractor.onDetailsClicked] + * Navigates to the cookie banners details panel. */ - fun handleDetailsClicked() + fun handleCookieBannerHandlingDetailsClicked() + + /** + * Navigates to the tracking protection details panel. + */ + fun handleTrackingProtectionDetailsClicked() /** * Navigates to the connection details. Called when a user clicks on the @@ -201,15 +207,34 @@ class DefaultQuickSettingsController( ) } - override fun handleDetailsClicked() { + override fun handleCookieBannerHandlingDetailsClicked() { + CookieBanners.visitedPanel.record(NoExtras()) + + navController.popBackStack() + + val state = quickSettingsStore.state.protectionsState + val directions = NavGraphDirections + .actionGlobalCookieBannerProtectionPanelDialogFragment( + sessionId = sessionId, + url = state.url, + trackingProtectionEnabled = state.isTrackingProtectionEnabled, + cookieBannerHandlingEnabled = state.isCookieBannerHandlingEnabled, + gravity = context.components.settings.toolbarPosition.androidGravity, + sitePermissions = sitePermissions, + ) + navController.navigate(directions) + } + + override fun handleTrackingProtectionDetailsClicked() { navController.popBackStack() - val state = quickSettingsStore.state.trackingProtectionState + val state = quickSettingsStore.state.protectionsState val directions = NavGraphDirections .actionGlobalTrackingProtectionPanelDialogFragment( sessionId = sessionId, url = state.url, trackingProtectionEnabled = state.isTrackingProtectionEnabled, + cookieBannerHandlingEnabled = state.isCookieBannerHandlingEnabled, gravity = context.components.settings.toolbarPosition.androidGravity, sitePermissions = sitePermissions, ) diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentAction.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentAction.kt index 7607516c0..9dd98ae4e 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentAction.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentAction.kt @@ -6,7 +6,7 @@ package org.mozilla.fenix.settings.quicksettings import mozilla.components.lib.state.Action import org.mozilla.fenix.settings.PhoneFeature -import org.mozilla.fenix.trackingprotection.TrackingProtectionState +import org.mozilla.fenix.trackingprotection.ProtectionsState /** * Parent [Action] for all the [QuickSettingsFragmentState] changes. @@ -49,7 +49,7 @@ sealed class WebsitePermissionAction(open val updatedFeature: PhoneFeature) : Qu } /** - * All possible [TrackingProtectionState] changes as a result oof user / system interactions. + * All possible [ProtectionsState] changes in the quick setting panel. */ sealed class TrackingProtectionAction : QuickSettingsFragmentAction() { /** diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentReducer.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentReducer.kt index 38373f452..3559a1230 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentReducer.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentReducer.kt @@ -4,7 +4,7 @@ package org.mozilla.fenix.settings.quicksettings -import org.mozilla.fenix.trackingprotection.TrackingProtectionState +import org.mozilla.fenix.trackingprotection.ProtectionsState /** * Parent Reducer for all [QuickSettingsFragmentState]s of all Views shown in this Fragment. @@ -27,8 +27,8 @@ internal fun quickSettingsFragmentReducer( ), ) is TrackingProtectionAction -> state.copy( - trackingProtectionState = TrackingProtectionStateReducer.reduce( - state = state.trackingProtectionState, + protectionsState = ProtectionsStateReducer.reduce( + state = state.protectionsState, action = action, ), ) @@ -67,15 +67,18 @@ object WebsitePermissionsStateReducer { } } -object TrackingProtectionStateReducer { +/** + * A reduce for [TrackingProtectionAction]s. + */ +object ProtectionsStateReducer { /** - * Handles creating a new [TrackingProtectionState] based on the specific + * Handles creating a new [ProtectionsState] based on the specific * [TrackingProtectionAction]. */ fun reduce( - state: TrackingProtectionState, + state: ProtectionsState, action: TrackingProtectionAction, - ): TrackingProtectionState { + ): ProtectionsState { return when (action) { is TrackingProtectionAction.ToggleTrackingProtectionEnabled -> state.copy(isTrackingProtectionEnabled = action.isTrackingProtectionEnabled) diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentState.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentState.kt index 3aa965f65..a6fcdde23 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentState.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentState.kt @@ -14,18 +14,18 @@ import mozilla.components.feature.sitepermissions.SitePermissionsRules.AutoplayA import mozilla.components.lib.state.State import org.mozilla.fenix.R import org.mozilla.fenix.settings.PhoneFeature -import org.mozilla.fenix.trackingprotection.TrackingProtectionState +import org.mozilla.fenix.trackingprotection.ProtectionsState import org.mozilla.fenix.utils.Settings /** * [State] containing all data displayed to the user by this Fragment. * - * Partitioned further to contain mutiple states for each standalone View this Fragment holds. + * Partitioned further to contain multiple states for each standalone View this Fragment holds. */ data class QuickSettingsFragmentState( val webInfoState: WebsiteInfoState, val websitePermissionsState: WebsitePermissionsState, - val trackingProtectionState: TrackingProtectionState, + val protectionsState: ProtectionsState, ) : State /** diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStore.kt index 2cc44456c..7062380b7 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStore.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStore.kt @@ -19,7 +19,7 @@ import org.mozilla.fenix.settings.quicksettings.QuickSettingsFragmentStore.Compa import org.mozilla.fenix.settings.quicksettings.WebsiteInfoState.Companion.createWebsiteInfoState import org.mozilla.fenix.settings.quicksettings.ext.shouldBeEnabled import org.mozilla.fenix.settings.quicksettings.ext.shouldBeVisible -import org.mozilla.fenix.trackingprotection.TrackingProtectionState +import org.mozilla.fenix.trackingprotection.ProtectionsState import org.mozilla.fenix.utils.Settings import java.util.EnumMap @@ -69,6 +69,7 @@ class QuickSettingsFragmentStore( settings: Settings, sessionId: String, isTrackingProtectionEnabled: Boolean, + isCookieHandlingEnabled: Boolean, ) = QuickSettingsFragmentStore( QuickSettingsFragmentState( webInfoState = createWebsiteInfoState( @@ -83,11 +84,12 @@ class QuickSettingsFragmentStore( permissionHighlights, settings, ), - trackingProtectionState = createTrackingProtectionState( + protectionsState = createTrackingProtectionState( context, sessionId, websiteUrl, isTrackingProtectionEnabled, + isCookieHandlingEnabled, ), ), ) @@ -123,14 +125,16 @@ class QuickSettingsFragmentStore( } /** - * Construct an initial [TrackingProtectionState] to be rendered by - * [TrackingProtectionView]. + * Construct an initial [ProtectionsState] to be rendered by + * [ProtectionsView]. * * @param context [Context] used for various Android interactions. * @param sessionId [String] The current session ID. * @param websiteUrl [String] the URL of the current web page. * @param isTrackingProtectionEnabled [Boolean] Current status of tracking protection * for this session. + * @param isCookieHandlingEnabled [Boolean] Current status of cookie banner handling + * for this session. */ @VisibleForTesting fun createTrackingProtectionState( @@ -138,13 +142,15 @@ class QuickSettingsFragmentStore( sessionId: String, websiteUrl: String, isTrackingProtectionEnabled: Boolean, - ): TrackingProtectionState { - return TrackingProtectionState( + isCookieHandlingEnabled: Boolean, + ): ProtectionsState { + return ProtectionsState( tab = context.components.core.store.state.findTabOrCustomTab(sessionId), url = websiteUrl, isTrackingProtectionEnabled = isTrackingProtectionEnabled, + isCookieBannerHandlingEnabled = isCookieHandlingEnabled, listTrackers = listOf(), - mode = TrackingProtectionState.Mode.Normal, + mode = ProtectionsState.Mode.Normal, lastAccessedCategory = "", ) } diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractor.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractor.kt index c2834416a..7dcac92b0 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractor.kt @@ -4,6 +4,8 @@ package org.mozilla.fenix.settings.quicksettings +import org.mozilla.fenix.settings.quicksettings.protections.ProtectionsInteractor + /** * [QuickSettingsSheetDialogFragment] interactor. * @@ -15,7 +17,7 @@ package org.mozilla.fenix.settings.quicksettings */ class QuickSettingsInteractor( private val controller: QuickSettingsController, -) : WebsitePermissionInteractor, TrackingProtectionInteractor, WebSiteInfoInteractor, ClearSiteDataViewInteractor { +) : WebsitePermissionInteractor, ProtectionsInteractor, WebSiteInfoInteractor, ClearSiteDataViewInteractor { override fun onPermissionsShown() { controller.handlePermissionsShown() } @@ -32,8 +34,12 @@ class QuickSettingsInteractor( controller.handleTrackingProtectionToggled(isEnabled) } - override fun onDetailsClicked() { - controller.handleDetailsClicked() + override fun onCookieBannerHandlingDetailsClicked() { + controller.handleCookieBannerHandlingDetailsClicked() + } + + override fun onTrackingProtectionDetailsClicked() { + controller.handleTrackingProtectionDetailsClicked() } override fun onConnectionDetailsClicked() { diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragment.kt index 9a9c05fbf..e9937b89d 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragment.kt @@ -17,7 +17,6 @@ import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.plus import mozilla.components.browser.state.selector.findTabOrCustomTab @@ -35,6 +34,7 @@ import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings import org.mozilla.fenix.settings.PhoneFeature +import org.mozilla.fenix.settings.quicksettings.protections.ProtectionsView /** * Dialog that presents the user with information about @@ -52,7 +52,7 @@ class QuickSettingsSheetDialogFragment : FenixDialogFragment() { private lateinit var clearSiteDataView: ClearSiteDataView @VisibleForTesting - internal lateinit var trackingProtectionView: TrackingProtectionView + internal lateinit var protectionsView: ProtectionsView private lateinit var interactor: QuickSettingsInteractor @@ -91,6 +91,7 @@ class QuickSettingsSheetDialogFragment : FenixDialogFragment() { permissionHighlights = args.permissionHighlights, sessionId = args.sessionId, isTrackingProtectionEnabled = args.isTrackingProtectionEnabled, + isCookieHandlingEnabled = args.isCookieHandlingEnabled, ) quickSettingsController = DefaultQuickSettingsController( @@ -115,8 +116,8 @@ class QuickSettingsSheetDialogFragment : FenixDialogFragment() { websiteInfoView = WebsiteInfoView(binding.websiteInfoLayout, interactor = interactor) websitePermissionsView = WebsitePermissionsView(binding.websitePermissionsLayout, interactor) - trackingProtectionView = - TrackingProtectionView(binding.trackingProtectionLayout, interactor, context.settings()) + protectionsView = + ProtectionsView(binding.trackingProtectionLayout, interactor, context.settings()) clearSiteDataView = ClearSiteDataView( context = context, ioScope = viewLifecycleOwner.lifecycleScope + Dispatchers.IO, @@ -135,7 +136,7 @@ class QuickSettingsSheetDialogFragment : FenixDialogFragment() { consumeFrom(quickSettingsStore) { websiteInfoView.update(it.webInfoState) websitePermissionsView.update(it.websitePermissionsState) - trackingProtectionView.update(it.trackingProtectionState) + protectionsView.update(it.protectionsState) clearSiteDataView.update(it.webInfoState) } } @@ -210,7 +211,7 @@ class QuickSettingsSheetDialogFragment : FenixDialogFragment() { provideTrackingProtectionUseCases().fetchTrackingLogs( tab.id, onSuccess = { trackers -> - trackingProtectionView.updateDetailsSection(trackers.isNotEmpty()) + protectionsView.updateDetailsSection(trackers.isNotEmpty()) }, onError = { Logger.error("QuickSettingsSheetDialogFragment - fetchTrackingLogs onError", it) diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/TrackingProtectionView.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/TrackingProtectionView.kt deleted file mode 100644 index 42a370457..000000000 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/TrackingProtectionView.kt +++ /dev/null @@ -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) - } - } -} diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/ProtectionsInteractor.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/ProtectionsInteractor.kt new file mode 100644 index 000000000..f3db6f8ff --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/ProtectionsInteractor.kt @@ -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() +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/ProtectionsView.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/ProtectionsView.kt new file mode 100644 index 000000000..175e60de8 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/ProtectionsView.kt @@ -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") }, + ) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerDetailsController.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerDetailsController.kt new file mode 100644 index 000000000..1da84b5a4 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerDetailsController.kt @@ -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) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerDetailsInteractor.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerDetailsInteractor.kt new file mode 100644 index 000000000..e17aeb924 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerDetailsInteractor.kt @@ -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) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerHandlingDetailsView.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerHandlingDetailsView.kt new file mode 100644 index 000000000..c73c777ec --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerHandlingDetailsView.kt @@ -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) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerPanelDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerPanelDialogFragment.kt new file mode 100644 index 000000000..4eb56ad19 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerPanelDialogFragment.kt @@ -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() + 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) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionStore.kt b/app/src/main/java/org/mozilla/fenix/trackingprotection/ProtectionsStore.kt similarity index 55% rename from app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionStore.kt rename to app/src/main/java/org/mozilla/fenix/trackingprotection/ProtectionsStore.kt index cb0de8b96..e8c47abc9 100644 --- a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionStore.kt +++ b/app/src/main/java/org/mozilla/fenix/trackingprotection/ProtectionsStore.kt @@ -13,57 +13,95 @@ import mozilla.components.lib.state.Store import org.mozilla.fenix.R /** - * The [Store] for holding the [TrackingProtectionState] and applying [TrackingProtectionAction]s. + * The [Store] for holding the [ProtectionsState] and applying [ProtectionsAction]s. */ -class TrackingProtectionStore(initialState: TrackingProtectionState) : - Store( +class ProtectionsStore(initialState: ProtectionsState) : + Store( initialState, - ::trackingProtectionStateReducer, + ::protectionsStateReducer, ) /** - * Actions to dispatch through the `TrackingProtectionStore` to modify `TrackingProtectionState` through the reducer. + * Actions to dispatch through the `TrackingProtectionStore` to modify `ProtectionsState` through the reducer. */ -sealed class TrackingProtectionAction : Action { +sealed class ProtectionsAction : Action { + /** + * The values of the tracking protection view has been changed. + */ data class Change( val url: String, val isTrackingProtectionEnabled: Boolean, + val isCookieBannerHandlingEnabled: Boolean, val listTrackers: List, - val mode: TrackingProtectionState.Mode, - ) : TrackingProtectionAction() + val mode: ProtectionsState.Mode, + ) : ProtectionsAction() - data class UrlChange(val url: String) : TrackingProtectionAction() - data class TrackerLogChange(val listTrackers: List) : TrackingProtectionAction() + /** + * Toggles the enabled state of cookie banner handling protection. + * + * @param isEnabled Whether or not cookie banner protection is enabled. + */ + data class ToggleCookieBannerHandlingProtectionEnabled(val isEnabled: Boolean) : + ProtectionsAction() - object ExitDetailsMode : TrackingProtectionAction() + /** + * Indicates the url has changed. + */ + data class UrlChange(val url: String) : ProtectionsAction() + + /** + * Indicates the url has the list of trackers has been updated. + */ + data class TrackerLogChange(val listTrackers: List) : ProtectionsAction() + + /** + * Indicates the user is leaving the detailed view. + */ + object ExitDetailsMode : ProtectionsAction() + + /** + * Holds the data to show a detailed tracking protection view. + */ data class EnterDetailsMode( val category: TrackingProtectionCategory, val categoryBlocked: Boolean, - ) : - TrackingProtectionAction() + ) : ProtectionsAction() } /** - * The state for the Tracking Protection Panel + * The state for the Protections Panel * @property tab Current session to display * @property url Current URL to display * @property isTrackingProtectionEnabled Current status of tracking protection for this session * (ie is an exception) + * @property isCookieBannerHandlingEnabled Current status of cookie banner handling protection + * for this session (ie is an exception). * @property listTrackers Current Tracker Log list of blocked and loaded tracker categories * @property mode Current Mode of TrackingProtection * @property lastAccessedCategory Remembers the last accessed details category, used to move * accessibly focus after returning from details_mode */ -data class TrackingProtectionState( +data class ProtectionsState( val tab: SessionState?, val url: String, val isTrackingProtectionEnabled: Boolean, + val isCookieBannerHandlingEnabled: Boolean, val listTrackers: List, val mode: Mode, val lastAccessedCategory: String, ) : State { + /** + * Indicates the modes in which a tracking protection view could be in. + */ sealed class Mode { + /** + * Indicates that tracking protection view should not be in detail mode. + */ object Normal : Mode() + + /** + * Indicates that tracking protection view in detailed mode. + */ data class Details( val selectedCategory: TrackingProtectionCategory, val categoryBlocked: Boolean, @@ -105,32 +143,36 @@ enum class TrackingProtectionCategory( } /** - * The TrackingProtectionState Reducer. + * The [ProtectionsState] reducer. */ -fun trackingProtectionStateReducer( - state: TrackingProtectionState, - action: TrackingProtectionAction, -): TrackingProtectionState { +fun protectionsStateReducer( + state: ProtectionsState, + action: ProtectionsAction, +): ProtectionsState { return when (action) { - is TrackingProtectionAction.Change -> state.copy( + is ProtectionsAction.Change -> state.copy( url = action.url, isTrackingProtectionEnabled = action.isTrackingProtectionEnabled, + isCookieBannerHandlingEnabled = action.isCookieBannerHandlingEnabled, listTrackers = action.listTrackers, mode = action.mode, ) - is TrackingProtectionAction.UrlChange -> state.copy( + is ProtectionsAction.UrlChange -> state.copy( url = action.url, ) - is TrackingProtectionAction.TrackerLogChange -> state.copy(listTrackers = action.listTrackers) - TrackingProtectionAction.ExitDetailsMode -> state.copy( - mode = TrackingProtectionState.Mode.Normal, + is ProtectionsAction.TrackerLogChange -> state.copy(listTrackers = action.listTrackers) + ProtectionsAction.ExitDetailsMode -> state.copy( + mode = ProtectionsState.Mode.Normal, ) - is TrackingProtectionAction.EnterDetailsMode -> state.copy( - mode = TrackingProtectionState.Mode.Details( + is ProtectionsAction.EnterDetailsMode -> state.copy( + mode = ProtectionsState.Mode.Details( action.category, action.categoryBlocked, ), lastAccessedCategory = action.category.name, ) + is ProtectionsAction.ToggleCookieBannerHandlingProtectionEnabled -> state.copy( + isCookieBannerHandlingEnabled = action.isEnabled, + ) } } diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotection/SwitchWithDescription.kt b/app/src/main/java/org/mozilla/fenix/trackingprotection/SwitchWithDescription.kt index 6cb2cd780..a95d2bd6a 100644 --- a/app/src/main/java/org/mozilla/fenix/trackingprotection/SwitchWithDescription.kt +++ b/app/src/main/java/org/mozilla/fenix/trackingprotection/SwitchWithDescription.kt @@ -7,7 +7,9 @@ package org.mozilla.fenix.trackingprotection import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater +import android.widget.CompoundButton import android.widget.TextView +import androidx.annotation.VisibleForTesting import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.widget.SwitchCompat import androidx.constraintlayout.widget.ConstraintLayout @@ -15,42 +17,114 @@ import androidx.core.content.withStyledAttributes import mozilla.components.support.ktx.android.view.putCompoundDrawablesRelativeWithIntrinsicBounds import org.mozilla.fenix.R +private const val DEFAULT_DRAWABLE: Int = 0 + +/** + * Add a [SwitchCompat] widget with description that will vary depending on switch status. + */ class SwitchWithDescription @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, ) : ConstraintLayout(context, attrs, defStyleAttr) { - lateinit var switchWidget: SwitchCompat - lateinit var trackingProtectionCategoryTitle: TextView - lateinit var trackingProtectionCategoryItemDescription: TextView + private lateinit var switchWidget: SwitchCompat + private lateinit var titleWidget: TextView + private lateinit var descriptionWidget: TextView + private lateinit var descriptionOn: String + private lateinit var descriptionOff: String + private var iconOn: Int = 0 + private var iconOff: Int = 0 + private var shouldShowIcons: Boolean = true init { LayoutInflater.from(context).inflate(R.layout.switch_with_description, this, true) context.withStyledAttributes(attrs, R.styleable.SwitchWithDescription, defStyleAttr, 0) { - val id = getResourceId( - R.styleable.SwitchWithDescription_switchIcon, - R.drawable.ic_tracking_protection, - ) switchWidget = findViewById(R.id.switch_widget) - trackingProtectionCategoryTitle = findViewById(R.id.trackingProtectionCategoryTitle) - trackingProtectionCategoryItemDescription = findViewById(R.id.trackingProtectionCategoryItemDescription) - switchWidget.putCompoundDrawablesRelativeWithIntrinsicBounds( - start = AppCompatResources.getDrawable(context, id), + titleWidget = findViewById(R.id.switch_with_description_title) + descriptionWidget = findViewById(R.id.switch_with_description_description) + + switchWidget.setOnCheckedChangeListener { _, isChecked -> + onSwitchChange(isChecked) + } + + iconOn = getResourceId( + R.styleable.SwitchWithDescription_switchIconOn, + DEFAULT_DRAWABLE, + ) + iconOff = getResourceId( + R.styleable.SwitchWithDescription_switchIconOff, + DEFAULT_DRAWABLE, + ) + + shouldShowIcons = getBoolean( + R.styleable.SwitchWithDescription_switchShowIcon, + true, ) - trackingProtectionCategoryTitle.text = resources.getString( + + descriptionOn = resources.getString( getResourceId( - R.styleable.SwitchWithDescription_switchTitle, - R.string.preference_enhanced_tracking_protection, + R.styleable.SwitchWithDescription_switchDescriptionOn, + R.string.empty_string, ), ) - trackingProtectionCategoryItemDescription.text = resources.getString( + descriptionOff = resources.getString( + getResourceId( + R.styleable.SwitchWithDescription_switchDescriptionOff, + R.string.empty_string, + ), + ) + + switchWidget.textOn = descriptionOn + switchWidget.textOff = descriptionOff + + titleWidget.text = resources.getString( getResourceId( - R.styleable.SwitchWithDescription_switchDescription, - R.string.preference_enhanced_tracking_protection_explanation, + R.styleable.SwitchWithDescription_switchTitle, + R.string.empty_string, ), ) + + if (shouldShowIcons) { + switchWidget.putCompoundDrawablesRelativeWithIntrinsicBounds( + start = AppCompatResources.getDrawable(context, iconOn), + ) + } + } + } + + /** + * Add a [CompoundButton.OnCheckedChangeListener] listener to the switch view. + */ + fun setOnCheckedChangeListener(listener: CompoundButton.OnCheckedChangeListener) { + switchWidget.setOnCheckedChangeListener { item, isChecked -> + onSwitchChange(isChecked) + listener.onCheckedChanged(item, isChecked) + } + } + + /** + * Allows to query switch view isChecked state. + */ + var isChecked: Boolean + get() = switchWidget.isChecked + set(value) { + switchWidget.isChecked = value + onSwitchChange(value) + } + + @VisibleForTesting + internal fun onSwitchChange(isChecked: Boolean) { + val newDescription = if (isChecked) descriptionOn else descriptionOff + val newIcon = if (isChecked) iconOn else iconOff + + if (shouldShowIcons) { + switchWidget.putCompoundDrawablesRelativeWithIntrinsicBounds( + start = AppCompatResources.getDrawable(context, newIcon), + ) } + descriptionWidget.text = newDescription + switchWidget.jumpDrawablesToCurrentState() } } diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragment.kt index b1c0bf0b8..a5c636b60 100644 --- a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragment.kt @@ -23,9 +23,10 @@ import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.launch +import kotlinx.coroutines.plus import mozilla.components.browser.state.selector.findTabOrCustomTab import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.store.BrowserStore @@ -65,7 +66,7 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), UserInt } @VisibleForTesting - internal lateinit var trackingProtectionStore: TrackingProtectionStore + internal lateinit var protectionsStore: ProtectionsStore private lateinit var trackingProtectionView: TrackingProtectionPanelView private lateinit var trackingProtectionInteractor: TrackingProtectionPanelInteractor private lateinit var trackingProtectionUseCases: TrackingProtectionUseCases @@ -84,14 +85,15 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), UserInt val view = inflateRootView(container) val tab = store.state.findTabOrCustomTab(provideCurrentTabId()) - trackingProtectionStore = StoreProvider.get(this) { - TrackingProtectionStore( - TrackingProtectionState( + protectionsStore = StoreProvider.get(this) { + ProtectionsStore( + ProtectionsState( tab = tab, url = args.url, isTrackingProtectionEnabled = args.trackingProtectionEnabled, + isCookieBannerHandlingEnabled = args.cookieBannerHandlingEnabled, listTrackers = listOf(), - mode = TrackingProtectionState.Mode.Normal, + mode = ProtectionsState.Mode.Normal, lastAccessedCategory = "", ), ) @@ -99,7 +101,9 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), UserInt trackingProtectionInteractor = TrackingProtectionPanelInteractor( context = requireContext(), fragment = this, - store = trackingProtectionStore, + store = protectionsStore, + ioScope = viewLifecycleOwner.lifecycleScope + Dispatchers.IO, + cookieBannersStorage = requireComponents.core.cookieBannersStorage, navController = { findNavController() }, openTrackingProtectionSettings = ::openTrackingProtectionSettings, openLearnMoreLink = ::handleLearnMoreClicked, @@ -119,7 +123,7 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), UserInt trackingProtectionUseCases.fetchTrackingLogs( tab.id, onSuccess = { - trackingProtectionStore.dispatch(TrackingProtectionAction.TrackerLogChange(it)) + protectionsStore.dispatch(ProtectionsAction.TrackerLogChange(it)) }, onError = { Logger.error("TrackingProtectionUseCases - fetchTrackingLogs onError", it) @@ -133,7 +137,7 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), UserInt observeUrlChange(store) observeTrackersChange(store) - trackingProtectionStore.observe(view) { + protectionsStore.observe(view) { viewLifecycleOwner.lifecycleScope.launch { whenStarted { trackingProtectionView.update(it) @@ -217,7 +221,7 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), UserInt state.findTabOrCustomTab(provideCurrentTabId()) }.ifChanged { tab -> tab.content.url } .collect { - trackingProtectionStore.dispatch(TrackingProtectionAction.UrlChange(it.content.url)) + protectionsStore.dispatch(ProtectionsAction.UrlChange(it.content.url)) } } } diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelInteractor.kt b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelInteractor.kt index 4fe5c8748..3fae2c02d 100644 --- a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelInteractor.kt @@ -7,7 +7,12 @@ package org.mozilla.fenix.trackingprotection import android.content.Context import androidx.fragment.app.Fragment import androidx.navigation.NavController +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import mozilla.components.browser.state.state.SessionState +import mozilla.components.concept.engine.cookiehandling.CookieBannersStorage import mozilla.components.concept.engine.permission.SitePermissions import org.mozilla.fenix.browser.BrowserFragmentDirections import org.mozilla.fenix.ext.components @@ -21,7 +26,9 @@ import org.mozilla.fenix.ext.runIfFragmentIsAttached class TrackingProtectionPanelInteractor( private val context: Context, private val fragment: Fragment, - private val store: TrackingProtectionStore, + private val store: ProtectionsStore, + private val ioScope: CoroutineScope, + private val cookieBannersStorage: CookieBannersStorage, private val navController: () -> NavController, private val openTrackingProtectionSettings: () -> Unit, private val openLearnMoreLink: () -> Unit, @@ -31,7 +38,7 @@ class TrackingProtectionPanelInteractor( ) : TrackingProtectionPanelViewInteractor { override fun openDetails(category: TrackingProtectionCategory, categoryBlocked: Boolean) { - store.dispatch(TrackingProtectionAction.EnterDetailsMode(category, categoryBlocked)) + store.dispatch(ProtectionsAction.EnterDetailsMode(category, categoryBlocked)) } override fun onLearnMoreClicked() { @@ -45,28 +52,36 @@ class TrackingProtectionPanelInteractor( override fun onBackPressed() { getCurrentTab()?.let { tab -> context.components.useCases.trackingProtectionUseCases.containsException(tab.id) { contains -> - fragment.runIfFragmentIsAttached { - navController().popBackStack() - val isTrackingProtectionEnabled = tab.trackingProtection.enabled && !contains - val directions = - BrowserFragmentDirections.actionGlobalQuickSettingsSheetDialogFragment( - sessionId = tab.id, - url = tab.content.url, - title = tab.content.title, - isSecured = tab.content.securityInfo.secure, - sitePermissions = sitePermissions, - gravity = gravity, - certificateName = tab.content.securityInfo.issuer, - permissionHighlights = tab.content.permissionHighlights, - isTrackingProtectionEnabled = isTrackingProtectionEnabled, - ) - navController().navigate(directions) + ioScope.launch { + val hasException = + cookieBannersStorage.hasException(tab.content.url, tab.content.private) + withContext(Dispatchers.Main) { + fragment.runIfFragmentIsAttached { + navController().popBackStack() + val isTrackingProtectionEnabled = + tab.trackingProtection.enabled && !contains + val directions = + BrowserFragmentDirections.actionGlobalQuickSettingsSheetDialogFragment( + sessionId = tab.id, + url = tab.content.url, + title = tab.content.title, + isSecured = tab.content.securityInfo.secure, + sitePermissions = sitePermissions, + gravity = gravity, + certificateName = tab.content.securityInfo.issuer, + permissionHighlights = tab.content.permissionHighlights, + isTrackingProtectionEnabled = isTrackingProtectionEnabled, + isCookieHandlingEnabled = !hasException, + ) + navController().navigate(directions) + } + } } } } } override fun onExitDetailMode() { - store.dispatch(TrackingProtectionAction.ExitDetailsMode) + store.dispatch(ProtectionsAction.ExitDetailsMode) } } diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelView.kt b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelView.kt index d9cb2f93f..9b4377158 100644 --- a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelView.kt +++ b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelView.kt @@ -84,7 +84,7 @@ class TrackingProtectionPanelView( val view: ConstraintLayout = binding.panelWrapper - private var mode: TrackingProtectionState.Mode = TrackingProtectionState.Mode.Normal + private var mode: ProtectionsState.Mode = ProtectionsState.Mode.Normal private var bucketedTrackers = TrackerBuckets() @@ -106,13 +106,16 @@ class TrackingProtectionPanelView( setCategoryClickListeners() } - fun update(state: TrackingProtectionState) { + /** + * Updates the display mode of the Protection view. + */ + fun update(state: ProtectionsState) { mode = state.mode bucketedTrackers.updateIfNeeded(state.listTrackers) when (val mode = state.mode) { - is TrackingProtectionState.Mode.Normal -> setUIForNormalMode(state) - is TrackingProtectionState.Mode.Details -> setUIForDetailsMode( + is ProtectionsState.Mode.Normal -> setUIForNormalMode(state) + is ProtectionsState.Mode.Details -> setUIForDetailsMode( mode.selectedCategory, mode.categoryBlocked, ) @@ -121,7 +124,7 @@ class TrackingProtectionPanelView( setAccessibilityViewHierarchy(binding.detailsBack, binding.categoryTitle) } - private fun setUIForNormalMode(state: TrackingProtectionState) { + private fun setUIForNormalMode(state: ProtectionsState) { binding.detailsMode.visibility = View.GONE binding.normalMode.visibility = View.VISIBLE @@ -280,8 +283,8 @@ class TrackingProtectionPanelView( fun onBackPressed(): Boolean { return when (mode) { - is TrackingProtectionState.Mode.Details -> { - mode = TrackingProtectionState.Mode.Normal + is ProtectionsState.Mode.Details -> { + mode = ProtectionsState.Mode.Normal interactor.onBackPressed() true } diff --git a/app/src/main/res/drawable/ic_cookies_disabled.xml b/app/src/main/res/drawable/ic_cookies_disabled.xml index e5ccc68b3..fde03e1c8 100644 --- a/app/src/main/res/drawable/ic_cookies_disabled.xml +++ b/app/src/main/res/drawable/ic_cookies_disabled.xml @@ -4,12 +4,12 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + android:viewportWidth="24" + android:viewportHeight="24"> + android:pathData="M7,6V6.404L5.558,4.962C6.903,3.723 8.601,2.861 10.489,2.56L11.054,3.058C11.022,3.344 10.999,3.633 10.999,3.928C10.999,7.474 13.307,10.477 16.5,11.529V12.751L17,13.251H18.5L19,12.751V11.934H19.006C19.649,11.934 20.264,11.833 20.862,11.689L21.492,12.244C21.416,14.61 20.47,16.752 18.972,18.376L13.25,12.654V11.25L12.75,10.75H11.346L8.596,8H9L9.5,7.5V6L9,5.5H7.5L7,6ZM18.159,21.78C18.305,21.926 18.497,22 18.689,22C18.881,22 19.073,21.927 19.219,21.78C19.512,21.487 19.512,21.012 19.219,20.719L3.134,4.634C2.841,4.341 2.366,4.341 2.073,4.634C1.78,4.927 1.78,5.402 2.073,5.695L3.697,7.319C2.931,8.684 2.492,10.256 2.492,11.934C2.492,17.185 6.749,21.442 12,21.442C13.679,21.442 15.253,21.005 16.619,20.241L18.159,21.78ZM5,12.75V11.25L5.5,10.75H7L7.5,11.25V12.75L7,13.25H5.5L5,12.75ZM9.5,18L9,18.5H7.5L7,18V16.5L7.5,16H9L9.5,16.5V18Z"/> diff --git a/app/src/main/res/drawable/ic_cookies_enabled.xml b/app/src/main/res/drawable/ic_cookies_enabled.xml index 9a29d5508..184d695ed 100644 --- a/app/src/main/res/drawable/ic_cookies_enabled.xml +++ b/app/src/main/res/drawable/ic_cookies_enabled.xml @@ -4,12 +4,12 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + android:viewportWidth="24" + android:viewportHeight="24"> + android:pathData="M20.862,11.69a7.886,7.886 0,0 1,-1.856 0.244L19,11.934v0.817l-0.5,0.5L17,13.251l-0.5,-0.5L16.5,11.53c-3.193,-1.052 -5.501,-4.055 -5.501,-7.6 0,-0.296 0.023,-0.585 0.055,-0.87l-0.565,-0.499c-4.531,0.725 -7.997,4.64 -7.997,9.375A9.508,9.508 0,0 0,12 21.443c5.146,0 9.327,-4.09 9.492,-9.198l-0.63,-0.556ZM5,12.75v-1.5l0.5,-0.5L7,10.75l0.5,0.5v1.5l-0.5,0.5L5.5,13.25l-0.5,-0.5ZM9.5,18l-0.5,0.5L7.5,18.5L7,18v-1.5l0.5,-0.5L9,16l0.5,0.5L9.5,18ZM9.5,7.5L9,8L7.5,8L7,7.5L7,6l0.5,-0.5L9,5.5l0.5,0.5v1.5ZM13.25,12.75 L12.75,13.25h-1.5l-0.5,-0.5v-1.5l0.5,-0.5h1.5l0.5,0.5v1.5ZM17,18l-0.5,0.5L15,18.5l-0.5,-0.5v-1.5l0.5,-0.5h1.5l0.5,0.5L17,18Z" /> diff --git a/app/src/main/res/drawable/ic_tracking_protection.xml b/app/src/main/res/drawable/ic_tracking_protection.xml deleted file mode 100644 index 64f2d3cac..000000000 --- a/app/src/main/res/drawable/ic_tracking_protection.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/layout/component_cookie_banner_details_panel.xml b/app/src/main/res/layout/component_cookie_banner_details_panel.xml new file mode 100644 index 000000000..82f2c7dfb --- /dev/null +++ b/app/src/main/res/layout/component_cookie_banner_details_panel.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_cookie_banner_handling_details_dialog.xml b/app/src/main/res/layout/fragment_cookie_banner_handling_details_dialog.xml new file mode 100644 index 000000000..1910cd35c --- /dev/null +++ b/app/src/main/res/layout/fragment_cookie_banner_handling_details_dialog.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/layout/quicksettings_tracking_protection.xml b/app/src/main/res/layout/quicksettings_protections_panel.xml similarity index 60% rename from app/src/main/res/layout/quicksettings_tracking_protection.xml rename to app/src/main/res/layout/quicksettings_protections_panel.xml index 24fd205c7..cbfa0a812 100644 --- a/app/src/main/res/layout/quicksettings_tracking_protection.xml +++ b/app/src/main/res/layout/quicksettings_protections_panel.xml @@ -1,24 +1,33 @@ - - - + + @@ -51,10 +51,7 @@ android:minHeight="@dimen/tracking_protection_item_height" android:layout_width="match_parent" android:layout_height="match_parent" - android:textOff="@string/etp_panel_off" - android:textOn="@string/etp_panel_on" - app:drawableStartCompat="@drawable/ic_tracking_protection" - app:layout_constraintBottom_toBottomOf="@id/trackingProtectionCategoryItemDescription" + app:layout_constraintBottom_toBottomOf="@id/switch_with_description_description" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index d11fec016..d0591a92c 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -130,6 +130,9 @@ + @@ -953,6 +956,9 @@ + + + + + + + + + + + + - - + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cb121c36f..df47f228f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -329,6 +329,18 @@ Reduce cookie banners Firefox automatically tries to reject cookie requests on cookie banners. If a reject option isn’t available, Firefox may accept all cookies to dismiss the banner. + + Off for this site + + On for this site + + Turn on Cookie Banner Reduction for %1$s? + + Turn off Cookie Banner Reduction for %1$s? + + %1$s will clear this site’s cookies and refresh the page. Clearing all cookies may sign you out or empty shopping carts. + + Firefox can try to automatically reject cookie requests. If a reject option isn’t available, Firefox may accept all cookies to dismiss the banner. Automatically attempts to connect to sites using HTTPS encryption protocol for increased security. diff --git a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/DefaultConnectionDetailsControllerTest.kt b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/DefaultConnectionDetailsControllerTest.kt index a97811289..4685ea80a 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/DefaultConnectionDetailsControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/DefaultConnectionDetailsControllerTest.kt @@ -17,10 +17,13 @@ import io.mockk.spyk import io.mockk.verify import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.createTab +import mozilla.components.concept.engine.cookiehandling.CookieBannersStorage import mozilla.components.concept.engine.permission.SitePermissions import mozilla.components.feature.session.TrackingProtectionUseCases import mozilla.components.support.test.robolectric.testContext +import mozilla.components.support.test.rule.MainCoroutineRule import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mozilla.fenix.ext.components @@ -40,12 +43,19 @@ class DefaultConnectionDetailsControllerTest { @MockK(relaxed = true) private lateinit var sitePermissions: SitePermissions + @MockK(relaxed = true) + private lateinit var cookieBannersStorage: CookieBannersStorage + private lateinit var controller: DefaultConnectionDetailsController private lateinit var tab: TabSessionState private var gravity = 54 + @get:Rule + val coroutinesTestRule = MainCoroutineRule() + private val scope = coroutinesTestRule.scope + @Before fun setUp() { MockKAnnotations.init(this) @@ -55,6 +65,8 @@ class DefaultConnectionDetailsControllerTest { controller = DefaultConnectionDetailsController( fragment = fragment, context = context, + ioScope = scope, + cookieBannersStorage = cookieBannersStorage, navController = { navController }, sitePermissions = sitePermissions, gravity = gravity, diff --git a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/DefaultQuickSettingsControllerTest.kt b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/DefaultQuickSettingsControllerTest.kt index 947f03852..cdbf807c5 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/DefaultQuickSettingsControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/DefaultQuickSettingsControllerTest.kt @@ -39,6 +39,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mozilla.fenix.GleanMetrics.CookieBanners import org.mozilla.fenix.GleanMetrics.TrackingProtection import org.mozilla.fenix.components.PermissionStorage import org.mozilla.fenix.ext.components @@ -303,6 +304,23 @@ class DefaultQuickSettingsControllerTest { } } + @Test + fun `handleCookieBannerHandlingDetailsClicked should call popBackStack and navigate to details page`() { + every { context.components.core.store } returns browserStore + every { store.state.protectionsState } returns mockk(relaxed = true) + every { context.components.settings } returns appSettings + every { context.components.settings.toolbarPosition.androidGravity } returns mockk(relaxed = true) + + controller.handleCookieBannerHandlingDetailsClicked() + + verify { + navController.popBackStack() + + navController.navigate(any()) + } + assertNotNull(CookieBanners.visitedPanel.testGetValue()) + } + @Test fun `handleTrackingProtectionToggled should call the right use cases`() = runTestOnMain { val trackingProtectionUseCases: TrackingProtectionUseCases = mockk(relaxed = true) @@ -348,11 +366,12 @@ class DefaultQuickSettingsControllerTest { websiteUrl = tab.content.url, sessionId = tab.id, isTrackingProtectionEnabled = isTrackingProtectionEnabled, + isCookieHandlingEnabled = isTrackingProtectionEnabled, ) - every { store.state.trackingProtectionState } returns state + every { store.state.protectionsState } returns state - controller.handleDetailsClicked() + controller.handleTrackingProtectionDetailsClicked() verify { navController.popBackStack() diff --git a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/ProtectionsViewTest.kt b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/ProtectionsViewTest.kt new file mode 100644 index 000000000..39a88ca0f --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/ProtectionsViewTest.kt @@ -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) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentReducerTest.kt b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentReducerTest.kt index a44f3d359..7fb4206c7 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentReducerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentReducerTest.kt @@ -12,8 +12,8 @@ import org.junit.Assert.assertNotSame import org.junit.Assert.assertTrue import org.junit.Test import org.mozilla.fenix.settings.PhoneFeature -import org.mozilla.fenix.trackingprotection.TrackingProtectionState -import org.mozilla.fenix.trackingprotection.TrackingProtectionState.Mode.Normal +import org.mozilla.fenix.trackingprotection.ProtectionsState +import org.mozilla.fenix.trackingprotection.ProtectionsState.Mode.Normal class QuickSettingsFragmentReducerTest { @@ -30,13 +30,14 @@ class QuickSettingsFragmentReducerTest { val map = mapOf(PhoneFeature.CAMERA to toggleablePermission) val infoState = WebsiteInfoState("", "", WebsiteSecurityUiValues.SECURE, "") - val tpState = TrackingProtectionState( + val tpState = ProtectionsState( null, "", - false, - emptyList(), - Normal, - "", + isTrackingProtectionEnabled = false, + isCookieBannerHandlingEnabled = false, + listTrackers = emptyList(), + mode = Normal, + lastAccessedCategory = "", ) val state = QuickSettingsFragmentState(infoState, map, tpState) val newState = quickSettingsFragmentReducer( @@ -67,13 +68,14 @@ class QuickSettingsFragmentReducerTest { val map = mapOf(PhoneFeature.AUTOPLAY to permissionPermission) val infoState = WebsiteInfoState("", "", WebsiteSecurityUiValues.SECURE, "") - val tpState = TrackingProtectionState( + val tpState = ProtectionsState( null, "", - false, - emptyList(), - Normal, - "", + isTrackingProtectionEnabled = false, + isCookieBannerHandlingEnabled = false, + listTrackers = emptyList(), + mode = Normal, + lastAccessedCategory = "", ) val state = QuickSettingsFragmentState(infoState, map, tpState) val autoplayValue = AutoplayValue.AllowAll( @@ -92,14 +94,15 @@ class QuickSettingsFragmentReducerTest { } @Test - fun `TrackingProtectionAction - ToggleTrackingProtectionEnabled`() = runTest { + fun `ProtectionsAction - ToggleTrackingProtectionEnabled`() = runTest { val state = QuickSettingsFragmentState( webInfoState = WebsiteInfoState("", "", WebsiteSecurityUiValues.SECURE, ""), websitePermissionsState = emptyMap(), - trackingProtectionState = TrackingProtectionState( + protectionsState = ProtectionsState( tab = null, url = "https://www.firefox.com", isTrackingProtectionEnabled = true, + isCookieBannerHandlingEnabled = true, listTrackers = listOf(), mode = Normal, lastAccessedCategory = "", @@ -112,7 +115,7 @@ class QuickSettingsFragmentReducerTest { ) assertNotSame(state, newState) - assertFalse(newState.trackingProtectionState.isTrackingProtectionEnabled) + assertFalse(newState.protectionsState.isTrackingProtectionEnabled) } private fun createTestRule() = SitePermissionsRules( diff --git a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStoreTest.kt b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStoreTest.kt index 98f71d78d..b36120a5a 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStoreTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStoreTest.kt @@ -39,7 +39,7 @@ import org.mozilla.fenix.settings.quicksettings.WebsiteInfoState.Companion.creat import org.mozilla.fenix.settings.quicksettings.ext.shouldBeEnabled import org.mozilla.fenix.settings.quicksettings.ext.shouldBeVisible import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_BLOCK_ALL -import org.mozilla.fenix.trackingprotection.TrackingProtectionState +import org.mozilla.fenix.trackingprotection.ProtectionsState import org.mozilla.fenix.utils.Settings @RunWith(FenixRobolectricTestRunner::class) @@ -83,13 +83,14 @@ class QuickSettingsFragmentStoreTest { settings = appSettings, sessionId = tab.id, isTrackingProtectionEnabled = true, + isCookieHandlingEnabled = true, ) assertNotNull(store) assertNotNull(store.state) assertNotNull(store.state.webInfoState) assertNotNull(store.state.websitePermissionsState) - assertNotNull(store.state.trackingProtectionState) + assertNotNull(store.state.protectionsState) } @Test @@ -286,7 +287,7 @@ class QuickSettingsFragmentStoreTest { val initialState = QuickSettingsFragmentState( webInfoState = websiteInfoState, websitePermissionsState = initialWebsitePermissionsState, - trackingProtectionState = mockk(), + protectionsState = mockk(), ) val store = QuickSettingsFragmentStore(initialState) @@ -340,6 +341,7 @@ class QuickSettingsFragmentStoreTest { val tab = createTab("https://www.firefox.com") val browserStore = BrowserStore(BrowserState(tabs = listOf(tab))) val isTrackingProtectionEnabled = true + val isCookieHandlingEnabled = true every { context.components.core.store } returns browserStore @@ -348,14 +350,16 @@ class QuickSettingsFragmentStoreTest { websiteUrl = tab.content.url, sessionId = tab.id, isTrackingProtectionEnabled = isTrackingProtectionEnabled, + isCookieHandlingEnabled = isCookieHandlingEnabled, ) assertNotNull(state) assertEquals(tab, state.tab) assertEquals(tab.content.url, state.url) assertEquals(isTrackingProtectionEnabled, state.isTrackingProtectionEnabled) + assertEquals(isCookieHandlingEnabled, state.isCookieBannerHandlingEnabled) assertEquals(0, state.listTrackers.size) - assertEquals(TrackingProtectionState.Mode.Normal, state.mode) + assertEquals(ProtectionsState.Mode.Normal, state.mode) assertEquals("", state.lastAccessedCategory) } diff --git a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractorTest.kt index e86e359fe..c9adbf05a 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractorTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractorTest.kt @@ -65,12 +65,21 @@ class QuickSettingsInteractorTest { } } + @Test + fun `onCookieBannerHandlingClicked should delegate the controller`() { + interactor.onCookieBannerHandlingDetailsClicked() + + verify { + controller.handleCookieBannerHandlingDetailsClicked() + } + } + @Test fun `onBlockedItemsClicked should delegate the controller`() { - interactor.onDetailsClicked() + interactor.onTrackingProtectionDetailsClicked() verify { - controller.handleDetailsClicked() + controller.handleTrackingProtectionDetailsClicked() } } diff --git a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragmentTest.kt b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragmentTest.kt index aea26c844..48a003da1 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragmentTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragmentTest.kt @@ -29,6 +29,7 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mozilla.fenix.helpers.FenixRobolectricTestRunner +import org.mozilla.fenix.settings.quicksettings.protections.ProtectionsView @RunWith(FenixRobolectricTestRunner::class) class QuickSettingsSheetDialogFragmentTest { @@ -107,11 +108,11 @@ class QuickSettingsSheetDialogFragmentTest { fun `GIVEN no trackers WHEN calling updateTrackers THEN hide the details section`() { val tab = createTab("mozilla.org") val trackingProtectionUseCases: TrackingProtectionUseCases = mockk(relaxed = true) - val trackingProtectionView: TrackingProtectionView = mockk(relaxed = true) + val protectionsView: ProtectionsView = mockk(relaxed = true) val onComplete = slot<(List) -> Unit>() - every { fragment.trackingProtectionView } returns trackingProtectionView + every { fragment.protectionsView } returns protectionsView every { trackingProtectionUseCases.fetchTrackingLogs.invoke( @@ -126,7 +127,7 @@ class QuickSettingsSheetDialogFragmentTest { fragment.updateTrackers(tab) verify { - trackingProtectionView.updateDetailsSection(false) + protectionsView.updateDetailsSection(false) } } @@ -134,11 +135,11 @@ class QuickSettingsSheetDialogFragmentTest { fun `GIVEN trackers WHEN calling updateTrackers THEN show the details section`() { val tab = createTab("mozilla.org") val trackingProtectionUseCases: TrackingProtectionUseCases = mockk(relaxed = true) - val trackingProtectionView: TrackingProtectionView = mockk(relaxed = true) + val protectionsView: ProtectionsView = mockk(relaxed = true) val onComplete = slot<(List) -> Unit>() - every { fragment.trackingProtectionView } returns trackingProtectionView + every { fragment.protectionsView } returns protectionsView every { trackingProtectionUseCases.fetchTrackingLogs.invoke( @@ -153,7 +154,7 @@ class QuickSettingsSheetDialogFragmentTest { fragment.updateTrackers(tab) verify { - trackingProtectionView.updateDetailsSection(true) + protectionsView.updateDetailsSection(true) } } diff --git a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/TrackingProtectionViewTest.kt b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/TrackingProtectionViewTest.kt deleted file mode 100644 index b40b877e2..000000000 --- a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/TrackingProtectionViewTest.kt +++ /dev/null @@ -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) - } -} diff --git a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerHandlingDetailsViewTest.kt b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerHandlingDetailsViewTest.kt new file mode 100644 index 000000000..dfa9e90f3 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerHandlingDetailsViewTest.kt @@ -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) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/DefaultCookieBannerDetailsControllerTest.kt b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/DefaultCookieBannerDetailsControllerTest.kt new file mode 100644 index 000000000..1745a52a3 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/DefaultCookieBannerDetailsControllerTest.kt @@ -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()) + } + } + + @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()) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/DefaultCookieBannerDetailsInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/DefaultCookieBannerDetailsInteractorTest.kt new file mode 100644 index 000000000..cdf17bcef --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/DefaultCookieBannerDetailsInteractorTest.kt @@ -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) + } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionStoreTest.kt b/app/src/test/java/org/mozilla/fenix/trackingprotection/ProtectionsStoreTest.kt similarity index 60% rename from app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionStoreTest.kt rename to app/src/test/java/org/mozilla/fenix/trackingprotection/ProtectionsStoreTest.kt index 73a5e5f29..06621efad 100644 --- a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionStoreTest.kt +++ b/app/src/test/java/org/mozilla/fenix/trackingprotection/ProtectionsStoreTest.kt @@ -10,19 +10,20 @@ import mozilla.components.browser.state.state.SessionState import mozilla.components.concept.engine.content.blocking.TrackerLog import org.junit.Assert.assertEquals import org.junit.Assert.assertNotSame +import org.junit.Assert.assertTrue import org.junit.Test -class TrackingProtectionStoreTest { +class ProtectionsStoreTest { val tab: SessionState = mockk(relaxed = true) @Test fun enterDetailsMode() = runTest { val initialState = defaultState() - val store = TrackingProtectionStore(initialState) + val store = ProtectionsStore(initialState) store.dispatch( - TrackingProtectionAction.EnterDetailsMode( + ProtectionsAction.EnterDetailsMode( TrackingProtectionCategory.FINGERPRINTERS, true, ), @@ -31,7 +32,7 @@ class TrackingProtectionStoreTest { assertNotSame(initialState, store.state) assertEquals( store.state.mode, - TrackingProtectionState.Mode.Details(TrackingProtectionCategory.FINGERPRINTERS, true), + ProtectionsState.Mode.Details(TrackingProtectionCategory.FINGERPRINTERS, true), ) assertEquals(store.state.lastAccessedCategory, TrackingProtectionCategory.FINGERPRINTERS.name) } @@ -39,13 +40,13 @@ class TrackingProtectionStoreTest { @Test fun exitDetailsMode() = runTest { val initialState = detailsState() - val store = TrackingProtectionStore(initialState) + val store = ProtectionsStore(initialState) - store.dispatch(TrackingProtectionAction.ExitDetailsMode).join() + store.dispatch(ProtectionsAction.ExitDetailsMode).join() assertNotSame(initialState, store.state) assertEquals( store.state.mode, - TrackingProtectionState.Mode.Normal, + ProtectionsState.Mode.Normal, ) assertEquals(store.state.lastAccessedCategory, initialState.lastAccessedCategory) } @@ -53,10 +54,10 @@ class TrackingProtectionStoreTest { @Test fun trackerListChanged() = runTest { val initialState = defaultState() - val store = TrackingProtectionStore(initialState) + val store = ProtectionsStore(initialState) val tracker = TrackerLog("url", listOf()) - store.dispatch(TrackingProtectionAction.TrackerLogChange(listOf(tracker))).join() + store.dispatch(ProtectionsAction.TrackerLogChange(listOf(tracker))).join() assertNotSame(initialState, store.state) assertEquals( listOf(tracker), @@ -67,9 +68,9 @@ class TrackingProtectionStoreTest { @Test fun urlChanged() = runTest { val initialState = defaultState() - val store = TrackingProtectionStore(initialState) + val store = ProtectionsStore(initialState) - store.dispatch(TrackingProtectionAction.UrlChange("newURL")).join() + store.dispatch(ProtectionsAction.UrlChange("newURL")).join() assertNotSame(initialState, store.state) assertEquals( "newURL", @@ -80,15 +81,16 @@ class TrackingProtectionStoreTest { @Test fun onChange() = runTest { val initialState = defaultState() - val store = TrackingProtectionStore(initialState) + val store = ProtectionsStore(initialState) val tracker = TrackerLog("url", listOf(), listOf(), cookiesHasBeenBlocked = false) store.dispatch( - TrackingProtectionAction.Change( + ProtectionsAction.Change( "newURL", - false, - listOf(tracker), - TrackingProtectionState.Mode.Details( + isTrackingProtectionEnabled = false, + isCookieBannerHandlingEnabled = false, + listTrackers = listOf(tracker), + mode = ProtectionsState.Mode.Details( TrackingProtectionCategory.FINGERPRINTERS, true, ), @@ -103,31 +105,51 @@ class TrackingProtectionStoreTest { false, store.state.isTrackingProtectionEnabled, ) + assertEquals( + false, + store.state.isCookieBannerHandlingEnabled, + ) assertEquals( listOf(tracker), store.state.listTrackers, ) assertEquals( - TrackingProtectionState.Mode.Details(TrackingProtectionCategory.FINGERPRINTERS, true), + ProtectionsState.Mode.Details(TrackingProtectionCategory.FINGERPRINTERS, true), store.state.mode, ) } - private fun defaultState(): TrackingProtectionState = TrackingProtectionState( + @Test + fun `ProtectionsAction - ToggleCookieBannerHandlingProtectionEnabled`() = runTest { + val initialState = defaultState() + val store = ProtectionsStore(initialState) + + store.dispatch( + ProtectionsAction.ToggleCookieBannerHandlingProtectionEnabled( + isEnabled = true, + ), + ).join() + + assertTrue(store.state.isCookieBannerHandlingEnabled) + } + + private fun defaultState(): ProtectionsState = ProtectionsState( tab = tab, url = "www.mozilla.org", isTrackingProtectionEnabled = true, + isCookieBannerHandlingEnabled = false, listTrackers = listOf(), - mode = TrackingProtectionState.Mode.Normal, + mode = ProtectionsState.Mode.Normal, lastAccessedCategory = "", ) - private fun detailsState(): TrackingProtectionState = TrackingProtectionState( + private fun detailsState(): ProtectionsState = ProtectionsState( tab = tab, url = "www.mozilla.org", isTrackingProtectionEnabled = true, + isCookieBannerHandlingEnabled = false, listTrackers = listOf(), - mode = TrackingProtectionState.Mode.Details(TrackingProtectionCategory.CRYPTOMINERS, true), + mode = ProtectionsState.Mode.Details(TrackingProtectionCategory.CRYPTOMINERS, true), lastAccessedCategory = TrackingProtectionCategory.CRYPTOMINERS.name, ) } diff --git a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragmentTest.kt b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragmentTest.kt index 7a7836188..6441b93f3 100644 --- a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragmentTest.kt +++ b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragmentTest.kt @@ -50,32 +50,32 @@ class TrackingProtectionPanelDialogFragmentTest { @Test fun `WHEN the url is updated THEN the url view is updated`() { - val trackingProtectionStore: TrackingProtectionStore = mockk(relaxed = true) + val protectionsStore: ProtectionsStore = mockk(relaxed = true) val tab = createTab("mozilla.org") - every { fragment.trackingProtectionStore } returns trackingProtectionStore + every { fragment.protectionsStore } returns protectionsStore every { fragment.provideCurrentTabId() } returns tab.id fragment.observeUrlChange(store) addAndSelectTab(tab) verify(exactly = 1) { - trackingProtectionStore.dispatch(TrackingProtectionAction.UrlChange("mozilla.org")) + protectionsStore.dispatch(ProtectionsAction.UrlChange("mozilla.org")) } store.dispatch(ContentAction.UpdateUrlAction(tab.id, "wikipedia.org")).joinBlocking() verify(exactly = 1) { - trackingProtectionStore.dispatch(TrackingProtectionAction.UrlChange("wikipedia.org")) + protectionsStore.dispatch(ProtectionsAction.UrlChange("wikipedia.org")) } } @Test fun `WHEN a tracker is loaded THEN trackers view is updated`() { - val trackingProtectionStore: TrackingProtectionStore = mockk(relaxed = true) + val protectionsStore: ProtectionsStore = mockk(relaxed = true) val tab = createTab("mozilla.org") - every { fragment.trackingProtectionStore } returns trackingProtectionStore + every { fragment.protectionsStore } returns protectionsStore every { fragment.provideCurrentTabId() } returns tab.id every { fragment.updateTrackers(any()) } returns Unit @@ -99,10 +99,10 @@ class TrackingProtectionPanelDialogFragmentTest { @Test fun `WHEN a tracker is blocked THEN trackers view is updated`() { - val trackingProtectionStore: TrackingProtectionStore = mockk(relaxed = true) + val protectionsStore: ProtectionsStore = mockk(relaxed = true) val tab = createTab("mozilla.org") - every { fragment.trackingProtectionStore } returns trackingProtectionStore + every { fragment.protectionsStore } returns protectionsStore every { fragment.provideCurrentTabId() } returns tab.id every { fragment.updateTrackers(any()) } returns Unit diff --git a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelInteractorTest.kt index bedab0cc7..13bfc0937 100644 --- a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelInteractorTest.kt +++ b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelInteractorTest.kt @@ -9,6 +9,7 @@ import androidx.fragment.app.Fragment import androidx.navigation.NavController import androidx.navigation.NavDirections import io.mockk.MockKAnnotations +import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.mockk @@ -17,11 +18,15 @@ import io.mockk.spyk import io.mockk.verify import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.createTab +import mozilla.components.concept.engine.cookiehandling.CookieBannersStorage import mozilla.components.concept.engine.permission.SitePermissions import mozilla.components.feature.session.TrackingProtectionUseCases import mozilla.components.support.test.robolectric.testContext +import mozilla.components.support.test.rule.MainCoroutineRule +import mozilla.components.support.test.rule.runTestOnMain import org.junit.Assert.assertEquals import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mozilla.fenix.ext.components @@ -42,12 +47,16 @@ class TrackingProtectionPanelInteractorTest { private lateinit var sitePermissions: SitePermissions @MockK(relaxed = true) - private lateinit var store: TrackingProtectionStore + private lateinit var store: ProtectionsStore private lateinit var interactor: TrackingProtectionPanelInteractor private lateinit var tab: TabSessionState + @get:Rule + val coroutinesTestRule = MainCoroutineRule() + private val scope = coroutinesTestRule.scope + private var learnMoreClicked = false private var openSettings = false private var gravity = 54 @@ -59,11 +68,14 @@ class TrackingProtectionPanelInteractorTest { context = spyk(testContext) tab = createTab("https://mozilla.org") + val cookieBannersStorage: CookieBannersStorage = mockk(relaxed = true) interactor = TrackingProtectionPanelInteractor( context = context, fragment = fragment, store = store, + ioScope = scope, + cookieBannersStorage = cookieBannersStorage, navController = { navController }, openTrackingProtectionSettings = { openSettings = true }, openLearnMoreLink = { learnMoreClicked = true }, @@ -92,7 +104,7 @@ class TrackingProtectionPanelInteractorTest { verify { store.dispatch( - TrackingProtectionAction.EnterDetailsMode( + ProtectionsAction.EnterDetailsMode( TrackingProtectionCategory.FINGERPRINTERS, true, ), @@ -103,7 +115,7 @@ class TrackingProtectionPanelInteractorTest { verify { store.dispatch( - TrackingProtectionAction.EnterDetailsMode( + ProtectionsAction.EnterDetailsMode( TrackingProtectionCategory.REDIRECT_TRACKERS, true, ), @@ -126,10 +138,10 @@ class TrackingProtectionPanelInteractorTest { } @Test - fun `WHEN onBackPressed is called THEN call popBackStack and navigate`() { + fun `WHEN onBackPressed is called THEN call popBackStack and navigate`() = runTestOnMain { interactor.onBackPressed() - verify { + coVerify { navController.popBackStack() navController.navigate(any()) @@ -140,6 +152,6 @@ class TrackingProtectionPanelInteractorTest { fun `WHEN onExitDetailMode is called THEN store should dispatch ExitDetailsMode action`() { interactor.onExitDetailMode() - verify { store.dispatch(TrackingProtectionAction.ExitDetailsMode) } + verify { store.dispatch(ProtectionsAction.ExitDetailsMode) } } } diff --git a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelViewTest.kt b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelViewTest.kt index 863f97dba..2d31a312e 100644 --- a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelViewTest.kt +++ b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelViewTest.kt @@ -37,12 +37,13 @@ class TrackingProtectionPanelViewTest { private lateinit var container: ViewGroup private lateinit var interactor: TrackingProtectionPanelInteractor private lateinit var view: TrackingProtectionPanelView - private val baseState = TrackingProtectionState( + private val baseState = ProtectionsState( tab = null, url = "", isTrackingProtectionEnabled = false, + isCookieBannerHandlingEnabled = false, listTrackers = emptyList(), - mode = TrackingProtectionState.Mode.Normal, + mode = ProtectionsState.Mode.Normal, lastAccessedCategory = "", ) @@ -61,7 +62,7 @@ class TrackingProtectionPanelViewTest { mockkStatic("org.mozilla.fenix.ext.ContextKt") { every { any().settings() } returns mockk(relaxed = true) - view.update(baseState.copy(mode = TrackingProtectionState.Mode.Normal)) + view.update(baseState.copy(mode = ProtectionsState.Mode.Normal)) assertFalse(view.binding.detailsMode.isVisible) assertTrue(view.binding.normalMode.isVisible) assertTrue(view.binding.protectionSettings.isVisible) @@ -78,7 +79,7 @@ class TrackingProtectionPanelViewTest { } val expectedTitle = testContext.getString(R.string.etp_cookies_title_2) - view.update(baseState.copy(mode = TrackingProtectionState.Mode.Normal)) + view.update(baseState.copy(mode = ProtectionsState.Mode.Normal)) assertEquals(expectedTitle, view.binding.crossSiteTracking.text) assertEquals(expectedTitle, view.binding.crossSiteTrackingLoaded.text) @@ -93,7 +94,7 @@ class TrackingProtectionPanelViewTest { } val expectedTitle = testContext.getString(R.string.etp_cookies_title) - view.update(baseState.copy(mode = TrackingProtectionState.Mode.Normal)) + view.update(baseState.copy(mode = ProtectionsState.Mode.Normal)) assertEquals(expectedTitle, view.binding.crossSiteTracking.text) assertEquals(expectedTitle, view.binding.crossSiteTrackingLoaded.text) @@ -104,7 +105,7 @@ class TrackingProtectionPanelViewTest { fun testPrivateModeUi() { view.update( baseState.copy( - mode = TrackingProtectionState.Mode.Details( + mode = ProtectionsState.Mode.Details( selectedCategory = TrackingProtectionCategory.TRACKING_CONTENT, categoryBlocked = false, ), @@ -137,7 +138,7 @@ class TrackingProtectionPanelViewTest { view.update( baseState.copy( - mode = TrackingProtectionState.Mode.Details( + mode = ProtectionsState.Mode.Details( selectedCategory = CROSS_SITE_TRACKING_COOKIES, categoryBlocked = false, ), @@ -160,7 +161,7 @@ class TrackingProtectionPanelViewTest { view.update( baseState.copy( - mode = TrackingProtectionState.Mode.Details( + mode = ProtectionsState.Mode.Details( selectedCategory = CROSS_SITE_TRACKING_COOKIES, categoryBlocked = false, ),