commit 221e0904fc03db1ec2952fc226536029cc78a199
parent ace4251c3dfa59a68bcdec051752e575c39d2a4f
Author: mike a. <mavduevskiy@mozilla.com>
Date: Tue, 18 Nov 2025 19:12:50 +0000
Bug 1955887 - Part 3: Preserve store state during configuration change r=android-reviewers,marcin,twhite
... while avoiding memory leaks.
Differential Revision: https://phabricator.services.mozilla.com/D271541
Diffstat:
5 files changed, 61 insertions(+), 25 deletions(-)
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
@@ -62,4 +62,11 @@ sealed interface SystemAction : AppIconAction {
* The app icon update error snackbar was dismissed.
*/
data object SnackbarDismissed : SystemAction
+
+ /**
+ * Signals a context update. For example, due to config change.
+ *
+ * @property appIconUpdater an interface used by [AppIconMiddleware] for changing the app icon.
+ */
+ data class EnvironmentRehydrated(val appIconUpdater: AppIconUpdater) : SystemAction
}
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
@@ -4,6 +4,7 @@
package org.mozilla.fenix.iconpicker
+import androidx.annotation.VisibleForTesting
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext
@@ -13,7 +14,8 @@ import mozilla.components.lib.state.MiddlewareContext
* @property updateAppIcon A interface that updates the main activity alias with the newly selected one.
*/
class AppIconMiddleware(
- val updateAppIcon: AppIconUpdater,
+ @get:VisibleForTesting
+ internal var updateAppIcon: AppIconUpdater,
) : Middleware<AppIconState, AppIconAction> {
override fun invoke(
@@ -32,12 +34,17 @@ class AppIconMiddleware(
}
}
+ is SystemAction.EnvironmentRehydrated -> {
+ updateAppIcon = action.appIconUpdater
+ }
+
+ is UserAction.Dismissed,
+ is UserAction.Selected,
is SystemAction.Applied,
is SystemAction.DialogDismissed,
is SystemAction.SnackbarDismissed,
is SystemAction.UpdateFailed,
- is UserAction.Dismissed,
- is UserAction.Selected,
+
-> {
// 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
@@ -32,5 +32,6 @@ private fun AppIconState.handleSystemAction(action: SystemAction): AppIconState
userSelectedAppIcon = null,
snackbarState = AppIconSnackbarState.ApplyingNewIconError,
)
+ is SystemAction.EnvironmentRehydrated -> this
}
}
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
@@ -13,13 +13,16 @@ import androidx.fragment.compose.content
import androidx.navigation.fragment.findNavController
import mozilla.components.support.base.feature.UserInteractionHandler
import org.mozilla.fenix.R
+import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.ext.showToolbar
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.AppIconUpdater
import org.mozilla.fenix.iconpicker.DefaultAppIconRepository
import org.mozilla.fenix.iconpicker.DefaultPackageManagerWrapper
+import org.mozilla.fenix.iconpicker.SystemAction
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.utils.ShortcutManagerWrapperDefault
import org.mozilla.fenix.utils.ShortcutsUpdaterDefault
@@ -44,37 +47,37 @@ class AppIconSelectionFragment : Fragment(), UserInteractionHandler {
) = content {
FirefoxTheme {
AppIconSelection(
- store = AppIconStore(
- initialState = AppIconState(
- currentAppIcon = appIconRepository.selectedAppIcon,
- groupedIconOptions = appIconRepository.groupedAppIcons,
- ),
- middleware = listOf(
- AppIconMiddleware(
- updateAppIcon = { newIcon, currentIcon ->
- updateAppIcon(
- currentAliasSuffix = currentIcon.aliasSuffix,
- newAliasSuffix = newIcon.aliasSuffix,
- )
- },
+ store = StoreProvider.get(this) {
+ AppIconStore(
+ initialState = AppIconState(
+ currentAppIcon = appIconRepository.selectedAppIcon,
+ groupedIconOptions = appIconRepository.groupedAppIcons,
),
- ),
- ),
+ middleware = listOf(
+ AppIconMiddleware(
+ updateAppIcon = updateAppIcon(),
+ ),
+ ),
+ )
+ }.also {
+ it.dispatch(
+ SystemAction.EnvironmentRehydrated(
+ appIconUpdater = updateAppIcon(),
+ ),
+ )
+ },
)
}
}
- private fun updateAppIcon(
- currentAliasSuffix: String,
- newAliasSuffix: String,
- ): Boolean {
+ private fun updateAppIcon(): AppIconUpdater = AppIconUpdater { newIcon, currentIcon ->
with(requireContext()) {
- return changeAppLauncherIcon(
+ changeAppLauncherIcon(
packageManager = packageManager,
shortcutManager = ShortcutManagerWrapperDefault(this),
shortcutInfo = ShortcutsUpdaterDefault(this),
- appAlias = ComponentName(this, "$packageName.$currentAliasSuffix"),
- newAppAlias = ComponentName(this, "$packageName.$newAliasSuffix"),
+ appAlias = ComponentName(this, "$packageName.${currentIcon.aliasSuffix}"),
+ newAppAlias = ComponentName(this, "$packageName.${newIcon.aliasSuffix}"),
)
}
}
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
@@ -89,4 +89,22 @@ class AppIconMiddlewareTest {
assertEquals(listOf(confirmAction, SystemAction.UpdateFailed), result)
}
+
+ @Test
+ fun `GIVEN EnvironmentRehydrated system action WHEN middleware is called THEN the new app icon updater replaces the old one`() {
+ val initialUpdater = AppIconUpdater { _, _ -> false }
+ val middleware = AppIconMiddleware(initialUpdater)
+ val store = AppIconStore(
+ initialState = AppIconState(),
+ reducer = { state, _ ->
+ state
+ },
+ middleware = listOf(middleware),
+ )
+ val newUpdater = AppIconUpdater { _, _ -> false }
+
+ store.dispatch(SystemAction.EnvironmentRehydrated(newUpdater))
+
+ assertEquals(newUpdater, middleware.updateAppIcon)
+ }
}