From 5944be021d4517131cd65f1a3f507183ecd68dcc Mon Sep 17 00:00:00 2001 From: thirumani-vihaan Date: Tue, 14 Jul 2026 17:38:31 +0530 Subject: [PATCH 1/2] [ai] app: Fix drawer category reset to Home directory on rotation Fixes: #4629 The issue occurs when a user navigates to a Drawer Category (like "Images") and rotates the device. After rotation, the view incorrectly resets to the default Home directory. This happens because the MainFragmentViewModel's currentPath is either lost or re-initialized from the original fragment arguments (which contain the Home path) upon recreation. To fix this, I implemented state-saving logic in MainFragment using onSaveInstanceState to explicitly preserve the currentPath and openMode, and restore them in onCreate(). --- .../ui/fragments/MainFragment.java | 30 ++++- .../ui/fragments/MainFragmentStateTest.kt | 114 ++++++++++++++++++ 2 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 app/src/test/java/com/amaze/filemanager/ui/fragments/MainFragmentStateTest.kt 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..58cfd757ee --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/ui/fragments/MainFragmentStateTest.kt @@ -0,0 +1,114 @@ +/* + * 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 + + @Before + fun setUp() { + RxJavaPlugins.reset() + RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() } + RxAndroidPlugins.reset() + RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() } + RxAndroidPlugins.setMainThreadSchedulerHandler { Schedulers.trampoline() } + ShadowSQLiteConnection.reset() + } + + @After + fun tearDown() { + if (::scenario.isInitialized) { + scenario.close() + } + ShadowSQLiteConnection.reset() + RxAndroidPlugins.reset() + RxJavaPlugins.reset() + } + + private fun MainActivity.firstMainFragment(): MainFragment? = getTabFragment()?.getFragmentAtIndex(0) as? MainFragment + + @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) + } + } +} From d0130b62084c8825536d7e21f8f6ea98288382a9 Mon Sep 17 00:00:00 2001 From: thirumani-vihaan Date: Tue, 14 Jul 2026 23:57:54 +0530 Subject: [PATCH 2/2] Add missing KDoc documentation to MainFragmentStateTest.kt --- .../ui/fragments/MainFragmentStateTest.kt | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) 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 index 58cfd757ee..5ea8237ece 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/fragments/MainFragmentStateTest.kt +++ b/app/src/test/java/com/amaze/filemanager/ui/fragments/MainFragmentStateTest.kt @@ -60,6 +60,9 @@ class MainFragmentStateTest { private lateinit var scenario: ActivityScenario + /** + * Sets up the RxJava and RxAndroid schedulers to run synchronously for testing. + */ @Before fun setUp() { RxJavaPlugins.reset() @@ -70,6 +73,9 @@ class MainFragmentStateTest { ShadowSQLiteConnection.reset() } + /** + * Cleans up the Rx schedulers and releases the activity scenario. + */ @After fun tearDown() { if (::scenario.isInitialized) { @@ -82,6 +88,10 @@ class MainFragmentStateTest { 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) @@ -94,8 +104,8 @@ class MainFragmentStateTest { val viewModel = mainFragment!!.mainFragmentViewModel // Set custom state that is different from default - viewModel.currentPath = "0" - viewModel.openMode = OpenMode.CUSTOM + viewModel!!.currentPath = "0" + viewModel!!.openMode = OpenMode.CUSTOM } // Trigger recreation (simulates configuration change/process death) @@ -107,8 +117,8 @@ class MainFragmentStateTest { 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) + assertEquals("Path should be restored after recreation", "0", viewModel!!.currentPath) + assertEquals("OpenMode should be restored after recreation", OpenMode.CUSTOM, viewModel!!.openMode) } } }