fix(dual-screen): the bottom-screen terminal keeps its shell when another app covers that screen

This commit is contained in:
Kurt Himebauch committed 2026-11-09 15:56:27 -05:00
1 parent 75a0230304
commit 3b35bd7b66
4 files changed
+99 -6

No files matched your search

@@ -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<String>,
hostEnvironment: Array<String>,
@@ -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
}
}
}
@@ -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())
}
@@ -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()
}
@@ -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))
}
}