Skip to content

Commit 94d02de

Browse files
committed
Guards and null checks against unsaved fragment transactions on configuration change
Fixes #1555
1 parent da62dc2 commit 94d02de

File tree

4 files changed

+287
-123
lines changed

4 files changed

+287
-123
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package com.amaze.filemanager.ui.fragments
2+
3+
import android.content.pm.ActivityInfo
4+
import android.os.Bundle
5+
import androidx.test.espresso.Espresso.onView
6+
import androidx.test.espresso.action.ViewActions.swipeLeft
7+
import androidx.test.espresso.action.ViewActions.swipeRight
8+
import androidx.test.espresso.matcher.ViewMatchers.withId
9+
import androidx.test.ext.junit.runners.AndroidJUnit4
10+
import androidx.test.platform.app.InstrumentationRegistry
11+
import androidx.test.rule.ActivityTestRule
12+
import androidx.test.rule.GrantPermissionRule
13+
import com.amaze.filemanager.R
14+
import com.amaze.filemanager.ui.activities.MainActivity
15+
import org.junit.Rule
16+
import org.junit.Test
17+
import org.junit.runner.RunWith
18+
19+
@RunWith(AndroidJUnit4::class)
20+
class TabFragmentTest {
21+
22+
@get:Rule
23+
val activityRule = ActivityTestRule(MainActivity::class.java)
24+
25+
@Rule
26+
@JvmField
27+
val storagePermissionRule: GrantPermissionRule =
28+
GrantPermissionRule
29+
.grant(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
30+
31+
@Test
32+
fun testFragmentStateSavingDuringConfigChange() {
33+
// First perform the swipe action
34+
onView(withId(R.id.pager)).perform(swipeLeft())
35+
36+
// Force a configuration change by rotating the screen
37+
activityRule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
38+
Thread.sleep(1000) // Give time for the rotation to complete
39+
activityRule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
40+
}
41+
42+
@Test
43+
fun testRapidTabSwitchingAndStateSaving() {
44+
// Perform rapid tab switches
45+
repeat(10) {
46+
onView(withId(R.id.pager)).perform(swipeLeft())
47+
Thread.sleep(100) // Small delay to ensure swipe completes
48+
onView(withId(R.id.pager)).perform(swipeRight())
49+
Thread.sleep(100) // Small delay to ensure swipe completes
50+
}
51+
52+
// Force a save state by rotating
53+
activityRule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
54+
}
55+
56+
@Test
57+
fun testFragmentDetachmentAndStateSaving() {
58+
// First switch to a different tab
59+
onView(withId(R.id.pager)).perform(swipeLeft())
60+
61+
// Get the TabFragment
62+
InstrumentationRegistry.getInstrumentation().runOnMainSync {
63+
val activity = activityRule.activity
64+
val tabFragment = activity.supportFragmentManager
65+
.findFragmentById(R.id.content_frame) as TabFragment
66+
67+
// Detach fragment through FragmentManager
68+
activity.supportFragmentManager.beginTransaction().apply {
69+
tabFragment.fragments.firstOrNull()?.let { detach(it) }
70+
commit()
71+
}
72+
}
73+
74+
// Force state save through configuration change
75+
activityRule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
76+
}
77+
}

app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -683,7 +683,7 @@ public void createHeaders(boolean invalidate, List<IconDataParcelable> uris) {
683683

684684
@Override
685685
public int getItemCount() {
686-
return getItemsDigested().size();
686+
return getItemsDigested() != null ? getItemsDigested().size() : 0;
687687
}
688688

689689
@Override

app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java

Lines changed: 148 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,9 @@ public void switchView() {
384384
reloadListElements(false, isPathLayoutGrid);
385385
}
386386

387-
private void loadViews() {
387+
void loadViews() {
388+
if (!isAdded() || getView() == null) return;
389+
388390
if (mainFragmentViewModel.getCurrentPath() != null) {
389391
if (mainFragmentViewModel.getListElements().size() == 0) {
390392
loadlist(
@@ -813,137 +815,148 @@ public void setListElements(
813815
}
814816

815817
public void reloadListElements(boolean back, boolean grid) {
816-
if (isAdded()) {
817-
boolean isOtg = (OTGUtil.PREFIX_OTG + "/").equals(mainFragmentViewModel.getCurrentPath());
818-
819-
if (getBoolean(PREFERENCE_SHOW_GOBACK_BUTTON)
820-
&& !"/".equals(mainFragmentViewModel.getCurrentPath())
821-
&& (mainFragmentViewModel.getOpenMode() == OpenMode.FILE
822-
|| mainFragmentViewModel.getOpenMode() == OpenMode.ROOT
823-
|| (mainFragmentViewModel.getIsCloudOpenMode()
824-
&& !mainFragmentViewModel.getIsOnCloudRoot()))
825-
&& !isOtg
826-
&& (mainFragmentViewModel.getListElements().size() == 0
827-
|| !mainFragmentViewModel
828-
.getListElements()
829-
.get(0)
830-
.size
831-
.equals(getString(R.string.goback)))) {
832-
mainFragmentViewModel.getListElements().add(0, getBackElement());
833-
}
818+
if (!isAdded() || getView() == null) return;
834819

835-
if (mainFragmentViewModel.getListElements().size() == 0) {
836-
nofilesview.setVisibility(View.VISIBLE);
837-
listView.setVisibility(View.GONE);
838-
mSwipeRefreshLayout.setEnabled(false);
839-
} else {
840-
mSwipeRefreshLayout.setEnabled(true);
841-
nofilesview.setVisibility(View.GONE);
842-
listView.setVisibility(View.VISIBLE);
820+
if (listView != null) {
821+
listView.removeCallbacks(null);
822+
}
823+
824+
// Initialize views if they're null
825+
if (mSwipeRefreshLayout == null || listView == null) {
826+
initViews();
827+
if (mSwipeRefreshLayout == null || listView == null) {
828+
LOG.warn("Failed to initialize views in reloadListElements");
829+
return;
843830
}
831+
}
844832

845-
if (grid && mainFragmentViewModel.isList()) {
846-
switchToGrid();
847-
} else if (!grid && !mainFragmentViewModel.isList()) {
848-
switchToList();
849-
}
833+
if (mainFragmentViewModel.getListElements().size() == 0) {
834+
if (nofilesview != null) nofilesview.setVisibility(View.VISIBLE);
835+
if (listView != null) listView.setVisibility(View.GONE);
836+
if (mSwipeRefreshLayout != null) mSwipeRefreshLayout.setEnabled(false);
837+
} else {
838+
if (nofilesview != null) nofilesview.setVisibility(View.GONE);
839+
if (listView != null) listView.setVisibility(View.VISIBLE);
840+
if (mSwipeRefreshLayout != null) mSwipeRefreshLayout.setEnabled(true);
841+
}
850842

851-
if (adapter == null) {
852-
final List<LayoutElementParcelable> listElements = mainFragmentViewModel.getListElements();
843+
boolean isOtg = (OTGUtil.PREFIX_OTG + "/").equals(mainFragmentViewModel.getCurrentPath());
844+
845+
if (getBoolean(PREFERENCE_SHOW_GOBACK_BUTTON)
846+
&& !"/".equals(mainFragmentViewModel.getCurrentPath())
847+
&& (mainFragmentViewModel.getOpenMode() == OpenMode.FILE
848+
|| mainFragmentViewModel.getOpenMode() == OpenMode.ROOT
849+
|| (mainFragmentViewModel.getIsCloudOpenMode()
850+
&& !mainFragmentViewModel.getIsOnCloudRoot()))
851+
&& !isOtg
852+
&& (mainFragmentViewModel.getListElements().size() == 0
853+
|| !mainFragmentViewModel
854+
.getListElements()
855+
.get(0)
856+
.size
857+
.equals(getString(R.string.goback)))) {
858+
mainFragmentViewModel.getListElements().add(0, getBackElement());
859+
}
853860

854-
adapter =
855-
new RecyclerAdapter(
856-
requireMainActivity(),
857-
this,
858-
utilsProvider,
859-
sharedPref,
860-
listView,
861-
listElements,
862-
requireContext(),
863-
grid);
864-
} else {
865-
adapter.setItems(listView, mainFragmentViewModel.getListElements());
866-
}
861+
if (grid && mainFragmentViewModel.isList()) {
862+
switchToGrid();
863+
} else if (!grid && !mainFragmentViewModel.isList()) {
864+
switchToList();
865+
}
867866

868-
mainFragmentViewModel.setStopAnims(true);
867+
if (adapter == null) {
868+
final List<LayoutElementParcelable> listElements = mainFragmentViewModel.getListElements();
869869

870-
if (mainFragmentViewModel.getOpenMode() != OpenMode.CUSTOM
871-
&& mainFragmentViewModel.getOpenMode() != OpenMode.TRASH_BIN) {
872-
DataUtils.getInstance().addHistoryFile(mainFragmentViewModel.getCurrentPath());
873-
}
870+
adapter =
871+
new RecyclerAdapter(
872+
requireMainActivity(),
873+
this,
874+
utilsProvider,
875+
sharedPref,
876+
listView,
877+
listElements,
878+
requireContext(),
879+
grid);
880+
} else {
881+
adapter.setItems(listView, mainFragmentViewModel.getListElements());
882+
}
874883

875-
listView.setAdapter(adapter);
884+
mainFragmentViewModel.setStopAnims(true);
876885

877-
if (!mainFragmentViewModel.getAddHeader()) {
878-
listView.removeItemDecoration(dividerItemDecoration);
879-
mainFragmentViewModel.setAddHeader(true);
880-
}
886+
if (mainFragmentViewModel.getOpenMode() != OpenMode.CUSTOM
887+
&& mainFragmentViewModel.getOpenMode() != OpenMode.TRASH_BIN) {
888+
DataUtils.getInstance().addHistoryFile(mainFragmentViewModel.getCurrentPath());
889+
}
881890

882-
if (mainFragmentViewModel.getAddHeader() && mainFragmentViewModel.isList()) {
883-
dividerItemDecoration =
884-
new DividerItemDecoration(
885-
requireMainActivity(), true, getBoolean(PREFERENCE_SHOW_DIVIDERS));
886-
listView.addItemDecoration(dividerItemDecoration);
887-
mainFragmentViewModel.setAddHeader(false);
888-
}
891+
listView.setAdapter(adapter);
889892

890-
if (back && scrolls.containsKey(mainFragmentViewModel.getCurrentPath())) {
891-
Bundle b = scrolls.get(mainFragmentViewModel.getCurrentPath());
892-
int index = b.getInt("index"), top = b.getInt("top");
893-
if (mainFragmentViewModel.isList()) {
894-
mLayoutManager.scrollToPositionWithOffset(index, top);
895-
} else {
896-
mLayoutManagerGrid.scrollToPositionWithOffset(index, top);
897-
}
893+
if (!mainFragmentViewModel.getAddHeader()) {
894+
listView.removeItemDecoration(dividerItemDecoration);
895+
mainFragmentViewModel.setAddHeader(true);
896+
}
897+
898+
if (mainFragmentViewModel.getAddHeader() && mainFragmentViewModel.isList()) {
899+
dividerItemDecoration =
900+
new DividerItemDecoration(
901+
requireMainActivity(), true, getBoolean(PREFERENCE_SHOW_DIVIDERS));
902+
listView.addItemDecoration(dividerItemDecoration);
903+
mainFragmentViewModel.setAddHeader(false);
904+
}
905+
906+
if (back && scrolls.containsKey(mainFragmentViewModel.getCurrentPath())) {
907+
Bundle b = scrolls.get(mainFragmentViewModel.getCurrentPath());
908+
int index = b.getInt("index"), top = b.getInt("top");
909+
if (mainFragmentViewModel.isList()) {
910+
mLayoutManager.scrollToPositionWithOffset(index, top);
911+
} else {
912+
mLayoutManagerGrid.scrollToPositionWithOffset(index, top);
898913
}
914+
}
899915

900-
requireMainActivity().updatePaths(mainFragmentViewModel.getNo());
901-
requireMainActivity().showFab();
902-
requireMainActivity().getAppbar().getAppbarLayout().setExpanded(true);
903-
listView.stopScroll();
904-
fastScroller.setRecyclerView(
905-
listView,
906-
mainFragmentViewModel.isList()
907-
? 1
908-
: (mainFragmentViewModel.getColumns() == 0
909-
|| mainFragmentViewModel.getColumns() == -1)
910-
? 3
911-
: mainFragmentViewModel.getColumns());
912-
mToolbarContainer.addOnOffsetChangedListener(
913-
(appBarLayout, verticalOffset) -> {
914-
fastScroller.updateHandlePosition(verticalOffset, 112);
915-
});
916-
fastScroller.registerOnTouchListener(
917-
() -> {
918-
if (mainFragmentViewModel.getStopAnims() && adapter != null) {
919-
stopAnimation();
920-
mainFragmentViewModel.setStopAnims(false);
921-
}
922-
});
916+
requireMainActivity().updatePaths(mainFragmentViewModel.getNo());
917+
requireMainActivity().showFab();
918+
requireMainActivity().getAppbar().getAppbarLayout().setExpanded(true);
919+
listView.stopScroll();
920+
fastScroller.setRecyclerView(
921+
listView,
922+
mainFragmentViewModel.isList()
923+
? 1
924+
: (mainFragmentViewModel.getColumns() == 0
925+
|| mainFragmentViewModel.getColumns() == -1)
926+
? 3
927+
: mainFragmentViewModel.getColumns());
928+
mToolbarContainer.addOnOffsetChangedListener(
929+
(appBarLayout, verticalOffset) -> {
930+
fastScroller.updateHandlePosition(verticalOffset, 112);
931+
});
932+
fastScroller.registerOnTouchListener(
933+
() -> {
934+
if (mainFragmentViewModel.getStopAnims() && adapter != null) {
935+
stopAnimation();
936+
mainFragmentViewModel.setStopAnims(false);
937+
}
938+
});
923939

924-
startFileObserver();
925-
926-
listView.post(
927-
() -> {
928-
String fileName = requireMainActivity().getScrollToFileName();
929-
930-
if (fileName != null)
931-
mainFragmentViewModel
932-
.getScrollPosition(fileName)
933-
.observe(
934-
getViewLifecycleOwner(),
935-
scrollPosition -> {
936-
if (scrollPosition != -1)
937-
listView.scrollToPosition(
938-
Math.min(scrollPosition + 4, adapter.getItemCount() - 1));
939-
adapter.notifyItemChanged(scrollPosition);
940-
});
941-
});
940+
startFileObserver();
942941

943-
} else {
944-
// fragment not added
945-
initNoFileLayout();
946-
}
942+
listView.post(
943+
() -> {
944+
if (!isAdded()) return;
945+
946+
String fileName = requireMainActivity().getScrollToFileName();
947+
948+
if (fileName != null)
949+
mainFragmentViewModel
950+
.getScrollPosition(fileName)
951+
.observe(
952+
getViewLifecycleOwner(),
953+
scrollPosition -> {
954+
if (scrollPosition != -1)
955+
listView.scrollToPosition(
956+
Math.min(scrollPosition + 4, adapter.getItemCount() - 1));
957+
adapter.notifyItemChanged(scrollPosition);
958+
});
959+
});
947960
}
948961

949962
private LayoutElementParcelable getBackElement() {
@@ -1559,4 +1572,22 @@ public boolean getHideFab() {
15591572
public void setHideFab(boolean hideFab) {
15601573
this.hideFab = hideFab;
15611574
}
1575+
1576+
public void initViews() {
1577+
if (getView() == null) return;
1578+
1579+
if (mSwipeRefreshLayout == null) {
1580+
mSwipeRefreshLayout = getView().findViewById(R.id.activity_main_swipe_refresh_layout);
1581+
}
1582+
1583+
if (listView == null) {
1584+
listView = getView().findViewById(R.id.listView);
1585+
}
1586+
1587+
if (mToolbarContainer == null) {
1588+
mToolbarContainer = requireMainActivity().getAppbar().getAppbarLayout();
1589+
}
1590+
1591+
loadViews();
1592+
}
15621593
}

0 commit comments

Comments
 (0)