tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit b2de242893eb221c651576a5e000812cbdfaab00
parent 7a45dd7b33df43005521596523db674f668158bb
Author: mike a. <mavduevskiy@mozilla.com>
Date:   Wed, 26 Nov 2025 23:12:53 +0000

Bug 1955888 - Add telemetry middleware to app icon selection feature r=android-reviewers,twhite

Differential Revision: https://phabricator.services.mozilla.com/D273375

Diffstat:
Mmobile/android/fenix/app/metrics.yaml | 19+++++++++++++++++++
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/iconpicker/AppIconAction.kt | 13++++++++++++-
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/iconpicker/AppIconMiddleware.kt | 9+++++++--
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/iconpicker/AppIconReducer.kt | 12++++++++----
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/iconpicker/AppIconState.kt | 5++++-
Amobile/android/fenix/app/src/main/java/org/mozilla/fenix/iconpicker/AppIconTelemetryMiddleware.kt | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/iconpicker/ui/AppIconSelection.kt | 13++++++++++---
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/iconpicker/ui/AppIconSelectionFragment.kt | 2++
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/iconpicker/AppIconMiddlewareTest.kt | 6+++---
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/iconpicker/AppIconReducerTest.kt | 27+++++++++++++++++++++++----
Amobile/android/fenix/app/src/test/java/org/mozilla/fenix/iconpicker/AppIconTelemetryMiddlewareTest.kt | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
11 files changed, 214 insertions(+), 18 deletions(-)

