Skip to content

Commit 06c0ec6

Browse files
committed
fix: prevent terminal sessions ListView crash by giving adapter a UI-thread-only snapshot instead of sharing TermuxService's live session list mutated off the UI thread
1 parent 31705c9 commit 06c0ec6

3 files changed

Lines changed: 38 additions & 5 deletions

File tree

‎termux/termux-app/src/main/java/com/termux/app/TermuxActivity.java‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -628,7 +628,7 @@ public void onResetTerminalSession() {
628628

629629
private void setTermuxSessionsListView() {
630630
ListView termuxSessionsListView = findViewById(R.id.terminal_sessions_list);
631-
mTermuxSessionListViewController = new TermuxSessionsListViewController(this, mTermuxService.getTermuxSessions());
631+
mTermuxSessionListViewController = new TermuxSessionsListViewController(this, mTermuxService.getTermuxSessionsListSnapshot());
632632
termuxSessionsListView.setAdapter(mTermuxSessionListViewController);
633633
termuxSessionsListView.setOnItemClickListener(mTermuxSessionListViewController);
634634
termuxSessionsListView.setOnItemLongClickListener(mTermuxSessionListViewController);
@@ -974,14 +974,22 @@ public boolean isTerminalToolbarTextInputViewSelected() {
974974

975975

976976
public void termuxSessionListNotifyUpdated() {
977-
if (mTermuxSessionListViewController == null) return;
977+
// The session list may be mutated on a background thread (e.g. onCreateNewSession() runs
978+
// createTermuxSession() on a background executor). Re-snapshotting the service's list into
979+
// the adapter must therefore happen on the UI thread, so the adapter's own list is only
980+
// ever read/written by the UI thread and the ListView can never observe a concurrent change.
978981
if (Looper.myLooper() == Looper.getMainLooper()) {
979-
mTermuxSessionListViewController.notifyDataSetChanged();
982+
refreshSessionsListView();
980983
} else {
981-
TermuxExecutor.executeOnMain(mTermuxSessionListViewController::notifyDataSetChanged);
984+
TermuxExecutor.executeOnMain(this::refreshSessionsListView);
982985
}
983986
}
984987

988+
private void refreshSessionsListView() {
989+
if (mTermuxSessionListViewController == null || mTermuxService == null) return;
990+
mTermuxSessionListViewController.updateSessions(mTermuxService.getTermuxSessionsListSnapshot());
991+
}
992+
985993
public boolean isVisible() {
986994
return mIsVisible;
987995
}

‎termux/termux-app/src/main/java/com/termux/app/TermuxService.java‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,16 @@ public synchronized List<TermuxSession> getTermuxSessions() {
896896
return mShellManager.mTermuxSessions;
897897
}
898898

899+
/**
900+
* Returns a consistent copy of the current {@link TermuxSession} list. Unlike
901+
* {@link #getTermuxSessions()}, this does not expose the live backing list, so it is safe to
902+
* iterate from any thread (e.g. to feed a UI adapter). The copy is taken under the same monitor
903+
* that guards all session mutations, so it can never observe a half-applied add/remove.
904+
*/
905+
public synchronized List<TermuxSession> getTermuxSessionsListSnapshot() {
906+
return new ArrayList<>(mShellManager.mTermuxSessions);
907+
}
908+
899909
@Nullable
900910
public synchronized TermuxSession getTermuxSession(int index) {
901911
if (index >= 0 && index < mShellManager.mTermuxSessions.size())

‎termux/termux-app/src/main/java/com/termux/app/terminal/TermuxSessionsListViewController.java‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.termux.shared.theme.NightMode;
2323
import com.termux.shared.theme.ThemeUtils;
2424
import com.termux.terminal.TerminalSession;
25+
import java.util.ArrayList;
2526
import java.util.List;
2627

2728
public class TermuxSessionsListViewController extends ArrayAdapter<TermuxSession> implements AdapterView.OnItemClickListener, AdapterView.OnItemLongClickListener {
@@ -32,10 +33,24 @@ public class TermuxSessionsListViewController extends ArrayAdapter<TermuxSession
3233
final StyleSpan italicSpan = new StyleSpan(Typeface.ITALIC);
3334

3435
public TermuxSessionsListViewController(TermuxActivity activity, List<TermuxSession> sessionList) {
35-
super(activity.getApplicationContext(), R.layout.item_terminal_sessions_list, sessionList);
36+
// Defensively copy so the adapter owns its data instead of sharing the service's live list.
37+
// The backing list is then only ever touched on the UI thread (here and in updateSessions),
38+
// so a background session add/remove can never mutate it while the ListView is laying out.
39+
super(activity.getApplicationContext(), R.layout.item_terminal_sessions_list, new ArrayList<>(sessionList));
3640
this.mActivity = activity;
3741
}
3842

43+
/**
44+
* Replace the adapter's session snapshot with a copy of {@code sessions} and refresh the
45+
* ListView in a single notification. Must be called on the UI thread.
46+
*/
47+
public void updateSessions(@NonNull List<TermuxSession> sessions) {
48+
setNotifyOnChange(false);
49+
clear();
50+
addAll(sessions);
51+
notifyDataSetChanged();
52+
}
53+
3954
@SuppressLint("SetTextI18n")
4055
@NonNull
4156
@Override

0 commit comments

Comments
 (0)