Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*
* Copyright (C) 2014-2026 Arpit Khurana <arpitkh96@gmail.com>, Vishal Nehra <vishalmeham2@gmail.com>,
* Emmanuel Messulam<emmanuelbendavid@gmail.com>, Raymond Lai <airwave209gt at gmail.com> and Contributors.
* Emmanuel Messulam<emmanuelbendavid@gmail.com>, Raymond Lai <airwave209gt at gmail.com>,
* Arjun Thirumani<arjunthirumani@gmail.com> and Contributors.
*
* This file is part of Amaze File Manager.
*
Expand Down Expand Up @@ -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;
Expand All @@ -145,7 +145,8 @@ public class MainFragment extends Fragment
AdjustListViewForTv<ItemViewHolder> {

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";
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright (C) 2026 Arjun Thirumani<arjunthirumani@gmail.com> 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 <http://www.gnu.org/licenses/>.
*/

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<MainActivity>

/**
* 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)
}
}
}