diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java b/app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java index c015409bc0..803e455f0e 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java @@ -1,6 +1,7 @@ /* * Copyright (C) 2014-2026 Arpit Khurana , Vishal Nehra , - * Emmanuel Messulam, Raymond Lai and Contributors. + * Emmanuel Messulam, Raymond Lai , + * Arjun Thirumani and Contributors. * * This file is part of Amaze File Manager. * @@ -124,7 +125,6 @@ import androidx.core.graphics.drawable.IconCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; -import androidx.fragment.app.FragmentManager; import androidx.lifecycle.ViewModelProvider; import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.DefaultItemAnimator; @@ -145,7 +145,8 @@ public class MainFragment extends Fragment AdjustListViewForTv { private static final Logger LOG = LoggerFactory.getLogger(MainFragment.class); - private static final String KEY_FRAGMENT_MAIN = "main"; + private static final String KEY_SAVED_CURRENT_PATH = "saved_current_path"; + private static final String KEY_SAVED_OPEN_MODE = "saved_open_mode"; /** Key for boolean in arguments whether to hide the FAB if this {@link MainFragment} is shown */ public static final String BUNDLE_HIDE_FAB = "hideFab"; @@ -207,6 +208,16 @@ public void onCreate(Bundle savedInstanceState) { utilsProvider = requireMainActivity().getUtilsProvider(); sharedPref = PreferenceManager.getDefaultSharedPreferences(requireActivity()); mainFragmentViewModel.initBundleArguments(getArguments()); + if (savedInstanceState != null) { + String savedPath = savedInstanceState.getString(KEY_SAVED_CURRENT_PATH); + if (savedPath != null) { + mainFragmentViewModel.setCurrentPath(savedPath); + } + int savedOpenMode = savedInstanceState.getInt(KEY_SAVED_OPEN_MODE, -1); + if (savedOpenMode != -1) { + mainFragmentViewModel.setOpenMode(OpenMode.getOpenMode(savedOpenMode)); + } + } mainFragmentViewModel.initIsList(); mainFragmentViewModel.initColumns(sharedPref); mainFragmentViewModel.initSortModes( @@ -303,9 +314,16 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); - FragmentManager fragmentManager = requireActivity().getSupportFragmentManager(); - fragmentManager.executePendingTransactions(); - fragmentManager.putFragment(outState, KEY_FRAGMENT_MAIN, this); + if (mainFragmentViewModel != null) { + if (mainFragmentViewModel.getCurrentPath() != null) { + outState.putString(KEY_SAVED_CURRENT_PATH, mainFragmentViewModel.getCurrentPath()); + } + try { + outState.putInt(KEY_SAVED_OPEN_MODE, mainFragmentViewModel.getOpenMode().ordinal()); + } catch (Exception e) { + LOG.warn("Failed to save openMode", e); + } + } } public void stopAnimation() { diff --git a/app/src/test/java/com/amaze/filemanager/ui/fragments/MainFragmentStateTest.kt b/app/src/test/java/com/amaze/filemanager/ui/fragments/MainFragmentStateTest.kt new file mode 100644 index 0000000000..5ea8237ece --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/ui/fragments/MainFragmentStateTest.kt @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2026 Arjun Thirumani and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.ui.fragments + +import android.os.Build.VERSION_CODES.LOLLIPOP +import androidx.lifecycle.Lifecycle +import androidx.test.core.app.ActivityScenario +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.amaze.filemanager.fileoperations.filesystem.OpenMode +import com.amaze.filemanager.shadows.ShadowMultiDex +import com.amaze.filemanager.shadows.jcifs.smb.ShadowSmbFile +import com.amaze.filemanager.test.ShadowPasswordUtil +import com.amaze.filemanager.test.ShadowTabHandler +import com.amaze.filemanager.ui.activities.MainActivity +import io.reactivex.android.plugins.RxAndroidPlugins +import io.reactivex.plugins.RxJavaPlugins +import io.reactivex.schedulers.Schedulers +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import org.robolectric.shadows.ShadowLooper +import org.robolectric.shadows.ShadowSQLiteConnection +import org.robolectric.shadows.ShadowStorageManager + +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config( + sdk = [LOLLIPOP], + shadows = [ + ShadowMultiDex::class, + ShadowStorageManager::class, + ShadowPasswordUtil::class, + ShadowSmbFile::class, + ShadowTabHandler::class, + ], +) +class MainFragmentStateTest { + + private lateinit var scenario: ActivityScenario + + /** + * Sets up the RxJava and RxAndroid schedulers to run synchronously for testing. + */ + @Before + fun setUp() { + RxJavaPlugins.reset() + RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() } + RxAndroidPlugins.reset() + RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() } + RxAndroidPlugins.setMainThreadSchedulerHandler { Schedulers.trampoline() } + ShadowSQLiteConnection.reset() + } + + /** + * Cleans up the Rx schedulers and releases the activity scenario. + */ + @After + fun tearDown() { + if (::scenario.isInitialized) { + scenario.close() + } + ShadowSQLiteConnection.reset() + RxAndroidPlugins.reset() + RxJavaPlugins.reset() + } + + private fun MainActivity.firstMainFragment(): MainFragment? = getTabFragment()?.getFragmentAtIndex(0) as? MainFragment + + /** + * Verifies that the MainFragment correctly persists its internal state + * (currentPath and openMode) across activity recreation (e.g., rotation). + */ + @Test + fun testMainFragmentSavesAndRestoresState() { + scenario = ActivityScenario.launch(MainActivity::class.java) + ShadowLooper.idleMainLooper() + scenario.moveToState(Lifecycle.State.RESUMED) + + scenario.onActivity { activity -> + val mainFragment = activity.firstMainFragment() + assertNotNull("MainFragment must be attached", mainFragment) + val viewModel = mainFragment!!.mainFragmentViewModel + + // Set custom state that is different from default + viewModel!!.currentPath = "0" + viewModel!!.openMode = OpenMode.CUSTOM + } + + // Trigger recreation (simulates configuration change/process death) + scenario.recreate() + ShadowLooper.idleMainLooper() + + scenario.onActivity { activity -> + val mainFragment = activity.firstMainFragment() + assertNotNull("MainFragment must be restored", mainFragment) + val viewModel = mainFragment!!.mainFragmentViewModel + + assertEquals("Path should be restored after recreation", "0", viewModel!!.currentPath) + assertEquals("OpenMode should be restored after recreation", OpenMode.CUSTOM, viewModel!!.openMode) + } + } +}