diff --git a/mobile/android/fenix/app/metrics.yaml b/mobile/android/fenix/app/metrics.yaml @@ -12937,6 +12937,25 @@ app_icon_selection: notification_emails: - android-probes@mozilla.com expires: never + error_snackbar_shown: + type: event + description: When a error snackbar was shown signaling a system error while applying the new icon. + extra_keys: + old_icon: + description: The title of the currently used app icon. + type: string + new_icon: + description: The title of the app icon that the system tried and failed to apply. + type: string + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1955888 + data_reviews: + - https://phabricator.services.mozilla.com/D273375 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never nimbus_system: recorded_nimbus_context: diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/iconpicker/AppIconAction.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/iconpicker/AppIconAction.kt @@ -55,8 +55,19 @@ sealed interface SystemAction : AppIconAction { /** * The app icon update failed (due to an exception thrown by a system call). + * + * @property oldIcon the currently used app icon. + * @property newIcon the app icon that the system tried and failed to apply. + */ + data class UpdateFailed(val oldIcon: AppIcon, val newIcon: AppIcon) : SystemAction + + /** + * The app icon update error snackbar was shown. + * + * @property oldIcon the currently used app icon. + * @property newIcon the app icon that the system tried and failed to apply. */ - data object UpdateFailed : SystemAction + data class SnackbarShown(val oldIcon: AppIcon, val newIcon: AppIcon) : SystemAction /** * The app icon update error snackbar was dismissed. diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/iconpicker/AppIconMiddleware.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/iconpicker/AppIconMiddleware.kt @@ -28,7 +28,12 @@ class AppIconMiddleware( if (updateAppIcon(old = action.newIcon, new = action.oldIcon)) { context.dispatch(SystemAction.Applied(action.newIcon)) } else { - context.dispatch(SystemAction.UpdateFailed) + context.dispatch( + SystemAction.UpdateFailed( + oldIcon = action.oldIcon, + newIcon = action.newIcon, + ), + ) } } @@ -37,8 +42,8 @@ class AppIconMiddleware( is SystemAction.Applied, is SystemAction.DialogDismissed, is SystemAction.SnackbarDismissed, + is SystemAction.SnackbarShown, is SystemAction.UpdateFailed, - -> { // no-op } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/iconpicker/AppIconReducer.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/iconpicker/AppIconReducer.kt @@ -34,15 +34,19 @@ private fun AppIconState.handleSystemAction(action: SystemAction): AppIconState userSelectedAppIcon = null, warningDialogState = AppIconWarningDialog.None, ) - SystemAction.DialogDismissed -> this.copy( + is SystemAction.DialogDismissed -> this.copy( userSelectedAppIcon = null, warningDialogState = AppIconWarningDialog.None, ) - SystemAction.SnackbarDismissed -> this.copy(snackbarState = AppIconSnackbarState.None) - SystemAction.UpdateFailed -> this.copy( + is SystemAction.SnackbarDismissed -> this.copy(snackbarState = AppIconSnackbarState.None) + is SystemAction.UpdateFailed -> this.copy( userSelectedAppIcon = null, - snackbarState = AppIconSnackbarState.ApplyingNewIconError, + snackbarState = AppIconSnackbarState.ApplyingNewIconError( + oldIcon = action.oldIcon, + newIcon = action.newIcon, + ), warningDialogState = AppIconWarningDialog.None, ) + is SystemAction.SnackbarShown -> this } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/iconpicker/AppIconState.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/iconpicker/AppIconState.kt @@ -34,8 +34,11 @@ sealed class AppIconSnackbarState { /** * Display a snackbar of the app icon update failure. + * + * @property oldIcon the currently used app icon. + * @property newIcon the app icon that the system tried and failed to apply. */ - data object ApplyingNewIconError : AppIconSnackbarState() + data class ApplyingNewIconError(val oldIcon: AppIcon, val newIcon: AppIcon) : AppIconSnackbarState() } /** diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/iconpicker/AppIconTelemetryMiddleware.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/iconpicker/AppIconTelemetryMiddleware.kt @@ -0,0 +1,50 @@ +/* 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.iconpicker + +import mozilla.components.lib.state.Middleware +import mozilla.components.lib.state.MiddlewareContext +import org.mozilla.fenix.GleanMetrics.AppIconSelection + +/** + * A middleware for handling the app icon selector feature telemetry. + */ +class AppIconTelemetryMiddleware : Middleware<AppIconState, AppIconAction> { + override fun invoke( + context: MiddlewareContext<AppIconState, AppIconAction>, + next: (AppIconAction) -> Unit, + action: AppIconAction, + ) { + next(action) + + when (action) { + is UserAction.Confirmed -> { + AppIconSelection.appIconSelectionConfirmed.record( + extra = AppIconSelection.AppIconSelectionConfirmedExtra( + oldIcon = action.oldIcon.aliasSuffix, + newIcon = action.newIcon.aliasSuffix, + ), + ) + } + is SystemAction.SnackbarShown -> { + AppIconSelection.errorSnackbarShown.record( + extra = AppIconSelection.ErrorSnackbarShownExtra( + oldIcon = action.oldIcon.aliasSuffix, + newIcon = action.newIcon.aliasSuffix, + ), + ) + } + is UserAction.Dismissed, + is UserAction.Selected, + is SystemAction.Applied, + is SystemAction.DialogDismissed, + is SystemAction.SnackbarDismissed, + is SystemAction.UpdateFailed, + -> { + // no-op + } + } + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/iconpicker/ui/AppIconSelection.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/iconpicker/ui/AppIconSelection.kt @@ -99,11 +99,18 @@ fun AppIconSelection( val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } + val snackbarState = state.snackbarState val errorSnackbarMessage = stringResource(R.string.shortcuts_update_error) - LaunchedEffect(state.snackbarState) { - when (state.snackbarState) { + LaunchedEffect(snackbarState) { + when (snackbarState) { AppIconSnackbarState.None -> return@LaunchedEffect - AppIconSnackbarState.ApplyingNewIconError -> scope.launch { + is AppIconSnackbarState.ApplyingNewIconError -> scope.launch { + store.dispatch( + SystemAction.SnackbarShown( + oldIcon = snackbarState.oldIcon, + newIcon = snackbarState.newIcon, + ), + ) snackbarHostState.displaySnackbar( message = errorSnackbarMessage, onDismissPerformed = { diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/iconpicker/ui/AppIconSelectionFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/iconpicker/ui/AppIconSelectionFragment.kt @@ -21,6 +21,7 @@ import org.mozilla.fenix.iconpicker.AppIconMiddleware import org.mozilla.fenix.iconpicker.AppIconRepository import org.mozilla.fenix.iconpicker.AppIconState import org.mozilla.fenix.iconpicker.AppIconStore +import org.mozilla.fenix.iconpicker.AppIconTelemetryMiddleware import org.mozilla.fenix.iconpicker.AppIconUpdater import org.mozilla.fenix.iconpicker.DefaultAppIconRepository import org.mozilla.fenix.iconpicker.DefaultPackageManagerWrapper @@ -58,6 +59,7 @@ class AppIconSelectionFragment : Fragment(), UserInteractionHandler { AppIconMiddleware( updateAppIcon = updateAppIcon(), ), + AppIconTelemetryMiddleware(), ), ) }, diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/iconpicker/AppIconMiddlewareTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/iconpicker/AppIconMiddlewareTest.kt @@ -43,7 +43,7 @@ class AppIconMiddlewareTest { fun `WHEN updateAppIcon call is successful THEN the middleware dispatches the Applied system action to the store`() { val currentIcon = AppIcon.AppDefault val newIcon = AppIcon.AppRetro2004 - val middleware = AppIconMiddleware { newIcon, currentIcon -> true } + val middleware = AppIconMiddleware { _, _ -> true } val result = mutableListOf<AppIconAction>() val store = AppIconStore( initialState = AppIconState( @@ -68,7 +68,7 @@ class AppIconMiddlewareTest { fun `WHEN updateAppIcon call returns with an a failure THEN the middleware dispatches the UpdateFailed system action to the store`() { val currentIcon = AppIcon.AppDefault val newIcon = AppIcon.AppRetro2004 - val middleware = AppIconMiddleware { newIcon, currentIcon -> false } + val middleware = AppIconMiddleware { _, _ -> false } val result = mutableListOf<AppIconAction>() val store = AppIconStore( initialState = AppIconState( @@ -86,6 +86,6 @@ class AppIconMiddlewareTest { val confirmAction = UserAction.Confirmed(newIcon = newIcon, oldIcon = currentIcon) store.dispatch(confirmAction) - assertEquals(listOf(confirmAction, SystemAction.UpdateFailed), result) + assertEquals(listOf(confirmAction, SystemAction.UpdateFailed(oldIcon = currentIcon, newIcon = newIcon)), result) } } diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/iconpicker/AppIconReducerTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/iconpicker/AppIconReducerTest.kt @@ -111,22 +111,41 @@ class AppIconReducerTest { assertEquals(dialogState, initialState.warningDialogState) assertEquals(AppIconSnackbarState.None, initialState.snackbarState) - val result = appIconReducer(initialState, SystemAction.UpdateFailed) + val result = appIconReducer( + initialState, + SystemAction.UpdateFailed(oldIcon = currentIcon, newIcon = newIcon), + ) assertEquals(null, result.userSelectedAppIcon) assertEquals(AppIconWarningDialog.None, result.warningDialogState) - assertEquals(AppIconSnackbarState.ApplyingNewIconError, result.snackbarState) + assertEquals( + AppIconSnackbarState.ApplyingNewIconError( + oldIcon = currentIcon, + newIcon = newIcon, + ), + result.snackbarState, + ) } @Test fun `GIVEN SnackbarDismissed system action WHEN reducer is called THEN the snackbar is hidden`() { val currentIcon = AppIcon.AppDefault + val newIcon = AppIcon.AppRetro2004 val initialState = AppIconState( currentAppIcon = currentIcon, - snackbarState = AppIconSnackbarState.ApplyingNewIconError, + snackbarState = AppIconSnackbarState.ApplyingNewIconError( + oldIcon = currentIcon, + newIcon = newIcon, + ), ) - assertEquals(AppIconSnackbarState.ApplyingNewIconError, initialState.snackbarState) + assertEquals( + AppIconSnackbarState.ApplyingNewIconError( + oldIcon = currentIcon, + newIcon = newIcon, + ), + initialState.snackbarState, + ) val result = appIconReducer(initialState, SystemAction.SnackbarDismissed) diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/iconpicker/AppIconTelemetryMiddlewareTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/iconpicker/AppIconTelemetryMiddlewareTest.kt @@ -0,0 +1,76 @@ +package org.mozilla.fenix.iconpicker + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.GleanMetrics.AppIconSelection +import org.mozilla.fenix.helpers.FenixGleanTestRule + +@RunWith(AndroidJUnit4::class) +class AppIconTelemetryMiddlewareTest { + + @get:Rule + val gleanRule = FenixGleanTestRule(testContext) + + @Test + fun `GIVEN user action Confirmed WHEN telemetry middleware gets invoked THEN record selection confirmed event`() { + val currentIcon = AppIcon.AppDefault + val newIcon = AppIcon.AppRetro2004 + val store = buildStore(currentIcon, newIcon) + + assertNull(AppIconSelection.appIconSelectionConfirmed.testGetValue()) + + store.dispatch(UserAction.Confirmed(newIcon = newIcon, oldIcon = currentIcon)) + + assertNotNull(AppIconSelection.appIconSelectionConfirmed.testGetValue()) + assertEventExtraData( + oldIcon = currentIcon, + newIcon = newIcon, + eventExtraData = AppIconSelection.appIconSelectionConfirmed.testGetValue()!!.last().extra!!, + ) + } + + @Test + fun `GIVEN system action SnackbarShown WHEN telemetry middleware gets invoked THEN record selection confirmed event`() { + val currentIcon = AppIcon.AppDefault + val newIcon = AppIcon.AppRetro2004 + val store = buildStore(currentIcon, newIcon) + + assertNull(AppIconSelection.errorSnackbarShown.testGetValue()) + + store.dispatch(SystemAction.SnackbarShown(newIcon = newIcon, oldIcon = currentIcon)) + + assertNotNull(AppIconSelection.errorSnackbarShown.testGetValue()) + assertEventExtraData( + oldIcon = currentIcon, + newIcon = newIcon, + eventExtraData = AppIconSelection.errorSnackbarShown.testGetValue()!!.last().extra!!, + ) + } + + private fun assertEventExtraData( + oldIcon: AppIcon, + newIcon: AppIcon, + eventExtraData: Map<String, String>, + ) { + assertEquals(oldIcon.aliasSuffix, eventExtraData["old_icon"]) + assertEquals(newIcon.aliasSuffix, eventExtraData["new_icon"]) + } + + private fun buildStore( + currentAppIcon: AppIcon = AppIcon.AppDefault, + userSelectedAppIcon: AppIcon? = AppIcon.AppRetro2004, + ) = AppIconStore( + initialState = AppIconState( + currentAppIcon = currentAppIcon, + userSelectedAppIcon = userSelectedAppIcon, + groupedIconOptions = mapOf(), + ), + middleware = listOf(AppIconTelemetryMiddleware()), + ) +}