From 3b35bd7b669c0f2827a5ff919b02129be31bd82f Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Mon, 9 Nov 2026 15:56:27 -0500 Subject: [PATCH] fix(dual-screen): the bottom-screen terminal keeps its shell when another app covers that screen --- .../launcher/session/SessionTerminal.kt | 31 +++++++++- .../launcher/ui/SecondScreenPresentation.kt | 2 +- .../droiddeck/launcher/ui/SessionPanels.kt | 14 +++-- .../session/SessionTerminalKeepTest.kt | 58 +++++++++++++++++++ 4 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 app/src/test/java/com/droiddeck/launcher/session/SessionTerminalKeepTest.kt diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionTerminal.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionTerminal.kt index c7ecba3bb..9458d2ba4 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionTerminal.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionTerminal.kt @@ -14,6 +14,12 @@ object SessionTerminal { @Volatile private var launch: Launch? = null + private class Kept(val session: TerminalSession, val generation: Int) + + // The bottom screen's presentation is dismissed whenever another app covers that display + // (a browser opened for a sign-in), so its shell is kept for the next panel. + @Volatile private var kept: Kept? = null + fun prepare( command: List, hostEnvironment: Array, @@ -32,8 +38,31 @@ object SessionTerminal { ).also { it.mSessionName = "DroidDeck Linux shell" } } + fun reuse(client: TerminalSessionClient): TerminalSession? { + val current = kept ?: return null + if (current.generation != launch?.generation || !current.session.isRunning) { + kept = null + return null + } + current.session.updateTerminalSessionClient(client) + return current.session + } + + fun keep(session: TerminalSession) { + val generation = launch?.generation ?: return + kept = Kept(session, generation) + } + + fun release(session: TerminalSession?) { + if (session != null && kept?.session === session) kept = null + } + fun clear(generation: Int? = null) { val current = launch ?: return - if (generation == null || current.generation == generation) launch = null + if (generation == null || current.generation == generation) { + launch = null + kept?.session?.takeIf { it.pid > 0 }?.finishIfRunning() + kept = null + } } } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SecondScreenPresentation.kt b/app/src/main/java/com/droiddeck/launcher/ui/SecondScreenPresentation.kt index d9a4967d7..554e3e602 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SecondScreenPresentation.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SecondScreenPresentation.kt @@ -364,7 +364,7 @@ class SecondScreenPresentation( private fun renderTerminal() { bar(SecondScreenMode.TERMINAL) - val panel = SessionTerminalPanel(context) + val panel = SessionTerminalPanel(context, keepShell = true) terminalPanel = panel root.addView(panel, weightParams()) } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SessionPanels.kt b/app/src/main/java/com/droiddeck/launcher/ui/SessionPanels.kt index 35d5538bf..50045d6c0 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SessionPanels.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SessionPanels.kt @@ -21,7 +21,7 @@ import com.termux.terminal.TerminalSessionClient import com.termux.view.TerminalView import com.termux.view.TerminalViewClient -class SessionTerminalPanel(context: Context) : LinearLayout(context) { +class SessionTerminalPanel(context: Context, private val keepShell: Boolean = false) : LinearLayout(context) { private var terminalSession: TerminalSession? = null private var terminalView: TerminalView? = null private var terminalPid = -1 @@ -45,7 +45,8 @@ class SessionTerminalPanel(context: Context) : LinearLayout(context) { isFocusableInTouchMode = true } terminalView = view - val session = SessionTerminal.create(terminalClient) + val session = (if (keepShell) SessionTerminal.reuse(terminalClient) else null) + ?: SessionTerminal.create(terminalClient)?.also { if (keepShell) SessionTerminal.keep(it) } if (session == null) { addView(TextView(context).apply { text = context.getString(R.string.second_terminal_not_ready) @@ -121,8 +122,12 @@ class SessionTerminalPanel(context: Context) : LinearLayout(context) { (context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager) .hideSoftInputFromWindow(view.windowToken, 0) } - val pid = terminalPid.takeIf { it > 1 } ?: (terminalSession?.pid ?: -1) - if (pid > 1) SessionService.stopTerminalProcess(context, pid) + val session = terminalSession + if (!keepShell || session?.isRunning != true) { + val pid = terminalPid.takeIf { it > 1 } ?: (session?.pid ?: -1) + if (pid > 1) SessionService.stopTerminalProcess(context, pid) + SessionTerminal.release(session) + } terminalPid = -1 terminalSession = null terminalView = null @@ -140,6 +145,7 @@ class SessionTerminalPanel(context: Context) : LinearLayout(context) { override fun onSessionFinished(finishedSession: TerminalSession) { val pid = terminalPid.takeIf { it > 1 } ?: finishedSession.pid if (pid > 1) SessionService.terminalProcessExited(context, pid) + SessionTerminal.release(finishedSession) terminalPid = -1 terminalView?.onScreenUpdated() } diff --git a/app/src/test/java/com/droiddeck/launcher/session/SessionTerminalKeepTest.kt b/app/src/test/java/com/droiddeck/launcher/session/SessionTerminalKeepTest.kt new file mode 100644 index 000000000..6b349f906 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/session/SessionTerminalKeepTest.kt @@ -0,0 +1,58 @@ +package com.droiddeck.launcher.session + +import com.termux.terminal.TerminalSession +import com.termux.terminal.TerminalSessionClient +import org.junit.After +import org.junit.Assert.assertNull +import org.junit.Assert.assertSame +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import java.io.File +import java.lang.reflect.Proxy + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [28], manifest = Config.NONE) +class SessionTerminalKeepTest { + private val client = Proxy.newProxyInstance( + TerminalSessionClient::class.java.classLoader, arrayOf(TerminalSessionClient::class.java), + ) { _, method, _ -> if (method.returnType == Int::class.javaPrimitiveType) 0 else null } as TerminalSessionClient + + private fun prepare(generation: Int) = + SessionTerminal.prepare(listOf("/bin/sh"), emptyArray(), File("/"), generation) + + private fun unstarted() = TerminalSession("/bin/sh", "/", arrayOf("/bin/sh"), emptyArray(), 3000, client) + + @After fun reset() = SessionTerminal.clear() + + @Test fun aKeptShellIsReusedByTheNextPanel() { + prepare(1) + val shell = unstarted() + SessionTerminal.keep(shell) + assertSame(shell, SessionTerminal.reuse(client)) + } + + @Test fun aReleasedShellIsNotReused() { + prepare(1) + val shell = unstarted() + SessionTerminal.keep(shell) + SessionTerminal.release(shell) + assertNull(SessionTerminal.reuse(client)) + } + + @Test fun aShellFromAnEarlierSessionIsNotReused() { + prepare(1) + SessionTerminal.keep(unstarted()) + prepare(2) + assertNull(SessionTerminal.reuse(client)) + } + + @Test fun clearingTheSessionDropsTheKeptShell() { + prepare(1) + SessionTerminal.keep(unstarted()) + SessionTerminal.clear(1) + prepare(1) + assertNull(SessionTerminal.reuse(client)) + } +}