commit a426a924811ad96d8bc101d6c4d91ed894c4d0cf parent b3a1260d5b2d73a7b9d4b17848168cf905e3605f Author: t-p-white <towhite@mozilla.com> Date: Tue, 14 Oct 2025 12:35:00 +0000 Bug 1991769 - Part 2: Set Terms of Use accepted date and version metrics for users who have already accepted the ToU during onboarding. r=android-reviewers,joberhauser,cpeterson - Added start-up metrics to record ToU accepted date and version. This includes fallback behaviour to cover users that have already accepted ToU but did not have the date and version fields available at the time of accepting. Differential Revision: https://phabricator.services.mozilla.com/D267956 Diffstat:
12 files changed, 131 insertions(+), 11 deletions(-)
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/PackageManager.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/PackageManager.kt @@ -43,6 +43,7 @@ fun PackageManager.resolveActivityCompat(intent: Intent, flag: Int): ResolveInfo * Get a package info with a specified flag * * @param packageName The name of the package to check for. + * @throws PackageManager.NameNotFoundException if the package for [packageName] is not installed. */ fun PackageManager.getPackageInfoCompat(packageName: String, flag: Int): PackageInfo { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt @@ -80,6 +80,7 @@ import org.mozilla.fenix.GleanMetrics.Metrics import org.mozilla.fenix.GleanMetrics.PerfStartup import org.mozilla.fenix.GleanMetrics.Preferences import org.mozilla.fenix.GleanMetrics.SearchDefaultEngine +import org.mozilla.fenix.GleanMetrics.TermsOfUse import org.mozilla.fenix.components.Components import org.mozilla.fenix.components.Core import org.mozilla.fenix.components.appstate.AppAction @@ -113,6 +114,7 @@ import org.mozilla.fenix.settings.doh.DohSettingsProvider import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.isLargeScreenSize import org.mozilla.fenix.wallpapers.Wallpaper +import java.util.Date import java.util.concurrent.TimeUnit import kotlin.math.roundToLong import mozilla.components.support.AppServicesInitializer.Config as AppServicesConfig @@ -769,6 +771,10 @@ open class FenixApplication : LocaleAwareApplication(), Provider { // Set this early to guarantee it's in every ping from here on. distributionId.set(components.distributionIdManager.getDistributionId()) + if (settings.hasAcceptedTermsOfService) { + setTermsOfUseStartUpMetrics(settings) + } + defaultBrowser.set(browsersCache.all(applicationContext).isDefaultBrowser) mozillaProductDetector.getMozillaBrowserDefault(applicationContext)?.also { defaultMozBrowser.set(it) @@ -883,6 +889,11 @@ open class FenixApplication : LocaleAwareApplication(), Provider { setAutofillMetrics() } + private fun setTermsOfUseStartUpMetrics(settings: Settings) { + TermsOfUse.version.set(settings.termsOfUseAcceptedVersion.toLong()) + TermsOfUse.date.set(Date(settings.termsOfUseAcceptedTimeInMillis)) + } + @VisibleForTesting internal fun getDeviceTotalRAM(): Long { val memoryInfo = getMemoryInfo() diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/Context.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/Context.kt @@ -7,6 +7,7 @@ package org.mozilla.fenix.ext import android.app.Activity import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.content.res.Configuration import android.content.res.Resources import android.provider.Settings @@ -19,6 +20,7 @@ import androidx.annotation.StringRes import mozilla.components.compose.base.theme.layout.AcornWindowSize import mozilla.components.support.base.log.logger.Logger import mozilla.components.support.locale.LocaleManager +import mozilla.components.support.utils.ext.getPackageInfoCompat import org.mozilla.fenix.FenixApplication import org.mozilla.fenix.R import org.mozilla.fenix.components.Components @@ -175,3 +177,16 @@ fun Context.isToolbarAtBottom() = * @return The pixel size corresponding to the given dimension resource. */ fun Context.pixelSizeFor(@DimenRes resId: Int) = resources.getDimensionPixelSize(resId) + +/** + * Returns the installation time of this application (in milliseconds). + * + * @param logger Used to log a warning if package information cannot be retrieved. + * @return The installation time in milliseconds since epoch, or `0L` if unavailable. + */ +fun Context.getApplicationInstalledTime(logger: Logger): Long = try { + packageManager.getPackageInfoCompat(packageName, 0).firstInstallTime +} catch (e: PackageManager.NameNotFoundException) { + logger.warn("Unable to retrieve package info for $packageName", e) + 0L +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/DefaultOnboardingTermsOfServiceEventHandler.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/DefaultOnboardingTermsOfServiceEventHandler.kt @@ -7,6 +7,7 @@ package org.mozilla.fenix.onboarding import mozilla.components.support.ktx.kotlin.ifNullOrEmpty import org.mozilla.fenix.onboarding.view.OnboardingTermsOfServiceEventHandler import org.mozilla.fenix.settings.SupportUtils +import org.mozilla.fenix.termsofuse.TOU_VERSION import org.mozilla.fenix.utils.Settings /** @@ -42,8 +43,10 @@ class DefaultOnboardingTermsOfServiceEventHandler( showManagePrivacyPreferencesDialog() } - override fun onAcceptTermsButtonClicked() { + override fun onAcceptTermsButtonClicked(nowMillis: Long) { telemetryRecorder.onTermsOfServiceManagerAcceptTermsButtonClick() settings.hasAcceptedTermsOfService = true + settings.termsOfUseAcceptedVersion = TOU_VERSION + settings.termsOfUseAcceptedTimeInMillis = nowMillis } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPageState.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPageState.kt @@ -155,6 +155,8 @@ interface OnboardingTermsOfServiceEventHandler { /** * Invoked when the accept button is clicked. + * + * @param nowMillis The current time in milliseconds. */ - fun onAcceptTermsButtonClicked() = Unit + fun onAcceptTermsButtonClicked(nowMillis: Long = System.currentTimeMillis()) = Unit } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/termsofuse/TermsOfUseVersion.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/termsofuse/TermsOfUseVersion.kt @@ -4,4 +4,7 @@ package org.mozilla.fenix.termsofuse +/** + * The current version of the Terms of Use. + */ const val TOU_VERSION = 5 diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/termsofuse/store/TermsOfUsePromptRepository.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/termsofuse/store/TermsOfUsePromptRepository.kt @@ -4,6 +4,7 @@ package org.mozilla.fenix.termsofuse.store +import org.mozilla.fenix.termsofuse.TOU_VERSION import org.mozilla.fenix.utils.Settings /** @@ -11,9 +12,11 @@ import org.mozilla.fenix.utils.Settings */ interface TermsOfUsePromptRepository { /** - * Updates the 'has accepted terms of use' preference to true. + * Updates the Terms of Use related preferences when the user accepts the ToU. + * + * @param nowMillis the current time in milliseconds. */ - fun updateHasAcceptedTermsOfUsePreference() + fun updateHasAcceptedTermsOfUsePreference(nowMillis: Long = System.currentTimeMillis()) /** * Updates the 'has postponed accepting terms of use' preference to true. @@ -44,15 +47,17 @@ interface TermsOfUsePromptRepository { } /** - * Default implementation of [TermsOfUsePromptRepository] + * Default implementation of [TermsOfUsePromptRepository]. * * @param settings the preferences settings */ class DefaultTermsOfUsePromptRepository( private val settings: Settings, ) : TermsOfUsePromptRepository { - override fun updateHasAcceptedTermsOfUsePreference() { + override fun updateHasAcceptedTermsOfUsePreference(nowMillis: Long) { settings.hasAcceptedTermsOfService = true + settings.termsOfUseAcceptedVersion = TOU_VERSION + settings.termsOfUseAcceptedTimeInMillis = nowMillis } override fun updateHasPostponedAcceptingTermsOfUsePreference() { diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -49,6 +49,7 @@ import org.mozilla.fenix.debugsettings.addresses.SharedPrefsAddressesDebugLocale import org.mozilla.fenix.ext.TALL_SCREEN_HEIGHT_DP import org.mozilla.fenix.ext.WIDE_SCREEN_WIDTH_DP import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.getApplicationInstalledTime import org.mozilla.fenix.ext.getPreferenceKey import org.mozilla.fenix.ext.pixelSizeFor import org.mozilla.fenix.home.pocket.ContentRecommendationsFeatureHelper @@ -70,6 +71,7 @@ import org.mozilla.fenix.settings.registerOnSharedPreferenceChangeListener import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_BLOCK_ALL import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_BLOCK_AUDIBLE import org.mozilla.fenix.tabstray.DefaultTabManagementFeatureHelper +import org.mozilla.fenix.termsofuse.TOU_VERSION import org.mozilla.fenix.wallpapers.Wallpaper import java.security.InvalidParameterException import java.util.UUID @@ -597,6 +599,33 @@ class Settings(private val appContext: Context) : PreferencesHolder { ) /** + * The date the user accepted the Terms of Use. + */ + var termsOfUseAcceptedTimeInMillis by longPreference( + key = appContext.getPreferenceKey(R.string.pref_key_terms_accepted_date), + default = { if (hasAcceptedTermsOfService) applicationInstalledTime else 0L }, + ) + + /** + * Temporary testing helper to set the date the user accepted the Terms of Use. + * + * Will be addressed in a more permanent refactor as part of + * https://bugzilla.mozilla.org/show_bug.cgi?id=1993949. + * + * ⚠️ Only mutate from tests. + */ + @VisibleForTesting + internal var applicationInstalledTime = appContext.getApplicationInstalledTime(logger) + + /** + * The version of the Terms of Use that the user has accepted. + */ + var termsOfUseAcceptedVersion by intPreference( + key = appContext.getPreferenceKey(R.string.pref_key_terms_accepted_version), + default = { if (hasAcceptedTermsOfService) TOU_VERSION else 0 }, + ) + + /** * Returns true if the terms of use feature flag is enabled */ var isTermsOfUsePromptEnabled by lazyFeatureFlagPreference( diff --git a/mobile/android/fenix/app/src/main/res/values/preference_keys.xml b/mobile/android/fenix/app/src/main/res/values/preference_keys.xml @@ -83,6 +83,8 @@ <string name="pref_key_show_first_time_translation" translatable="false">pref_key_show_first_time_translation</string> <string name="pref_key_translations_offer" translatable="false">pref_key_translations_offer</string> <string name="pref_key_terms_accepted" translatable="false">pref_key_terms_accepted</string> + <string name="pref_key_terms_accepted_date" translatable="false">pref_key_terms_accepted_date</string> + <string name="pref_key_terms_accepted_version" translatable="false">pref_key_terms_accepted_version</string> <string name="pref_key_terms_prompt_enabled" translatable="false">pref_key_terms_prompt_enabled</string> <string name="pref_key_terms_prompt_displayed_count" translatable="false">pref_key_terms_prompt_displayed_count</string> <string name="pref_key_terms_last_prompt_time" translatable="false">pref_key_terms_last_prompt_time</string> diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/onboarding/DefaultOnboardingTermsOfServiceEventHandlerTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/onboarding/DefaultOnboardingTermsOfServiceEventHandlerTest.kt @@ -7,12 +7,15 @@ package org.mozilla.fenix.onboarding import io.mockk.mockk import io.mockk.verify import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mozilla.fenix.utils.Settings import org.robolectric.RobolectricTestRunner +private const val TIME_IN_MILLIS = 1759926358L + @RunWith(RobolectricTestRunner::class) class DefaultOnboardingTermsOfServiceEventHandlerTest { @@ -79,12 +82,14 @@ class DefaultOnboardingTermsOfServiceEventHandlerTest { @Test fun onAcceptTermsButtonClicked() { - eventHandler.onAcceptTermsButtonClicked() + eventHandler.onAcceptTermsButtonClicked(nowMillis = TIME_IN_MILLIS) verify { telemetryRecorder.onTermsOfServiceManagerAcceptTermsButtonClick() } assert(settings.hasAcceptedTermsOfService) + assertEquals(5, settings.termsOfUseAcceptedVersion) + assertEquals(TIME_IN_MILLIS, settings.termsOfUseAcceptedTimeInMillis) } } diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/termsofuse/store/TermsOfUsePromptRepositoryTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/termsofuse/store/TermsOfUsePromptRepositoryTest.kt @@ -14,9 +14,10 @@ import org.junit.runner.RunWith import org.mozilla.fenix.utils.Settings import org.robolectric.RobolectricTestRunner +private const val TIME_IN_MILLIS = 1759926358L + @RunWith(RobolectricTestRunner::class) class TermsOfUsePromptRepositoryTest { - private lateinit var settings: Settings private lateinit var repository: DefaultTermsOfUsePromptRepository @@ -28,10 +29,16 @@ class TermsOfUsePromptRepositoryTest { } @Test - fun `WHEN updateHasAcceptedTermsOfUsePreference is called THEN the preference is updated`() { + fun `WHEN updateHasAcceptedTermsOfUsePreference is called THEN the related ToU preferences are updated`() { assertFalse(settings.hasAcceptedTermsOfService) - repository.updateHasAcceptedTermsOfUsePreference() + assertEquals(0, settings.termsOfUseAcceptedVersion) + assertEquals(0L, settings.termsOfUseAcceptedTimeInMillis) + + repository.updateHasAcceptedTermsOfUsePreference(nowMillis = TIME_IN_MILLIS) + assertTrue(settings.hasAcceptedTermsOfService) + assertEquals(5, settings.termsOfUseAcceptedVersion) + assertEquals(TIME_IN_MILLIS, settings.termsOfUseAcceptedTimeInMillis) } @Test diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/utils/SettingsTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/utils/SettingsTest.kt @@ -4,7 +4,6 @@ package org.mozilla.fenix.utils -import android.content.res.Configuration import androidx.core.content.edit import io.mockk.every import io.mockk.spyk @@ -32,6 +31,8 @@ import org.mozilla.fenix.settings.deletebrowsingdata.DeleteBrowsingDataOnQuitTyp import org.robolectric.RobolectricTestRunner import java.util.Calendar +private const val TOU_VERSION = 5 + @RunWith(RobolectricTestRunner::class) class SettingsTest { @@ -1240,4 +1241,40 @@ class SettingsTest { assertFalse(settings.preferences.contains(oldKey)) eventStore.assertNoPastEvents() } + + @Test + fun `WHEN user has accepted the ToU THEN termsOfUseAcceptedTimeInMillis returns the app installed time`() { + val installTime = 12345L + settings.applicationInstalledTime = installTime + settings.hasAcceptedTermsOfService = true + + val result = settings.termsOfUseAcceptedTimeInMillis + assertEquals(installTime, result) + } + + @Test + fun `WHEN user has not accepted the ToU THEN termsOfUseAcceptedTimeInMillis returns 0L`() { + val installTime = 12345L + settings.applicationInstalledTime = installTime + settings.hasAcceptedTermsOfService = false + + val result = settings.termsOfUseAcceptedTimeInMillis + assertEquals(0L, result) + } + + @Test + fun `WHEN user has accepted the ToU THEN termsOfUseAcceptedVersion returns the ToU version`() { + settings.hasAcceptedTermsOfService = true + + val result = settings.termsOfUseAcceptedVersion + assertEquals(TOU_VERSION, result) + } + + @Test + fun `WHEN user has not accepted the ToU THEN termsOfUseAcceptedVersion returns 0`() { + settings.hasAcceptedTermsOfService = false + + val result = settings.termsOfUseAcceptedVersion + assertEquals(0, result) + } }