commit da73d00805a26f2b728f09b2852eef137995c0e8 parent ab4335e0195df50ef1f61671e1c85ceb0a6650df Author: t-p-white <towhite@mozilla.com> Date: Tue, 4 Nov 2025 13:49:21 +0000 Bug 1992306 - Add Android support for Nimbus advanced targeting for ToU experience points r=android-reviewers,rebecatudor273 This patch adds advanced targeting support for categories A & B. C will be implemented in the Nimbus configuration file in a separate Nimbus patch. Differential Revision: https://phabricator.services.mozilla.com/D270859 Diffstat:
12 files changed, 896 insertions(+), 0 deletions(-)
diff --git a/mobile/android/fenix/app/metrics.yaml b/mobile/android/fenix/app/metrics.yaml @@ -12935,6 +12935,8 @@ nimbus_system: type: array items: type: string + tou_points: + type: number description: | The Nimbus context object that is recorded to Glean bugs: @@ -12945,6 +12947,7 @@ nimbus_system: - 'https://bugzilla.mozilla.org/show_bug.cgi?id=1984639' - 'https://bugzilla.mozilla.org/show_bug.cgi?id=1991528' - 'https://bugzilla.mozilla.org/show_bug.cgi?id=1993034' + - 'https://bugzilla.mozilla.org/show_bug.cgi?id=1992306' data_reviews: - 'https://bugzilla.mozilla.org/show_bug.cgi?id=1898552#c3' - 'https://phabricator.services.mozilla.com/D258942' @@ -12953,6 +12956,7 @@ nimbus_system: - 'https://phabricator.services.mozilla.com/D262139' - 'https://phabricator.services.mozilla.com/D268391' - 'https://phabricator.services.mozilla.com/D268931' + - 'https://phabricator.services.mozilla.com/D270859' data_sensitivity: - interaction notification_emails: diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/experiments/RecordedNimbusContext.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/experiments/RecordedNimbusContext.kt @@ -19,6 +19,8 @@ import org.mozilla.fenix.GleanMetrics.NimbusSystem import org.mozilla.fenix.GleanMetrics.Pings import org.mozilla.fenix.ext.settings import org.mozilla.fenix.home.pocket.ContentRecommendationsFeatureHelper +import org.mozilla.fenix.termsofuse.experimentation.TermsOfUseAdvancedTargetingHelper +import org.mozilla.fenix.termsofuse.experimentation.utils.DefaultTermsOfUseDataProvider import org.mozilla.fenix.utils.Settings import java.io.File @@ -64,6 +66,7 @@ class RecordedNimbusContext( val userAcceptedTou: Boolean, val noShortcutsOrStoriesOptOuts: Boolean, val addonIds: List<String>, + val touPoints: Int?, ) : RecordedContext { /** * [getEventQueries] is called by the Nimbus SDK Rust code to retrieve the map of event @@ -105,6 +108,7 @@ class RecordedNimbusContext( userAcceptedTou = userAcceptedTou, noShortcutsOrStoriesOptOuts = noShortcutsOrStoriesOptOuts, addonIds = NimbusSystem.RecordedNimbusContextObjectAddonIds(addonIds.toMutableList()), + touPoints = touPoints, ), ) Pings.nimbus.submit() @@ -151,6 +155,7 @@ class RecordedNimbusContext( "user_accepted_tou" to userAcceptedTou, "no_shortcuts_or_stories_opt_outs" to noShortcutsOrStoriesOptOuts, "addon_ids" to JSONArray(addonIds), + "tou_points" to touPoints, ), ) return obj @@ -172,6 +177,9 @@ class RecordedNimbusContext( isFirstRun: Boolean, ): RecordedNimbusContext { val settings = context.settings() + val termsOfUseAdvancedTargetingHelper = TermsOfUseAdvancedTargetingHelper( + DefaultTermsOfUseDataProvider(settings), + ) val packageInfo = context.packageManager.getPackageInfoCompat(context.packageName, 0) val deviceInfo = NimbusDeviceInfo.default() @@ -199,6 +207,7 @@ class RecordedNimbusContext( userAcceptedTou = settings.hasAcceptedTermsOfService, noShortcutsOrStoriesOptOuts = settings.noShortcutsOrStoriesOptOuts(context), addonIds = getFormattedAddons(settings), + touPoints = termsOfUseAdvancedTargetingHelper.getTouPoints(), ) } @@ -261,6 +270,7 @@ class RecordedNimbusContext( userAcceptedTou = true, noShortcutsOrStoriesOptOuts = true, addonIds = addonIds, + touPoints = 3, ) } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/termsofuse/experimentation/TermsOfUseAdvancedTargetingHelper.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/termsofuse/experimentation/TermsOfUseAdvancedTargetingHelper.kt @@ -0,0 +1,73 @@ +/* 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.termsofuse.experimentation + +import androidx.annotation.VisibleForTesting +import mozilla.components.support.locale.LocaleManager.getSystemDefault +import org.mozilla.fenix.termsofuse.experimentation.utils.TermsOfUseDataProvider +import org.mozilla.fenix.termsofuse.experimentation.utils.supportedSponsoredShortcutsRegions +import org.mozilla.fenix.termsofuse.experimentation.utils.supportedSponsoredStoriesRegions + +/** + * Helper class for Terms of Use advanced targeting options. + * + * @param termsOfUseDataProvider provides Terms of Use related data. + * @param systemLanguageTag the language tag of the system locale. + */ +class TermsOfUseAdvancedTargetingHelper( + private val termsOfUseDataProvider: TermsOfUseDataProvider, + private val systemLanguageTag: String = getSystemDefault().toLanguageTag(), +) { + /** + * Gets the users 'points' for Advanced Targeting. + */ + fun getTouPoints() = privacySettingsPoints().plus(sponsoredContentPoints()) + + @VisibleForTesting + internal fun privacySettingsPoints() = with(termsOfUseDataProvider) { + val enabledEtpStrictMode = useStrictTrackingProtection + val enabledGpc = shouldEnableGlobalPrivacyControl + val enabledIncreasedDohProtection = isIncreasedDohProtectionEnabled() + val enabledHttpsOnlyMode = enabledHttpsOnlyMode() + + when { + enabledEtpStrictMode || + enabledGpc || + enabledIncreasedDohProtection || + enabledHttpsOnlyMode -> 1 + + else -> 0 + } + } + + @VisibleForTesting + internal fun sponsoredContentPoints( + hasEligibleUserOptedOutOfSponsoredShortcuts: Boolean = hasEligibleUserOptedOutOfSponsoredShortcuts(), + hasEligibleUserOptedOutOfSponsoredStories: Boolean = hasEligibleUserOptedOutOfSponsoredStories(), + ) = + if (hasEligibleUserOptedOutOfSponsoredShortcuts || hasEligibleUserOptedOutOfSponsoredStories) { + 1 + } else { + 0 + } + + @VisibleForTesting + internal fun hasEligibleUserOptedOutOfSponsoredShortcuts() = with(termsOfUseDataProvider) { + regionSupportsSponsoredShortcuts() && (!showSponsoredShortcuts || !showShortcutsFeature) + } + + @VisibleForTesting + internal fun hasEligibleUserOptedOutOfSponsoredStories() = with(termsOfUseDataProvider) { + regionSupportsSponsoredStories() && (!showSponsoredStories || !showStoriesFeature) + } + + @VisibleForTesting + internal fun regionSupportsSponsoredShortcuts() = + supportedSponsoredShortcutsRegions.contains(systemLanguageTag) + + @VisibleForTesting + internal fun regionSupportsSponsoredStories() = + supportedSponsoredStoriesRegions.contains(systemLanguageTag) +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/termsofuse/experimentation/utils/Regions.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/termsofuse/experimentation/utils/Regions.kt @@ -0,0 +1,105 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.termsofuse.experimentation.utils + +/** + * List of supported regions for sponsored shortcuts (Top Sites). + */ +internal val supportedSponsoredShortcutsRegions = listOf( + // Australia + "en-AU", + + // Austria + "de-AT", + "fr-AT", + "it-AT", + "rm-AT", + + // Belgium + "de-BE", + "fr-BE", + "it-BE", + "rm-BE", + + // Brazil + "pt-BR", + + // Canada + "en-CA", + + // Czechia + "cs-CZ", + + // Denmark + "da-DK", + + // Finland + "fi-FI", + + // France + "fr", + "fr-FR", + + // Germany + "de", + "de-DE", + + // Hungary + "hu-HU", + + // India + "en-IN", + + // Ireland + "en-IE", + + // Japan + "ja-JP", + + // Netherlands + "nl-NL", + + // New Zealand + "en-NZ", + + // Norway + "nb-NO", + + // Poland + "pl-PL", + + // Portugal + "pt-PT", + + // Singapore + "en-SG", + + // Slovakia + "sk-SK", + + // Spain + "es", + "es-ES", + + // Sweden + "sv-SE", + + // UK + "en-GB", + + // US + "en-US", +) + +/** + * List of supported regions for sponsored stories. + */ +internal val supportedSponsoredStoriesRegions = listOf( + // US + "en-US", + + // Canada + "en-CA", +) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/termsofuse/experimentation/utils/TermsOfUseDataProvider.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/termsofuse/experimentation/utils/TermsOfUseDataProvider.kt @@ -0,0 +1,84 @@ +/* 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.termsofuse.experimentation.utils + +import mozilla.components.concept.engine.Engine +import mozilla.components.concept.engine.Engine.HttpsOnlyMode +import org.mozilla.fenix.utils.Settings + +/** + * Provides Terms of Use related data. + */ +interface TermsOfUseDataProvider { + /** + * Whether Strict Tracking Protection is enabled. + */ + val useStrictTrackingProtection: Boolean + + /** + * Whether Global Privacy Control is enabled. + */ + val shouldEnableGlobalPrivacyControl: Boolean + + /** + * Whether increased DoH protection is enabled. + */ + fun isIncreasedDohProtectionEnabled(): Boolean + + /** + * Whether HTTPS-only mode is enabled. + */ + fun enabledHttpsOnlyMode(): Boolean + + /** + * Whether sponsored Shortcuts are enabled. + */ + val showSponsoredShortcuts: Boolean + + /** + * Whether the Shortcuts feature is enabled. + */ + val showShortcutsFeature: Boolean + + /** + * Whether sponsored Stories are enabled. + */ + val showSponsoredStories: Boolean + + /** + * Whether the Stories feature is enabled. + */ + val showStoriesFeature: Boolean +} + +/** + * Default implementation of [TermsOfUseDataProvider]. + */ +class DefaultTermsOfUseDataProvider(private val settings: Settings) : TermsOfUseDataProvider { + override val useStrictTrackingProtection: Boolean + get() = settings.useStrictTrackingProtection + + override val shouldEnableGlobalPrivacyControl: Boolean + get() = settings.shouldEnableGlobalPrivacyControl + + override fun isIncreasedDohProtectionEnabled(): Boolean { + val dohSettingsMode = settings.getDohSettingsMode() + return dohSettingsMode == Engine.DohSettingsMode.INCREASED || dohSettingsMode == Engine.DohSettingsMode.MAX + } + + override fun enabledHttpsOnlyMode() = settings.getHttpsOnlyMode() != HttpsOnlyMode.DISABLED + + override val showSponsoredShortcuts: Boolean + get() = settings.showContileFeature + + override val showShortcutsFeature: Boolean + get() = settings.showTopSitesFeature + + override val showSponsoredStories: Boolean + get() = settings.showPocketSponsoredStories + + override val showStoriesFeature: Boolean + get() = settings.showPocketRecommendationsFeature +} diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/experiments/RecordedNimbusContextTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/experiments/RecordedNimbusContextTest.kt @@ -74,6 +74,7 @@ class RecordedNimbusContextTest { put("user_accepted_tou", true) put("no_shortcuts_or_stories_opt_outs", true) putJsonArray("addon_ids") {} + put("tou_points", 3) }, contextAsJson, ) @@ -118,6 +119,7 @@ class RecordedNimbusContextTest { put("region", "US") put("user_accepted_tou", true) put("no_shortcuts_or_stories_opt_outs", true) + put("tou_points", 3) }, recordedValue?.jsonObject, ) diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/termsofuse/experimentation/PrivacySettingsPointsTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/termsofuse/experimentation/PrivacySettingsPointsTest.kt @@ -0,0 +1,98 @@ +/* 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.termsofuse.experimentation + +import org.junit.Assert.assertEquals +import org.junit.Test +import org.mozilla.fenix.termsofuse.experimentation.utils.FakeTermsOfUseDataProvider + +/** + * Test for the [TermsOfUseAdvancedTargetingHelper.privacySettingsPoints] function. + */ +class PrivacySettingsPointsTest { + + @Test + fun `WHEN strict tracking protection is enabled THEN privacySettingsPoints returns 1`() { + val dataProvider = FakeTermsOfUseDataProvider(useStrictTrackingProtection = true) + val helper = TermsOfUseAdvancedTargetingHelper(dataProvider, "UNUSED") + + val result = helper.privacySettingsPoints() + + assertEquals(1, result) + } + + @Test + fun `WHEN global privacy control is enabled THEN privacySettingsPoints returns 1`() { + val dataProvider = FakeTermsOfUseDataProvider(shouldEnableGlobalPrivacyControl = true) + val helper = TermsOfUseAdvancedTargetingHelper(dataProvider, "UNUSED") + + val result = helper.privacySettingsPoints() + + assertEquals(1, result) + } + + @Test + fun `WHEN increased DoH protection is enabled THEN privacySettingsPoints returns 1`() { + val dataProvider = FakeTermsOfUseDataProvider(isIncreasedDohProtectionEnabled = true) + val helper = TermsOfUseAdvancedTargetingHelper(dataProvider, "UNUSED") + + val result = helper.privacySettingsPoints() + + assertEquals(1, result) + } + + @Test + fun `WHEN HTTPS Only mode is enabled THEN privacySettingsPoints returns 1`() { + val dataProvider = FakeTermsOfUseDataProvider(enabledHttpsOnlyMode = true) + val helper = TermsOfUseAdvancedTargetingHelper(dataProvider, "UNUSED") + + val result = helper.privacySettingsPoints() + + assertEquals(1, result) + } + + @Test + fun `WHEN some privacy settings are enabled THEN privacySettingsPoints returns 1`() { + val dataProvider = FakeTermsOfUseDataProvider( + isIncreasedDohProtectionEnabled = true, + enabledHttpsOnlyMode = true, + ) + val helper = TermsOfUseAdvancedTargetingHelper(dataProvider, "UNUSED") + + val result = helper.privacySettingsPoints() + + assertEquals(1, result) + } + + @Test + fun `WHEN all privacy settings are enabled THEN privacySettingsPoints returns 1`() { + val dataProvider = FakeTermsOfUseDataProvider( + useStrictTrackingProtection = true, + shouldEnableGlobalPrivacyControl = true, + isIncreasedDohProtectionEnabled = true, + enabledHttpsOnlyMode = true, + ) + val helper = TermsOfUseAdvancedTargetingHelper(dataProvider, "UNUSED") + + val result = helper.privacySettingsPoints() + + assertEquals(1, result) + } + + @Test + fun `WHEN no privacy settings are enabled THEN privacySettingsPoints returns 0`() { + val dataProvider = FakeTermsOfUseDataProvider( + useStrictTrackingProtection = false, + shouldEnableGlobalPrivacyControl = false, + isIncreasedDohProtectionEnabled = false, + enabledHttpsOnlyMode = false, + ) + val helper = TermsOfUseAdvancedTargetingHelper(dataProvider, "UNUSED") + + val result = helper.privacySettingsPoints() + + assertEquals(0, result) + } +} diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/termsofuse/experimentation/SponsoredContentPointsTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/termsofuse/experimentation/SponsoredContentPointsTest.kt @@ -0,0 +1,203 @@ +/* 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.termsofuse.experimentation + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.mozilla.fenix.termsofuse.experimentation.utils.FakeTermsOfUseDataProvider +import org.mozilla.fenix.termsofuse.experimentation.utils.supportedSponsoredShortcutsRegions +import org.mozilla.fenix.termsofuse.experimentation.utils.supportedSponsoredStoriesRegions + +/** + * Test for the [TermsOfUseAdvancedTargetingHelper.sponsoredContentPoints] function and dependencies. + */ +class SponsoredContentPointsTest { + + private val shortcutsLocale = supportedSponsoredShortcutsRegions.first() + private val storiesLocale = supportedSponsoredStoriesRegions.first() + + @Test + fun `WHEN eligible user opted out of shortcuts THEN sponsoredContentPoints returns 1`() { + val helper = + TermsOfUseAdvancedTargetingHelper(FakeTermsOfUseDataProvider(), shortcutsLocale) + + assertEquals( + 1, + helper.sponsoredContentPoints( + hasEligibleUserOptedOutOfSponsoredShortcuts = true, + hasEligibleUserOptedOutOfSponsoredStories = false, + ), + ) + } + + @Test + fun `WHEN eligible user opted out of stories THEN sponsoredContentPoints returns 1`() { + val helper = + TermsOfUseAdvancedTargetingHelper(FakeTermsOfUseDataProvider(), shortcutsLocale) + + assertEquals( + 1, + helper.sponsoredContentPoints( + hasEligibleUserOptedOutOfSponsoredShortcuts = false, + hasEligibleUserOptedOutOfSponsoredStories = true, + ), + ) + } + + @Test + fun `WHEN eligible user opted out of shortcuts and stories THEN sponsoredContentPoints returns 1`() { + val helper = + TermsOfUseAdvancedTargetingHelper(FakeTermsOfUseDataProvider(), shortcutsLocale) + + assertEquals( + 1, + helper.sponsoredContentPoints( + hasEligibleUserOptedOutOfSponsoredShortcuts = true, + hasEligibleUserOptedOutOfSponsoredStories = true, + ), + ) + } + + @Test + fun `WHEN eligible user has no opt outs THEN sponsoredContentPoints returns 0`() { + val helper = + TermsOfUseAdvancedTargetingHelper(FakeTermsOfUseDataProvider(), shortcutsLocale) + + assertEquals( + 0, + helper.sponsoredContentPoints( + hasEligibleUserOptedOutOfSponsoredShortcuts = false, + hasEligibleUserOptedOutOfSponsoredStories = false, + ), + ) + } + + @Test + fun `WHEN region not supported THEN hasEligibleUserOptedOutOfSponsoredShortcuts returns false`() { + val unsupportedRegion = "te-ST" + val helper = + TermsOfUseAdvancedTargetingHelper(FakeTermsOfUseDataProvider(), unsupportedRegion) + + val result = helper.hasEligibleUserOptedOutOfSponsoredShortcuts() + + assertFalse(result) + } + + @Test + fun `GIVEN region supported WHEN show sponsored shortcuts opted out THEN hasEligibleUserOptedOutOfSponsoredShortcuts returns true`() { + val dataProvider = FakeTermsOfUseDataProvider(showSponsoredShortcuts = false) + val helper = + TermsOfUseAdvancedTargetingHelper(dataProvider, shortcutsLocale) + + val result = helper.hasEligibleUserOptedOutOfSponsoredShortcuts() + + assertTrue(result) + } + + @Test + fun `GIVEN region supported WHEN shortcuts feature opted out THEN hasEligibleUserOptedOutOfSponsoredShortcuts returns true`() { + val dataProvider = FakeTermsOfUseDataProvider(showShortcutsFeature = false) + val helper = + TermsOfUseAdvancedTargetingHelper(dataProvider, shortcutsLocale) + + val result = helper.hasEligibleUserOptedOutOfSponsoredShortcuts() + + assertTrue(result) + } + + @Test + fun `GIVEN region supported WHEN sponsored shortcuts and shortcuts feature opted out THEN hasEligibleUserOptedOutOfSponsoredShortcuts returns true`() { + val dataProvider = + FakeTermsOfUseDataProvider(showSponsoredShortcuts = false, showShortcutsFeature = false) + val helper = TermsOfUseAdvancedTargetingHelper(dataProvider, shortcutsLocale) + + val result = helper.hasEligibleUserOptedOutOfSponsoredShortcuts() + + assertTrue(result) + } + + @Test + fun `GIVEN region supported WHEN no opt outs THEN hasEligibleUserOptedOutOfSponsoredShortcuts returns false`() { + val dataProvider = FakeTermsOfUseDataProvider() + val helper = TermsOfUseAdvancedTargetingHelper(dataProvider, shortcutsLocale) + + val result = helper.hasEligibleUserOptedOutOfSponsoredShortcuts() + + assertFalse(result) + } + + @Test + fun `WHEN region not supported THEN hasEligibleUserOptedOutOfSponsoredStories returns false`() { + val unsupportedRegion = "te-ST" + val helper = + TermsOfUseAdvancedTargetingHelper(FakeTermsOfUseDataProvider(), unsupportedRegion) + + val result = helper.hasEligibleUserOptedOutOfSponsoredStories() + + assertFalse(result) + } + + @Test + fun `GIVEN region supported WHEN show sponsored stories opted out THEN hasEligibleUserOptedOutOfSponsoredStories returns true`() { + val dataProvider = FakeTermsOfUseDataProvider(showSponsoredStories = false) + val helper = + TermsOfUseAdvancedTargetingHelper(dataProvider, storiesLocale) + + val result = helper.hasEligibleUserOptedOutOfSponsoredStories() + + assertTrue(result) + } + + @Test + fun `GIVEN region supported WHEN stories feature opted out THEN hasEligibleUserOptedOutOfSponsoredStories returns true`() { + val dataProvider = FakeTermsOfUseDataProvider(showStoriesFeature = false) + val helper = + TermsOfUseAdvancedTargetingHelper(dataProvider, storiesLocale) + + val result = helper.hasEligibleUserOptedOutOfSponsoredStories() + + assertTrue(result) + } + + @Test + fun `GIVEN region supported WHEN sponsored stories and stories feature opted out THEN hasEligibleUserOptedOutOfSponsoredStories returns true`() { + val dataProvider = + FakeTermsOfUseDataProvider(showSponsoredStories = false, showStoriesFeature = false) + val helper = TermsOfUseAdvancedTargetingHelper(dataProvider, storiesLocale) + + val result = helper.hasEligibleUserOptedOutOfSponsoredStories() + + assertTrue(result) + } + + @Test + fun `GIVEN region supported WHEN no opt outs THEN hasEligibleUserOptedOutOfSponsoredStories returns false`() { + val helper = TermsOfUseAdvancedTargetingHelper(FakeTermsOfUseDataProvider(), storiesLocale) + + val result = helper.hasEligibleUserOptedOutOfSponsoredStories() + + assertFalse(result) + } + + @Test + fun `WHEN region is supported THEN regionSupportsSponsoredShortcuts returns true`() { + supportedSponsoredShortcutsRegions.forEach { + val helper = TermsOfUseAdvancedTargetingHelper(FakeTermsOfUseDataProvider(), it) + + assertTrue(helper.regionSupportsSponsoredShortcuts()) + } + } + + @Test + fun `WHEN region is supported THEN regionSupportsSponsoredStories returns true`() { + supportedSponsoredStoriesRegions.forEach { + val helper = TermsOfUseAdvancedTargetingHelper(FakeTermsOfUseDataProvider(), it) + + assertTrue(helper.regionSupportsSponsoredStories()) + } + } +} diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/termsofuse/experimentation/TermsOfUseAdvancedTargetingHelperTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/termsofuse/experimentation/TermsOfUseAdvancedTargetingHelperTest.kt @@ -0,0 +1,58 @@ +/* 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.termsofuse.experimentation + +import org.junit.Assert.assertEquals +import org.junit.Test +import org.mozilla.fenix.termsofuse.experimentation.utils.FakeTermsOfUseDataProvider +import org.mozilla.fenix.termsofuse.experimentation.utils.supportedSponsoredShortcutsRegions + +class TermsOfUseAdvancedTargetingHelperTest { + + private val shortcutsLocale = supportedSponsoredShortcutsRegions.first() + + @Test + fun `WHEN privacySettingsPoints and sponsoredContentPoints each return 1 THEN getTouPoints returns 2`() { + val dataProvider = FakeTermsOfUseDataProvider( + useStrictTrackingProtection = true, + showSponsoredShortcuts = false, + ) + + val result = TermsOfUseAdvancedTargetingHelper(dataProvider, shortcutsLocale).getTouPoints() + + assertEquals(2, result) + } + + @Test + fun `WHEN only privacySettingsPoints returns 1 THEN getTouPoints returns 1`() { + val dataProvider = FakeTermsOfUseDataProvider( + useStrictTrackingProtection = true, + ) + + val result = TermsOfUseAdvancedTargetingHelper(dataProvider, shortcutsLocale).getTouPoints() + + assertEquals(1, result) + } + + @Test + fun `WHEN only sponsoredContentPoints returns 1 THEN getTouPoints returns 1`() { + val dataProvider = FakeTermsOfUseDataProvider( + showSponsoredShortcuts = false, + ) + + val result = TermsOfUseAdvancedTargetingHelper(dataProvider, shortcutsLocale).getTouPoints() + + assertEquals(1, result) + } + + @Test + fun `WHEN privacySettingsPoints and sponsoredContentPoints each return 0 THEN getTouPoints returns 0`() { + val dataProvider = FakeTermsOfUseDataProvider() + + val result = TermsOfUseAdvancedTargetingHelper(dataProvider, shortcutsLocale).getTouPoints() + + assertEquals(0, result) + } +} diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/termsofuse/experimentation/utils/DefaultTermsOfUseDataProviderTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/termsofuse/experimentation/utils/DefaultTermsOfUseDataProviderTest.kt @@ -0,0 +1,132 @@ +/* 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.termsofuse.experimentation.utils + +import mozilla.components.concept.engine.Engine +import mozilla.components.concept.engine.Engine.HttpsOnlyMode +import mozilla.components.support.test.whenever +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.mockito.Mockito.mock +import org.mozilla.fenix.utils.Settings + +class DefaultTermsOfUseDataProviderTest { + @Test + fun `useStrictTrackingProtection returns the same as the referenced Settings value`() { + val settings = mock<Settings>() + + whenever(settings.useStrictTrackingProtection).thenReturn(true) + val defaultTermsOfUseDataProvider1 = DefaultTermsOfUseDataProvider(settings) + assertTrue(defaultTermsOfUseDataProvider1.useStrictTrackingProtection) + + whenever(settings.useStrictTrackingProtection).thenReturn(false) + val defaultTermsOfUseDataProvider2 = DefaultTermsOfUseDataProvider(settings) + assertFalse(defaultTermsOfUseDataProvider2.useStrictTrackingProtection) + } + + @Test + fun `shouldEnableGlobalPrivacyControl returns the same as the referenced Settings value`() { + val settings = mock<Settings>() + + whenever(settings.shouldEnableGlobalPrivacyControl).thenReturn(true) + val defaultTermsOfUseDataProvider1 = DefaultTermsOfUseDataProvider(settings) + assertTrue(defaultTermsOfUseDataProvider1.shouldEnableGlobalPrivacyControl) + + whenever(settings.shouldEnableGlobalPrivacyControl).thenReturn(false) + val defaultTermsOfUseDataProvider2 = DefaultTermsOfUseDataProvider(settings) + assertFalse(defaultTermsOfUseDataProvider2.shouldEnableGlobalPrivacyControl) + } + + @Test + fun `isIncreasedDohProtectionEnabled returns true if the referenced Settings value is increased or max`() { + Engine.DohSettingsMode.entries.forEach { + val settings = mock<Settings>() + whenever(settings.getDohSettingsMode()).thenReturn(it) + val defaultTermsOfUseDataProvider = DefaultTermsOfUseDataProvider(settings) + + val result = defaultTermsOfUseDataProvider.isIncreasedDohProtectionEnabled() + when (it) { + Engine.DohSettingsMode.INCREASED, + Engine.DohSettingsMode.MAX, + -> assertTrue(result) + + Engine.DohSettingsMode.DEFAULT, + Engine.DohSettingsMode.OFF, + -> assertFalse(result) + } + } + } + + @Test + fun `enabledHttpsOnlyMode returns true if the referenced Settings value is increased or max`() { + HttpsOnlyMode.entries.forEach { + val settings = mock<Settings>() + whenever(settings.getHttpsOnlyMode()).thenReturn(it) + val defaultTermsOfUseDataProvider = DefaultTermsOfUseDataProvider(settings) + + val result = defaultTermsOfUseDataProvider.enabledHttpsOnlyMode() + when (it) { + HttpsOnlyMode.ENABLED_PRIVATE_ONLY, + HttpsOnlyMode.ENABLED, + -> assertTrue(result) + + HttpsOnlyMode.DISABLED -> assertFalse(result) + } + } + } + + @Test + fun `showSponsoredShortcuts returns the same as the referenced Settings value`() { + val settings = mock<Settings>() + + whenever(settings.showContileFeature).thenReturn(true) + val defaultTermsOfUseDataProvider1 = DefaultTermsOfUseDataProvider(settings) + assertTrue(defaultTermsOfUseDataProvider1.showSponsoredShortcuts) + + whenever(settings.showContileFeature).thenReturn(false) + val defaultTermsOfUseDataProvider2 = DefaultTermsOfUseDataProvider(settings) + assertFalse(defaultTermsOfUseDataProvider2.showSponsoredShortcuts) + } + + @Test + fun `showShortcutsFeature returns the same as the referenced Settings value`() { + val settings = mock<Settings>() + + whenever(settings.showTopSitesFeature).thenReturn(true) + val defaultTermsOfUseDataProvider1 = DefaultTermsOfUseDataProvider(settings) + assertTrue(defaultTermsOfUseDataProvider1.showShortcutsFeature) + + whenever(settings.showTopSitesFeature).thenReturn(false) + val defaultTermsOfUseDataProvider2 = DefaultTermsOfUseDataProvider(settings) + assertFalse(defaultTermsOfUseDataProvider2.showShortcutsFeature) + } + + @Test + fun `showSponsoredStories returns the same as the referenced Settings value`() { + val settings = mock<Settings>() + + whenever(settings.showPocketSponsoredStories).thenReturn(true) + val defaultTermsOfUseDataProvider1 = DefaultTermsOfUseDataProvider(settings) + assertTrue(defaultTermsOfUseDataProvider1.showSponsoredStories) + + whenever(settings.showPocketSponsoredStories).thenReturn(false) + val defaultTermsOfUseDataProvider2 = DefaultTermsOfUseDataProvider(settings) + assertFalse(defaultTermsOfUseDataProvider2.showSponsoredStories) + } + + @Test + fun `showRecommendationsFeature returns the same as the referenced Settings value`() { + val settings = mock<Settings>() + + whenever(settings.showPocketRecommendationsFeature).thenReturn(true) + val defaultTermsOfUseDataProvider1 = DefaultTermsOfUseDataProvider(settings) + assertTrue(defaultTermsOfUseDataProvider1.showStoriesFeature) + + whenever(settings.showPocketRecommendationsFeature).thenReturn(false) + val defaultTermsOfUseDataProvider2 = DefaultTermsOfUseDataProvider(settings) + assertFalse(defaultTermsOfUseDataProvider2.showStoriesFeature) + } +} diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/termsofuse/experimentation/utils/FakeTermsOfUseDataProvider.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/termsofuse/experimentation/utils/FakeTermsOfUseDataProvider.kt @@ -0,0 +1,22 @@ +/* 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.termsofuse.experimentation.utils + +/** + * Fake implementation of [TermsOfUseDataProvider] for tests. + */ +class FakeTermsOfUseDataProvider( + override val useStrictTrackingProtection: Boolean = false, + override val shouldEnableGlobalPrivacyControl: Boolean = false, + private val isIncreasedDohProtectionEnabled: Boolean = false, + private val enabledHttpsOnlyMode: Boolean = false, + override val showSponsoredShortcuts: Boolean = true, + override val showShortcutsFeature: Boolean = true, + override val showSponsoredStories: Boolean = true, + override val showStoriesFeature: Boolean = true, +) : TermsOfUseDataProvider { + override fun isIncreasedDohProtectionEnabled() = isIncreasedDohProtectionEnabled + override fun enabledHttpsOnlyMode() = enabledHttpsOnlyMode +} diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/termsofuse/experimentation/utils/Regions.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/termsofuse/experimentation/utils/Regions.kt @@ -0,0 +1,105 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.termsofuse.experimentation.utils + +/** + * Test duplicate of [supportedSponsoredShortcutsRegions], these should be the same regions. + */ +internal val supportedSponsoredShortcutsRegions = listOf( + // Australia + "en-AU", + + // Austria + "de-AT", + "fr-AT", + "it-AT", + "rm-AT", + + // Belgium + "de-BE", + "fr-BE", + "it-BE", + "rm-BE", + + // Brazil + "pt-BR", + + // Canada + "en-CA", + + // Czechia + "cs-CZ", + + // Denmark + "da-DK", + + // Finland + "fi-FI", + + // France + "fr", + "fr-FR", + + // Germany + "de", + "de-DE", + + // Hungary + "hu-HU", + + // India + "en-IN", + + // Ireland + "en-IE", + + // Japan + "ja-JP", + + // Netherlands + "nl-NL", + + // New Zealand + "en-NZ", + + // Norway + "nb-NO", + + // Poland + "pl-PL", + + // Portugal + "pt-PT", + + // Singapore + "en-SG", + + // Slovakia + "sk-SK", + + // Spain + "es", + "es-ES", + + // Sweden + "sv-SE", + + // UK + "en-GB", + + // US + "en-US", +) + +/** + * Test duplicate of [supportedSponsoredStoriesRegions], these should be the same regions. + */ +internal val supportedSponsoredStoriesRegions = listOf( + // US + "en-US", + + // Canada + "en-CA", +)