commit 2b26018c95b37e1ac0208e2e7f3f5455592e9a39 parent 69d9eb769e905244baeb9be6955d5aa4467c4375 Author: Mugurell <Mugurell@users.noreply.github.com> Date: Thu, 20 Nov 2025 09:12:33 +0000 Bug 1996643 - part 9 - Remove the Fenix specific StoreProvider in favor of the upstream one r=android-reviewers,matt-tighe,nalexander Differential Revision: https://phabricator.services.mozilla.com/D272162 Diffstat:
30 files changed, 245 insertions(+), 454 deletions(-)
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/HistoryTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/HistoryTest.kt @@ -11,6 +11,7 @@ import androidx.test.filters.SdkSuppress import org.junit.Rule import org.junit.Test import org.mozilla.fenix.R +import org.mozilla.fenix.customannotations.SkipLeaks import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources import org.mozilla.fenix.helpers.HomeActivityIntentTestRule @@ -194,6 +195,7 @@ class HistoryTest : TestSetup() { // TestRail link: https://mozilla.testrail.io/index.php?/cases/view/346098 @Test + @SkipLeaks(reasons = ["https://bugzilla.mozilla.org/show_bug.cgi?id=2000810"]) fun openMultipleSelectedHistoryItemsInPrivateTabTest() { val firstWebPage = mockWebServer.getGenericAsset(1) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationFragment.kt @@ -15,8 +15,8 @@ import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.navArgs import mozilla.components.lib.state.ext.consumeFrom +import mozilla.components.lib.state.helpers.StoreProvider.Companion.storeProvider import org.mozilla.fenix.R -import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.databinding.FragmentCreateCollectionBinding import org.mozilla.fenix.ext.requireComponents @@ -42,9 +42,9 @@ class CollectionCreationFragment : DialogFragment() { _binding = FragmentCreateCollectionBinding.inflate(inflater, container, false) val args: CollectionCreationFragmentArgs by navArgs() - collectionCreationStore = StoreProvider.get(this) { + collectionCreationStore = storeProvider.get { restoredState -> CollectionCreationStore( - createInitialCollectionCreationState( + restoredState ?: createInitialCollectionCreationState( browserState = requireComponents.core.store.state, tabCollectionStorage = requireComponents.core.tabCollectionStorage, publicSuffixList = requireComponents.publicSuffixList, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/StoreProvider.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/StoreProvider.kt @@ -1,61 +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.components - -import androidx.annotation.VisibleForTesting -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.ViewModelStoreOwner -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.CoroutineScope -import mozilla.components.lib.state.Store - -/** - * Generic ViewModel wrapper of a [Store] helping to persist it across process/activity recreations. - * - * @param createStore [Store] factory receiving also the [ViewModel.viewModelScope] associated with this [ViewModel]. - */ -class StoreProvider<T : Store<*, *>>( - createStore: (CoroutineScope) -> T, -) : ViewModel() { - - @VisibleForTesting - @PublishedApi - internal val store: T = createStore(viewModelScope) - - companion object { - /** - * Returns an existing [Store] instance or creates a new one scoped to a [ViewModelStoreOwner]. - * - * @see [ViewModelProvider.get]. - */ - inline fun <reified T : Store<*, *>> get( - owner: ViewModelStoreOwner, - noinline createStore: (CoroutineScope) -> T, - ): T { - val factory = StoreProviderFactory(createStore) - val viewModel: StoreProvider<*> = - ViewModelProvider(owner, factory).get(T::class.java.name, StoreProvider::class.java) - return viewModel.store as T - } - } -} - -/** - * [ViewModel] factory to create [StoreProvider] instances that will wrap a [Store] instance - * helping to persist it across process/activity recreations. - * - * @param createStore [Store] factory receiving also the [ViewModel.viewModelScope] associated with this [ViewModel]. - */ -@VisibleForTesting -class StoreProviderFactory<T : Store<*, *>>( - private val createStore: (CoroutineScope) -> T, -) : ViewModelProvider.Factory { - - @Suppress("UNCHECKED_CAST") - override fun <VM : ViewModel> create(modelClass: Class<VM>): VM { - return StoreProvider(createStore) as VM - } -} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/exceptions/login/LoginExceptionsFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/exceptions/login/LoginExceptionsFragment.kt @@ -14,8 +14,8 @@ import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.plus import mozilla.components.lib.state.ext.consumeFrom +import mozilla.components.lib.state.helpers.StoreProvider.Companion.fragmentStore import org.mozilla.fenix.R -import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.databinding.FragmentExceptionsBinding import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.showToolbar @@ -44,11 +44,9 @@ class LoginExceptionsFragment : Fragment() { container, false, ) - exceptionsStore = StoreProvider.get(this) { - ExceptionsFragmentStore( - ExceptionsFragmentState(items = emptyList()), - ) - } + exceptionsStore = fragmentStore(ExceptionsFragmentState(items = emptyList())) { + ExceptionsFragmentStore(it) + }.value exceptionsInteractor = DefaultLoginExceptionsInteractor( ioScope = viewLifecycleOwner.lifecycleScope + Dispatchers.IO, loginExceptionStorage = requireComponents.core.loginExceptionStorage, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsFragment.kt @@ -10,9 +10,9 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import mozilla.components.lib.state.ext.consumeFrom +import mozilla.components.lib.state.helpers.StoreProvider.Companion.fragmentStore import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R -import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.databinding.FragmentExceptionsBinding import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.showToolbar @@ -42,11 +42,9 @@ class TrackingProtectionExceptionsFragment : Fragment() { container, false, ) - exceptionsStore = StoreProvider.get(this) { - ExceptionsFragmentStore( - ExceptionsFragmentState(items = emptyList()), - ) - } + exceptionsStore = fragmentStore(ExceptionsFragmentState(items = emptyList())) { + ExceptionsFragmentStore(it) + }.value exceptionsInteractor = DefaultTrackingProtectionExceptionsInteractor( activity = activity as HomeActivity, exceptionsStore = exceptionsStore, 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,11 +62,4 @@ 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,18 +4,16 @@ package org.mozilla.fenix.iconpicker -import androidx.annotation.VisibleForTesting import mozilla.components.lib.state.Middleware import mozilla.components.lib.state.MiddlewareContext /** * A middleware for handling side-effects in response to [AppIconAction]s. * - * @property updateAppIcon A interface that updates the main activity alias with the newly selected one. + * @param updateAppIcon A interface that updates the main activity alias with the newly selected one. */ class AppIconMiddleware( - @get:VisibleForTesting - internal var updateAppIcon: AppIconUpdater, + private val updateAppIcon: AppIconUpdater, ) : Middleware<AppIconState, AppIconAction> { override fun invoke( @@ -34,10 +32,6 @@ class AppIconMiddleware( } } - is SystemAction.EnvironmentRehydrated -> { - updateAppIcon = action.appIconUpdater - } - is UserAction.Dismissed, is UserAction.Selected, is SystemAction.Applied, 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 @@ -44,6 +44,5 @@ private fun AppIconState.handleSystemAction(action: SystemAction): AppIconState snackbarState = AppIconSnackbarState.ApplyingNewIconError, warningDialogState = AppIconWarningDialog.None, ) - 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 @@ -11,9 +11,9 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.compose.content import androidx.navigation.fragment.findNavController +import mozilla.components.lib.state.helpers.StoreProvider.Companion.storeProvider import mozilla.components.support.base.feature.UserInteractionHandler import org.mozilla.fenix.R -import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.iconpicker.AppIconMiddleware @@ -23,7 +23,6 @@ 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 @@ -48,9 +47,9 @@ class AppIconSelectionFragment : Fragment(), UserInteractionHandler { ) = content { FirefoxTheme { AppIconSelection( - store = StoreProvider.get(this) { + store = storeProvider.get { restoredState -> AppIconStore( - initialState = AppIconState( + initialState = restoredState ?: AppIconState( currentAppIcon = appIconRepository.selectedAppIcon, groupedIconOptions = appIconRepository.groupedAppIcons, ), @@ -60,12 +59,6 @@ class AppIconSelectionFragment : Fragment(), UserInteractionHandler { ), ), ) - }.also { - it.dispatch( - SystemAction.EnvironmentRehydrated( - appIconUpdater = updateAppIcon(), - ), - ) }, ) } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt @@ -100,7 +100,6 @@ import org.mozilla.fenix.addons.showSnackBar import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.QrScanFenixFeature -import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.components.VoiceSearchFeature import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.components.history.DefaultPagedHistoryProvider @@ -196,16 +195,16 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler, ): View { _binding = FragmentHistoryBinding.inflate(inflater, container, false) val view = binding.root - historyStore = StoreProvider.get(this) { + historyStore = fragmentStore(HistoryFragmentState.initial) { HistoryFragmentStore( - initialState = HistoryFragmentState.initial, + initialState = it, middleware = listOf( HistoryTelemetryMiddleware( isInPrivateMode = requireComponents.appStore.state.mode == BrowsingMode.Private, ), ), ) - } + }.value searchStore = buildSearchStore(toolbarStore).value _historyView = HistoryView( diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/historymetadata/HistoryMetadataGroupFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/historymetadata/HistoryMetadataGroupFragment.kt @@ -28,6 +28,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.map import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.lib.state.ext.flowScoped +import mozilla.components.lib.state.helpers.StoreProvider.Companion.fragmentStore import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.ktx.kotlin.toShortUrl import mozilla.components.ui.widgets.withCenterAlignedButtons @@ -35,7 +36,6 @@ import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.addons.showSnackBar import org.mozilla.fenix.browser.browsingmode.BrowsingMode -import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.databinding.FragmentHistoryMetadataGroupBinding import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.nav @@ -86,15 +86,13 @@ class HistoryMetadataGroupFragment : _binding = FragmentHistoryMetadataGroupBinding.inflate(inflater, container, false) val historyItems = args.historyMetadataItems.filterIsInstance<History.Metadata>() - historyMetadataGroupStore = StoreProvider.get(this) { - HistoryMetadataGroupFragmentStore( - HistoryMetadataGroupFragmentState( - items = historyItems, - pendingDeletionItems = requireContext().components.appStore.state.pendingDeletionHistoryItems, - isEmpty = historyItems.isEmpty(), - ), - ) - } + historyMetadataGroupStore = fragmentStore( + HistoryMetadataGroupFragmentState( + items = historyItems, + pendingDeletionItems = requireContext().components.appStore.state.pendingDeletionHistoryItems, + isEmpty = historyItems.isEmpty(), + ), + ) { HistoryMetadataGroupFragmentStore(it) }.value interactor = DefaultHistoryMetadataGroupInteractor( controller = DefaultHistoryMetadataGroupController( diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragment.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.map import mozilla.components.browser.state.state.recover.RecoverableTab import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.lib.state.ext.flowScoped +import mozilla.components.lib.state.helpers.StoreProvider.Companion.fragmentStore import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.telemetry.glean.private.NoExtras import org.mozilla.fenix.BrowserDirection @@ -28,7 +29,6 @@ import org.mozilla.fenix.GleanMetrics.RecentlyClosedTabs import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.browser.browsingmode.BrowsingMode -import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.databinding.FragmentRecentlyClosedTabsBinding import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.setTextColor @@ -106,14 +106,12 @@ class RecentlyClosedFragment : savedInstanceState: Bundle?, ): View { val binding = FragmentRecentlyClosedTabsBinding.inflate(inflater, container, false) - recentlyClosedFragmentStore = StoreProvider.get(this) { - RecentlyClosedFragmentStore( - RecentlyClosedFragmentState( - items = listOf(), - selectedTabs = emptySet(), - ), - ) - } + recentlyClosedFragmentStore = fragmentStore( + RecentlyClosedFragmentState( + items = listOf(), + selectedTabs = emptySet(), + ), + ) { RecentlyClosedFragmentStore(it) }.value recentlyClosedController = DefaultRecentlyClosedController( navController = findNavController(), browserStore = requireComponents.core.store, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/nimbus/NimbusBranchesFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/nimbus/NimbusBranchesFragment.kt @@ -15,9 +15,9 @@ import androidx.navigation.fragment.navArgs import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import mozilla.components.lib.state.ext.consumeFrom +import mozilla.components.lib.state.helpers.StoreProvider.Companion.fragmentStore import mozilla.components.support.base.log.logger.Logger import org.mozilla.fenix.R -import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.compose.core.Action import org.mozilla.fenix.compose.snackbar.Snackbar import org.mozilla.fenix.compose.snackbar.SnackbarState @@ -49,9 +49,9 @@ class NimbusBranchesFragment : Fragment() { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - nimbusBranchesStore = StoreProvider.get(this) { - NimbusBranchesStore(NimbusBranchesState(branches = emptyList())) - } + nimbusBranchesStore = fragmentStore(NimbusBranchesState(branches = emptyList())) { + NimbusBranchesStore(it) + }.value controller = NimbusBranchesController( isTelemetryEnabled = { requireContext().settings().isTelemetryEnabled }, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/account/AccountSettingsFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/account/AccountSettingsFragment.kt @@ -30,6 +30,7 @@ import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.ConstellationState import mozilla.components.concept.sync.DeviceConstellationObserver import mozilla.components.lib.state.ext.consumeFrom +import mozilla.components.lib.state.helpers.StoreProvider.Companion.fragmentStore import mozilla.components.service.fxa.SyncEngine import mozilla.components.service.fxa.manager.FxaAccountManager import mozilla.components.service.fxa.manager.SyncEnginesStorage @@ -42,7 +43,6 @@ import mozilla.components.ui.widgets.withCenterAlignedButtons import mozilla.telemetry.glean.private.NoExtras import org.mozilla.fenix.GleanMetrics.SyncAccount import org.mozilla.fenix.R -import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.components.accounts.FenixFxAEntryPoint import org.mozilla.fenix.compose.snackbar.Snackbar import org.mozilla.fenix.compose.snackbar.SnackbarState @@ -97,6 +97,9 @@ class AccountSettingsFragment : PreferenceFragmentCompat() { super.onCreate(savedInstanceState) SyncTelemetry.processOpenSyncSettingsMenuTelemetry() SyncAccount.opened.record(NoExtras()) + + accountManager = requireComponents.backgroundServices.accountManager + accountManager.register(accountStateObserver, this, true) } override fun onStop() { @@ -127,6 +130,19 @@ class AccountSettingsFragment : PreferenceFragmentCompat() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + accountSettingsStore = fragmentStore( + AccountSettingsFragmentState( + lastSyncedDate = if (getLastSynced(requireContext()) == 0L) { + LastSyncTime.Never + } else { + LastSyncTime.Success(getLastSynced(requireContext())) + }, + deviceName = requireComponents.backgroundServices.defaultDeviceName( + requireContext(), + ), + ), + ) { AccountSettingsFragmentStore(it) }.value + consumeFrom(accountSettingsStore) { updateLastSyncTimePref(it) updateDeviceName(it) @@ -138,33 +154,41 @@ class AccountSettingsFragment : PreferenceFragmentCompat() { ::syncDeviceName, accountSettingsStore, ) + + setupPreferenceListeners() } @Suppress("LongMethod", "CyclomaticComplexMethod") override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.account_settings_preferences, rootKey) + } - accountSettingsStore = StoreProvider.get(this) { - AccountSettingsFragmentStore( - AccountSettingsFragmentState( - lastSyncedDate = if (getLastSynced(requireContext()) == 0L) { - LastSyncTime.Never - } else { - LastSyncTime.Success(getLastSynced(requireContext())) - }, - deviceName = requireComponents.backgroundServices.defaultDeviceName( - requireContext(), - ), - ), - ) - } + override fun onDisplayPreferenceDialog(preference: Preference) { + val handled = showCustomEditTextPreferenceDialog(preference) - accountManager = requireComponents.backgroundServices.accountManager - accountManager.register(accountStateObserver, this, true) + if (!handled) { + super.onDisplayPreferenceDialog(preference) + } + } + private fun setupPreferenceListeners() { val preferenceManageAccount = requirePreference<Preference>(R.string.pref_key_sync_manage_account) preferenceManageAccount.onPreferenceClickListener = getClickListenerForManageAccount() + setupSignInOutPreferenceListeners() + setupDeviceNamePreferenceListeners() + setupSyncCategoriesPreferenceListeners() + + // NB: ObserverRegistry will take care of cleaning up internal references to 'observer' and + // 'owner' when appropriate. + requireComponents.backgroundServices.accountManager.registerForSyncEvents( + syncStatusObserver, + owner = this, + autoPause = true, + ) + } + + private fun setupSignInOutPreferenceListeners() { // Sign out val preferenceSignOut = requirePreference<Preference>(R.string.pref_key_sign_out) preferenceSignOut.onPreferenceClickListener = getClickListenerForSignOut() @@ -188,9 +212,11 @@ class AccountSettingsFragment : PreferenceFragmentCompat() { isEnabled = true } } + } - // Device Name + private fun setupDeviceNamePreferenceListeners() { val deviceConstellation = accountManager.authenticatedAccount()?.deviceConstellation() + requirePreference<EditTextPreference>(R.string.pref_key_sync_device_name).apply { onPreferenceChangeListener = getChangeListenerForDeviceName() deviceConstellation?.state()?.currentDevice?.let { device -> @@ -204,6 +230,14 @@ class AccountSettingsFragment : PreferenceFragmentCompat() { } } + deviceConstellation?.registerDeviceObserver( + deviceConstellationObserver, + owner = this, + autoPause = true, + ) + } + + private fun setupSyncCategoriesPreferenceListeners() { // Make sure out sync engine checkboxes are up-to-date and disabled if currently syncing updateSyncEngineStates() setDisabledWhileSyncing(accountManager.isSyncActive()) @@ -244,28 +278,6 @@ class AccountSettingsFragment : PreferenceFragmentCompat() { } } } - - deviceConstellation?.registerDeviceObserver( - deviceConstellationObserver, - owner = this, - autoPause = true, - ) - - // NB: ObserverRegistry will take care of cleaning up internal references to 'observer' and - // 'owner' when appropriate. - requireComponents.backgroundServices.accountManager.registerForSyncEvents( - syncStatusObserver, - owner = this, - autoPause = true, - ) - } - - override fun onDisplayPreferenceDialog(preference: Preference) { - val handled = showCustomEditTextPreferenceDialog(preference) - - if (!handled) { - super.onDisplayPreferenceDialog(preference) - } } /** diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/address/AddressManagementFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/address/AddressManagementFragment.kt @@ -16,10 +16,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.lib.state.ext.observeAsComposableState +import mozilla.components.lib.state.helpers.StoreProvider.Companion.storeProvider import mozilla.telemetry.glean.private.NoExtras import org.mozilla.fenix.GleanMetrics.Addresses import org.mozilla.fenix.R -import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.settings.address.controller.DefaultAddressManagementController @@ -44,8 +44,8 @@ class AddressManagementFragment : Fragment() { container: ViewGroup?, savedInstanceState: Bundle?, ): View { - store = StoreProvider.get(this) { - AutofillFragmentStore(AutofillFragmentState()) + store = storeProvider.get { restoredState -> + AutofillFragmentStore(restoredState ?: AutofillFragmentState()) } interactor = DefaultAddressManagementInteractor( diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/advanced/LocaleSettingsFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/advanced/LocaleSettingsFragment.kt @@ -17,10 +17,10 @@ import androidx.core.view.MenuProvider import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle import mozilla.components.lib.state.ext.consumeFrom +import mozilla.components.lib.state.helpers.StoreProvider.Companion.storeProvider import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.components.support.locale.LocaleUseCases import org.mozilla.fenix.R -import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.databinding.FragmentLocaleSettingsBinding import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.showToolbar @@ -49,10 +49,8 @@ class LocaleSettingsFragment : Fragment(), MenuProvider { val browserStore = requireContext().components.core.store val localeUseCase = LocaleUseCases(browserStore) - localeSettingsStore = StoreProvider.get(this) { - LocaleSettingsStore( - createInitialLocaleSettingsState(requireContext()), - ) + localeSettingsStore = storeProvider.get { restoredState -> + LocaleSettingsStore(restoredState ?: createInitialLocaleSettingsState(requireContext())) } interactor = LocaleSettingsInteractor( controller = DefaultLocaleSettingsController( diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/autofill/AutofillSettingFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/autofill/AutofillSettingFragment.kt @@ -25,12 +25,12 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import mozilla.components.lib.state.ext.consumeFrom +import mozilla.components.lib.state.helpers.StoreProvider.Companion.storeProvider import mozilla.components.service.fxa.SyncEngine import mozilla.components.service.sync.autofill.AutofillCreditCardsAddressesStorage import mozilla.components.ui.widgets.withCenterAlignedButtons import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.R -import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.components.accounts.FenixFxAEntryPoint import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.runIfFragmentIsAttached @@ -77,12 +77,16 @@ class AutofillSettingFragment : BiometricPromptPreferenceFragment() { } } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - store = StoreProvider.get(this) { - AutofillFragmentStore(AutofillFragmentState()) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + store = storeProvider.get { restoredState -> + AutofillFragmentStore(restoredState ?: AutofillFragmentState()) } loadAutofillState() + return super.onCreateView(inflater, container, savedInstanceState) } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { @@ -122,15 +126,6 @@ class AutofillSettingFragment : BiometricPromptPreferenceFragment() { } } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ): View { - loadAutofillState() - return super.onCreateView(inflater, container, savedInstanceState) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardsManagementFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardsManagementFragment.kt @@ -13,9 +13,9 @@ import androidx.navigation.fragment.findNavController import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import mozilla.components.lib.state.ext.consumeFrom +import mozilla.components.lib.state.helpers.StoreProvider.Companion.storeProvider import org.mozilla.fenix.R import org.mozilla.fenix.SecureFragment -import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.databinding.ComponentCreditCardsBinding import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.redirectToReAuth @@ -44,8 +44,8 @@ class CreditCardsManagementFragment : SecureFragment() { ): View? { val view = inflater.inflate(CreditCardsManagementView.LAYOUT_ID, container, false) - store = StoreProvider.get(this) { - AutofillFragmentStore(AutofillFragmentState()) + store = storeProvider.get { restoredState -> + AutofillFragmentStore(restoredState ?: AutofillFragmentState()) } interactor = DefaultCreditCardsManagementInteractor( diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/AddLoginFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/AddLoginFragment.kt @@ -24,6 +24,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import mozilla.components.lib.state.ext.consumeFrom +import mozilla.components.lib.state.helpers.StoreProvider.Companion.navBackStackStore import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.components.support.ktx.android.view.showKeyboard import mozilla.components.support.ktx.util.URLStringUtils @@ -31,7 +32,6 @@ import org.mozilla.fenix.GleanMetrics.Logins import org.mozilla.fenix.R import org.mozilla.fenix.biometricauthentication.AuthenticationStatus import org.mozilla.fenix.biometricauthentication.BiometricAuthenticationManager -import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.databinding.FragmentAddLoginBinding import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.registerForActivityResult @@ -83,12 +83,10 @@ class AddLoginFragment : Fragment(R.layout.fragment_add_login), MenuProvider { _binding = FragmentAddLoginBinding.bind(view) - loginsFragmentStore = - StoreProvider.get(findNavController().getBackStackEntry(R.id.savedLogins)) { - LoginsFragmentStore( - createInitialLoginsListState(requireContext().settings()), - ) - } + loginsFragmentStore = findNavController().getBackStackEntry(R.id.savedLogins) + .navBackStackStore(createInitialLoginsListState(requireContext().settings())) { + LoginsFragmentStore(it) + }.value interactor = AddLoginInteractor( SavedLoginsStorageController( diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/EditLoginFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/EditLoginFragment.kt @@ -25,13 +25,13 @@ import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.google.android.material.textfield.TextInputLayout import mozilla.components.lib.state.ext.consumeFrom +import mozilla.components.lib.state.helpers.StoreProvider.Companion.navBackStackStore import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.telemetry.glean.private.NoExtras import org.mozilla.fenix.GleanMetrics.Logins import org.mozilla.fenix.R import org.mozilla.fenix.biometricauthentication.AuthenticationStatus import org.mozilla.fenix.biometricauthentication.BiometricAuthenticationManager -import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.databinding.FragmentEditLoginBinding import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.registerForActivityResult @@ -53,7 +53,6 @@ import org.mozilla.fenix.settings.logins.togglePasswordReveal class EditLoginFragment : Fragment(R.layout.fragment_edit_login), MenuProvider { private val args by navArgs<EditLoginFragmentArgs>() - private lateinit var loginsFragmentStore: LoginsFragmentStore private lateinit var interactor: EditLoginInteractor private lateinit var oldLogin: SavedLogin @@ -89,11 +88,9 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login), MenuProvider { oldLogin = args.savedLoginItem - loginsFragmentStore = - StoreProvider.get(findNavController().getBackStackEntry(R.id.savedLogins)) { - LoginsFragmentStore( - createInitialLoginsListState(requireContext().settings()), - ) + val loginsFragmentStore by findNavController().getBackStackEntry(R.id.savedLogins) + .navBackStackStore(createInitialLoginsListState(requireContext().settings())) { + LoginsFragmentStore(it) } interactor = EditLoginInteractor( diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/LoginDetailFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/LoginDetailFragment.kt @@ -29,6 +29,7 @@ import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.google.android.material.dialog.MaterialAlertDialogBuilder import mozilla.components.lib.state.ext.consumeFrom +import mozilla.components.lib.state.helpers.StoreProvider.Companion.navBackStackStore import mozilla.components.ui.widgets.withCenterAlignedButtons import mozilla.telemetry.glean.private.NoExtras import org.mozilla.fenix.BrowserDirection @@ -38,7 +39,6 @@ import org.mozilla.fenix.R import org.mozilla.fenix.SecureFragment import org.mozilla.fenix.biometricauthentication.AuthenticationStatus import org.mozilla.fenix.biometricauthentication.BiometricAuthenticationManager -import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.compose.snackbar.Snackbar import org.mozilla.fenix.compose.snackbar.SnackbarState import org.mozilla.fenix.databinding.FragmentLoginDetailBinding @@ -65,7 +65,6 @@ class LoginDetailFragment : SecureFragment(R.layout.fragment_login_detail), Menu private val args by navArgs<LoginDetailFragmentArgs>() private var login: SavedLogin? = null - private lateinit var savedLoginsStore: LoginsFragmentStore private lateinit var loginDetailsBindingDelegate: LoginDetailsBindingDelegate private lateinit var interactor: LoginDetailInteractor private var menu: Menu? = null @@ -91,12 +90,6 @@ class LoginDetailFragment : SecureFragment(R.layout.fragment_login_detail), Menu setSecureContentVisibility(true) } - savedLoginsStore = - StoreProvider.get(findNavController().getBackStackEntry(R.id.savedLogins)) { - LoginsFragmentStore( - createInitialLoginsListState(requireContext().settings()), - ) - } loginDetailsBindingDelegate = LoginDetailsBindingDelegate(binding) return view @@ -106,6 +99,11 @@ class LoginDetailFragment : SecureFragment(R.layout.fragment_login_detail), Menu super.onViewCreated(view, savedInstanceState) requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) + val savedLoginsStore by findNavController().getBackStackEntry(R.id.savedLogins) + .navBackStackStore(createInitialLoginsListState(requireContext().settings())) { + LoginsFragmentStore(it) + } + interactor = LoginDetailInteractor( SavedLoginsStorageController( passwordsStorage = requireContext().components.core.passwordsStorage, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsFragment.kt @@ -37,6 +37,7 @@ import mozilla.components.concept.menu.MenuController import mozilla.components.concept.menu.Orientation import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.lib.state.helpers.StoreProvider.Companion.fragmentStore +import mozilla.components.lib.state.helpers.StoreProvider.Companion.navBackStackStore import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.Config import org.mozilla.fenix.HomeActivity @@ -45,7 +46,6 @@ import org.mozilla.fenix.SecureFragment import org.mozilla.fenix.biometricauthentication.AuthenticationStatus import org.mozilla.fenix.biometricauthentication.BiometricAuthenticationManager import org.mozilla.fenix.components.LogMiddleware -import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.databinding.FragmentSavedLoginsBinding import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.hideToolbar @@ -216,12 +216,10 @@ class SavedLoginsFragment : SecureFragment(), MenuProvider { _binding = FragmentSavedLoginsBinding.bind(view) - savedLoginsStore = - StoreProvider.get(findNavController().getBackStackEntry(R.id.savedLogins)) { - LoginsFragmentStore( - createInitialLoginsListState(requireContext().settings()), - ) - } + savedLoginsStore = findNavController().getBackStackEntry(R.id.savedLogins) + .navBackStackStore(createInitialLoginsListState(requireContext().settings())) { + LoginsFragmentStore(it) + }.value loginsListController = LoginsListController( diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerPanelDialogFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerPanelDialogFragment.kt @@ -17,9 +17,9 @@ 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 mozilla.components.lib.state.helpers.StoreProvider.Companion.storeProvider 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 @@ -52,9 +52,9 @@ class CookieBannerPanelDialogFragment : FenixDialogFragment() { val rootView = inflateRootView(container) val tab = store.state.findTabOrCustomTab(provideCurrentTabId()) - protectionsStore = StoreProvider.get(this) { + protectionsStore = storeProvider.get { restoredState -> ProtectionsStore( - ProtectionsState( + restoredState ?: ProtectionsState( tab = tab, url = args.url, isTrackingProtectionEnabled = args.trackingProtectionEnabled, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt @@ -40,6 +40,7 @@ import mozilla.components.feature.accounts.push.CloseTabsUseCases import mozilla.components.feature.downloads.ui.DownloadCancelDialogFragment import mozilla.components.feature.tabs.tabstray.TabsFeature import mozilla.components.lib.state.ext.observeAsState +import mozilla.components.lib.state.helpers.StoreProvider.Companion.storeProvider import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.ktx.android.util.AndroidDisplayUnitConverter import mozilla.telemetry.glean.private.NoExtras @@ -49,7 +50,6 @@ import org.mozilla.fenix.GleanMetrics.TabsTray import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.R -import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.compose.core.Action import org.mozilla.fenix.compose.snackbar.Snackbar import org.mozilla.fenix.compose.snackbar.SnackbarState @@ -137,16 +137,6 @@ class TabsTrayFragment : AppCompatDialogFragment() { args.accessPoint.takeIf { it != TabsTrayAccessPoint.None }?.let { TabsTray.accessPoint[it.name.lowercase()].add() } - val initialMode = if (args.enterMultiselect) { - TabsTrayState.Mode.Select(emptySet()) - } else { - TabsTrayState.Mode.Normal - } - val initialPage = args.page - val activity = activity as HomeActivity - val initialInactiveExpanded = requireComponents.appStore.state.inactiveTabsExpanded - val inactiveTabs = requireComponents.core.store.state.actualInactiveTabs(requireContext().settings()) - val normalTabs = requireComponents.core.store.state.normalTabs - inactiveTabs.toSet() enablePbmPinLauncher = registerForActivityResult( onSuccess = { @@ -159,63 +149,6 @@ class TabsTrayFragment : AppCompatDialogFragment() { }, ) - tabsTrayStore = StoreProvider.get(this) { - TabsTrayStore( - initialState = TabsTrayState( - selectedPage = initialPage, - mode = initialMode, - inactiveTabs = inactiveTabs, - inactiveTabsExpanded = initialInactiveExpanded, - normalTabs = normalTabs, - privateTabs = requireComponents.core.store.state.privateTabs, - selectedTabId = requireComponents.core.store.state.selectedTabId, - ), - middlewares = listOf( - TabsTrayTelemetryMiddleware(requireComponents.nimbus.events), - ), - ) - } - - navigationInteractor = - DefaultNavigationInteractor( - browserStore = requireComponents.core.store, - navController = findNavController(), - dismissTabTray = ::dismissTabsTray, - dismissTabTrayAndNavigateHome = ::dismissTabsTrayAndNavigateHome, - showCancelledDownloadWarning = ::showCancelledDownloadWarning, - accountManager = requireComponents.backgroundServices.accountManager, - ) - - tabsTrayController = DefaultTabsTrayController( - activity = activity, - appStore = requireComponents.appStore, - tabsTrayStore = tabsTrayStore, - browserStore = requireComponents.core.store, - settings = requireContext().settings(), - browsingModeManager = activity.browsingModeManager, - navController = findNavController(), - navigateToHomeAndDeleteSession = ::navigateToHomeAndDeleteSession, - navigationInteractor = navigationInteractor, - profiler = requireComponents.core.engine.profiler, - tabsUseCases = requireComponents.useCases.tabsUseCases, - fenixBrowserUseCases = requireComponents.useCases.fenixBrowserUseCases, - closeSyncedTabsUseCases = requireComponents.useCases.closeSyncedTabsUseCases, - bookmarksStorage = requireComponents.core.bookmarksStorage, - ioDispatcher = Dispatchers.IO, - collectionStorage = requireComponents.core.tabCollectionStorage, - dismissTray = ::dismissTabsTray, - showUndoSnackbarForTab = ::showUndoSnackbarForTab, - showUndoSnackbarForInactiveTab = ::showUndoSnackbarForInactiveTab, - showUndoSnackbarForSyncedTab = ::showUndoSnackbarForSyncedTab, - showCancelledDownloadWarning = ::showCancelledDownloadWarning, - showCollectionSnackbar = ::showCollectionSnackbar, - showBookmarkSnackbar = ::showBookmarkSnackbar, - ) - - tabsTrayInteractor = DefaultTabsTrayInteractor( - controller = tabsTrayController, - ) - recordBreadcrumb("TabsTrayFragment onCreateDialog") tabsTrayDialog = TabsTrayDialog(requireContext(), theme) { tabsTrayInteractor } @@ -252,6 +185,8 @@ class TabsTrayFragment : AppCompatDialogFragment() { true, ) + setupUserInteractionsHandling() + tabsTrayComposeBinding.root .setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) tabsTrayComposeBinding.root.setContent { @@ -422,6 +357,76 @@ class TabsTrayFragment : AppCompatDialogFragment() { return tabsTrayDialogBinding.root } + private fun setupUserInteractionsHandling() { + val args by navArgs<TabsTrayFragmentArgs>() + val initialMode = if (args.enterMultiselect) { + TabsTrayState.Mode.Select(emptySet()) + } else { + TabsTrayState.Mode.Normal + } + val initialPage = args.page + val activity = activity as HomeActivity + val initialInactiveExpanded = requireComponents.appStore.state.inactiveTabsExpanded + val inactiveTabs = requireComponents.core.store.state.actualInactiveTabs(requireContext().settings()) + val normalTabs = requireComponents.core.store.state.normalTabs - inactiveTabs.toSet() + tabsTrayStore = storeProvider.get { restoredState -> + TabsTrayStore( + initialState = restoredState ?: TabsTrayState( + selectedPage = initialPage, + mode = initialMode, + inactiveTabs = inactiveTabs, + inactiveTabsExpanded = initialInactiveExpanded, + normalTabs = normalTabs, + privateTabs = requireComponents.core.store.state.privateTabs, + selectedTabId = requireComponents.core.store.state.selectedTabId, + ), + middlewares = listOf( + TabsTrayTelemetryMiddleware(requireComponents.nimbus.events), + ), + ) + } + + navigationInteractor = + DefaultNavigationInteractor( + browserStore = requireComponents.core.store, + navController = findNavController(), + dismissTabTray = ::dismissTabsTray, + dismissTabTrayAndNavigateHome = ::dismissTabsTrayAndNavigateHome, + showCancelledDownloadWarning = ::showCancelledDownloadWarning, + accountManager = requireComponents.backgroundServices.accountManager, + ) + + tabsTrayController = DefaultTabsTrayController( + activity = activity, + appStore = requireComponents.appStore, + tabsTrayStore = tabsTrayStore, + browserStore = requireComponents.core.store, + settings = requireContext().settings(), + browsingModeManager = activity.browsingModeManager, + navController = findNavController(), + navigateToHomeAndDeleteSession = ::navigateToHomeAndDeleteSession, + navigationInteractor = navigationInteractor, + profiler = requireComponents.core.engine.profiler, + tabsUseCases = requireComponents.useCases.tabsUseCases, + fenixBrowserUseCases = requireComponents.useCases.fenixBrowserUseCases, + closeSyncedTabsUseCases = requireComponents.useCases.closeSyncedTabsUseCases, + bookmarksStorage = requireComponents.core.bookmarksStorage, + ioDispatcher = Dispatchers.IO, + collectionStorage = requireComponents.core.tabCollectionStorage, + dismissTray = ::dismissTabsTray, + showUndoSnackbarForTab = ::showUndoSnackbarForTab, + showUndoSnackbarForInactiveTab = ::showUndoSnackbarForInactiveTab, + showUndoSnackbarForSyncedTab = ::showUndoSnackbarForSyncedTab, + showCancelledDownloadWarning = ::showCancelledDownloadWarning, + showCollectionSnackbar = ::showCollectionSnackbar, + showBookmarkSnackbar = ::showBookmarkSnackbar, + ) + + tabsTrayInteractor = DefaultTabsTrayInteractor( + controller = tabsTrayController, + ) + } + private fun shouldShowBanner(settings: Settings) = with(settings) { privateBrowsingLockedFeatureEnabled && shouldShowLockPbmBanner } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/TabManagementFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/TabManagementFragment.kt @@ -43,6 +43,7 @@ import mozilla.components.feature.accounts.push.CloseTabsUseCases import mozilla.components.feature.downloads.ui.DownloadCancelDialogFragment import mozilla.components.feature.tabs.tabstray.TabsFeature import mozilla.components.lib.state.ext.observeAsState +import mozilla.components.lib.state.helpers.StoreProvider.Companion.storeProvider import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.ktx.android.view.setSystemBarsBackground import mozilla.telemetry.glean.private.NoExtras @@ -51,7 +52,6 @@ import org.mozilla.fenix.GleanMetrics.PrivateBrowsingLocked import org.mozilla.fenix.GleanMetrics.TabsTray import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R -import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.ext.actualInactiveTabs import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.getBottomToolbarHeight @@ -120,6 +120,29 @@ class TabManagementFragment : DialogFragment() { super.onCreate(savedInstanceState) recordBreadcrumb("TabManagementFragment onCreate") + enablePbmPinLauncher = registerForActivityResult( + onSuccess = { + PrivateBrowsingLocked.authSuccess.record() + PrivateBrowsingLocked.featureEnabled.record() + requireContext().settings().privateBrowsingModeLocked = true + }, + onFailure = { + PrivateBrowsingLocked.authFailure.record() + }, + ) + + setStyle(STYLE_NO_TITLE, R.style.TabManagerDialogStyle) + } + + @Suppress("LongMethod", "CognitiveComplexMethod") + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View? { + // Remove the window dimming so the Toolbar UI from Home/Browser is still visible during the transition + dialog?.window?.setDimAmount(0f) + val args by navArgs<TabManagementFragmentArgs>() args.accessPoint.takeIf { it != TabsTrayAccessPoint.None }?.let { TabsTray.accessPoint[it.name.lowercase()].add() @@ -135,20 +158,9 @@ class TabManagementFragment : DialogFragment() { val inactiveTabs = requireComponents.core.store.state.actualInactiveTabs(requireContext().settings()) val normalTabs = requireComponents.core.store.state.normalTabs - inactiveTabs.toSet() - enablePbmPinLauncher = registerForActivityResult( - onSuccess = { - PrivateBrowsingLocked.authSuccess.record() - PrivateBrowsingLocked.featureEnabled.record() - requireContext().settings().privateBrowsingModeLocked = true - }, - onFailure = { - PrivateBrowsingLocked.authFailure.record() - }, - ) - - tabsTrayStore = StoreProvider.get(this) { + tabsTrayStore = storeProvider.get { restoredState -> TabsTrayStore( - initialState = TabsTrayState( + initialState = restoredState ?: TabsTrayState( selectedPage = initialPage, mode = initialMode, inactiveTabs = inactiveTabs, @@ -192,18 +204,6 @@ class TabManagementFragment : DialogFragment() { controller = tabManagerController, ) - setStyle(STYLE_NO_TITLE, R.style.TabManagerDialogStyle) - } - - @Suppress("LongMethod", "CognitiveComplexMethod") - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ): View? { - // Remove the window dimming so the Toolbar UI from Home/Browser is still visible during the transition - dialog?.window?.setDimAmount(0f) - return content { val page by tabsTrayStore.observeAsState(tabsTrayStore.state.selectedPage) { it.selectedPage } val isPbmLocked by requireComponents.appStore diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragment.kt @@ -36,6 +36,7 @@ import mozilla.components.browser.state.store.BrowserStore import mozilla.components.feature.session.TrackingProtectionUseCases import mozilla.components.lib.state.ext.consumeFlow import mozilla.components.lib.state.ext.observe +import mozilla.components.lib.state.helpers.StoreProvider.Companion.storeProvider import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.log.logger.Logger import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged @@ -44,7 +45,6 @@ import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.GleanMetrics.TrackingProtection import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R -import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.databinding.FragmentTrackingProtectionBinding import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.requireComponents @@ -89,9 +89,9 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), UserInt val view = inflateRootView(container) val tab = store.state.findTabOrCustomTab(provideCurrentTabId()) - protectionsStore = StoreProvider.get(this) { + protectionsStore = storeProvider.get { restoredState -> ProtectionsStore( - ProtectionsState( + restoredState ?: ProtectionsState( tab = tab, url = args.url, isTrackingProtectionEnabled = args.trackingProtectionEnabled, diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/StoreProviderTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/StoreProviderTest.kt @@ -1,100 +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.components - -import androidx.fragment.app.Fragment -import kotlinx.coroutines.CoroutineScope -import mozilla.components.lib.state.Action -import mozilla.components.lib.state.State -import mozilla.components.lib.state.Store -import mozilla.components.support.test.robolectric.createAddedTestFragment -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertSame -import org.junit.Assert.assertTrue -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class StoreProviderTest { - - private class BasicState : State - - private val basicStore = Store(BasicState(), { state, _: Action -> state }) - - @Test - fun `factory returns store provider`() { - var createCalled = false - val factory = StoreProviderFactory { - createCalled = true - basicStore - } - - assertFalse(createCalled) - - assertEquals(basicStore, factory.create(StoreProvider::class.java).store) - - assertTrue(createCalled) - } - - @Test - fun `get returns store`() { - val fragment = createAddedTestFragment { Fragment() } - - val store = StoreProvider.get(fragment) { basicStore } - assertEquals(basicStore, store) - } - - @Test - fun `get only calls createStore if needed`() { - val fragment = createAddedTestFragment { Fragment() } - - var createCalled = false - val createStore: (CoroutineScope) -> Store<BasicState, Action> = { - createCalled = true - basicStore - } - - StoreProvider.get(fragment, createStore) - assertTrue(createCalled) - - createCalled = false - StoreProvider.get(fragment, createStore) - assertFalse(createCalled) - } - - @Test - fun `GIVEN different stores are persisted WHEN requesting them THEN get their unique instances`() { - val fragment = createAddedTestFragment { Fragment() } - var createACalled = false - val storeAFactory: (CoroutineScope) -> Store<BasicState, Action> = { - createACalled = true - basicStore - } - var createBCalled = false - val storeBFactory: (CoroutineScope) -> StoreB = { - createBCalled = true - StoreB(BasicState()) - } - - val storeA: Store<BasicState, Action> = StoreProvider.get(fragment, storeAFactory) - val storeB: StoreB = StoreProvider.get(fragment, storeBFactory) - assertTrue(createACalled) - assertTrue(createBCalled) - - createACalled = false - createBCalled = false - assertSame(storeA, StoreProvider.get(fragment, storeAFactory)) - assertSame(storeB, StoreProvider.get(fragment, storeBFactory)) - assertFalse(createACalled) - assertFalse(createBCalled) - } - - private class StoreB(initialState: BasicState) : Store<BasicState, Action>( - initialState, - { state, _: Action -> state }, - ) -} 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 @@ -5,7 +5,6 @@ package org.mozilla.fenix.iconpicker import androidx.test.ext.junit.runners.AndroidJUnit4 -import mozilla.components.support.test.ext.joinBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test @@ -89,22 +88,4 @@ 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) - } } diff --git a/mobile/android/fenix/docs/architecture-overview.md b/mobile/android/fenix/docs/architecture-overview.md @@ -27,7 +27,7 @@ It is recommended that consumers rely as much as possible on observing State upd There are several global stores like `AppStore` and `BrowserStore`, as well as Stores scoped to individual screens. Screen-based Stores can be persisted across configuration changes, but are generally created and destroyed during fragment transactions. This means that data that must be shared across Stores should be lifted to a global Store or should be passed as arguments to the new fragment. -Screen-based Stores should be created using [StoreProvider.get](https://searchfox.org/mozilla-central/source/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/StoreProvider.kt). +Screen-based Stores should be created using [StoreProvider APIs](https://searchfox.org/firefox-main/source/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/helpers/StoreProvider.kt). ------- @@ -106,7 +106,7 @@ In some cases, it can be appropriate to initiate side-effects from the view when ## Important notes - Unlike other common implementations of unidirectional data flow, which typically have one global Store of data, we maintain smaller Stores for each screen and several global Stores. - There is often no need to maintain UI state for views that are destroyed, and this allows us to to operate within the physical hardware constraints presented by Android development, such as having more limited memory resources. -- Stores that are local to a feature or screen should usually be persisted across configuration changes in a ViewModel by using [StoreProvider.get](https://searchfox.org/mozilla-central/source/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/StoreProvider.kt). +- Stores that are local to a feature or screen should usually be persisted across configuration changes in a ViewModel by using [StoreProvider APIs](https://searchfox.org/firefox-main/source/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/helpers/StoreProvider.kt). ------- diff --git a/mobile/android/fenix/docs/architectureexample/HistoryFragmentExample.kt b/mobile/android/fenix/docs/architectureexample/HistoryFragmentExample.kt @@ -6,17 +6,15 @@ // /docs/architecture-overview.md class HistoryFragment : Fragment() { - private val store by lazy { - StoreProvider.get(this) { - HistoryStore( - initialState = HistoryState.initial, - middleware = listOf( - HistoryNavigationMiddleware(findNavController()) - HistoryStorageMiddleware(HistoryStorage()), - HistoryTelemetryMiddleware(), - ) + private val store by storeFragment(HistoryState.initial) { restoredState -> + HistoryStore( + initialState = restoredState, + middleware = listOf( + HistoryNavigationMiddleware(findNavController()) + HistoryStorageMiddleware(HistoryStorage()), + HistoryTelemetryMiddleware(), ) - } + ) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {