From c767953c69ee17506fd068dc86153d5efe4ab255 Mon Sep 17 00:00:00 2001 From: Rudi Date: Thu, 20 Aug 2026 20:21:41 -0400 Subject: [PATCH] Add input buffering --- build.gradle | 4 +- build.sh | 2 +- gradle.properties | 3 +- src/main/java/org/pubvm/a3270/MainActivity.kt | 14 +- .../java/org/pubvm/a3270/TerminalViewModel.kt | 271 +++++++++--------- 5 files changed, 142 insertions(+), 152 deletions(-) diff --git a/build.gradle b/build.gradle index 204b724..4333533 100644 --- a/build.gradle +++ b/build.gradle @@ -11,8 +11,8 @@ android { applicationId "org.pubvm.a3270" minSdk 24 targetSdk 34 - versionCode 1 - versionName "0.1.0" + versionCode 2 + versionName "0.1.1" } buildTypes { diff --git a/build.sh b/build.sh index 32900fb..1289199 100755 --- a/build.sh +++ b/build.sh @@ -42,7 +42,7 @@ if [ ! -d "$J3270_PATH/lib3270j" ]; then fi echo "Building debug APK..." -./gradlew assembleDebug --no-daemon +./gradlew assembleDebug APK_PATH="$SCRIPT_DIR/build/outputs/apk/debug/a3270-debug.apk" if [ -f "$APK_PATH" ]; then diff --git a/gradle.properties b/gradle.properties index 50234ab..259fe02 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,3 @@ android.useAndroidX=true -org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m +org.gradle.jvmargs=-Xmx2048m -XX:+IgnoreUnrecognizedVMOptions -XX:MaxMetaspaceSize=512m + diff --git a/src/main/java/org/pubvm/a3270/MainActivity.kt b/src/main/java/org/pubvm/a3270/MainActivity.kt index 2ed1ae0..c7d513d 100644 --- a/src/main/java/org/pubvm/a3270/MainActivity.kt +++ b/src/main/java/org/pubvm/a3270/MainActivity.kt @@ -136,6 +136,12 @@ class MainActivity : ComponentActivity() { viewModel.tab() } return true + } else if (keyCode == KeyEvent.KEYCODE_DEL) { + viewModel.backspace() + return true + } else if (keyCode == KeyEvent.KEYCODE_ESCAPE) { + viewModel.resetKeyboard() + return true } } return super.dispatchKeyEvent(event) @@ -236,13 +242,7 @@ fun MainScreen( onValueChange = { newValue -> val text = newValue.text if (text.isNotEmpty()) { - for (ch in text) { - if (ch == '\n' || ch == '\r') { - viewModel.sendAid(AID_ENTER) - } else { - viewModel.typeChar(ch) - } - } + viewModel.typeString(text) textFieldValue = TextFieldValue("") onClearShift() } diff --git a/src/main/java/org/pubvm/a3270/TerminalViewModel.kt b/src/main/java/org/pubvm/a3270/TerminalViewModel.kt index 6ba90ad..8b64501 100644 --- a/src/main/java/org/pubvm/a3270/TerminalViewModel.kt +++ b/src/main/java/org/pubvm/a3270/TerminalViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -15,6 +16,7 @@ import org.lib3270j.Telnet3270Client import org.lib3270j.TerminalModel import org.lib3270j.listener.ConnectionListener import org.lib3270j.listener.ScreenUpdateListener +import org.lib3270j.protocol.DS3270Constants.AID_ENTER import org.lib3270j.protocol.DS3270Constants.faIsProtected import org.lib3270j.screen.ScreenBuffer import org.pubvm.a3270.service.TerminalService @@ -22,6 +24,20 @@ import org.pubvm.a3270.storage.AppSettings import java.util.concurrent.Executors import java.util.logging.Logger +sealed interface TerminalInputAction { + data class TypeText(val text: String) : TerminalInputAction + data class SendAid(val aidCode: Int) : TerminalInputAction + data object Backspace : TerminalInputAction + data object Tab : TerminalInputAction + data object BackTab : TerminalInputAction + data object CursorLeft : TerminalInputAction + data object CursorRight : TerminalInputAction + data object CursorUp : TerminalInputAction + data object CursorDown : TerminalInputAction + data object Reset : TerminalInputAction + data class SetCursor(val baddr: Int) : TerminalInputAction +} + class TerminalViewModel(application: Application) : AndroidViewModel(application) { private val log = Logger.getLogger(TerminalViewModel::class.java.name) @@ -32,6 +48,9 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application private val inputExecutor = Executors.newSingleThreadExecutor() private val inputDispatcher = inputExecutor.asCoroutineDispatcher() + // Non-blocking FIFO input channel buffer + private val inputChannel = Channel(Channel.UNLIMITED) + private val _connectionState = MutableStateFlow(ConnectionState.NOT_CONNECTED) val connectionState: StateFlow = _connectionState.asStateFlow() @@ -62,13 +81,6 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application private val _cursorBlink = MutableStateFlow(AppSettings.isCursorBlinkEnabled(application)) val cursorBlink: StateFlow = _cursorBlink.asStateFlow() - fun updateSettings(maskHidden: Boolean, blink: Boolean) { - AppSettings.setMaskHiddenInputEnabled(getApplication(), maskHidden) - AppSettings.setCursorBlinkEnabled(getApplication(), blink) - _maskHiddenInput.value = maskHidden - _cursorBlink.value = blink - } - private var client: Telnet3270Client? = null var currentHost: String = "" private set @@ -76,6 +88,98 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application private var lastScreenContentHash: Int = 0 private var hasInitialScreenLoaded: Boolean = false + init { + // Start continuous background input buffer consumer + viewModelScope.launch(inputDispatcher) { + for (action in inputChannel) { + processInputAction(action) + // Drain any additional pending actions in the buffer before updating the UI + while (true) { + val next = inputChannel.tryReceive().getOrNull() ?: break + processInputAction(next) + } + val c = client + if (c != null) { + _cursorAddress.value = c.screenBuffer.cursorAddress + } + _screenVersion.value = System.currentTimeMillis() + } + } + } + + private fun processInputAction(action: TerminalInputAction) { + val c = client ?: return + val ip = c.inputProcessor + val buf = c.screenBuffer + + try { + when (action) { + is TerminalInputAction.TypeText -> { + ip.isKeyboardLocked = false + for (ch in action.text) { + if (ch == '\n' || ch == '\r') { + ip.setKeyboardLocked(false) + ip.sendAid(AID_ENTER) + } else if (ch >= ' ') { + var curAddr = buf.cursorAddress + if (buf.isFormatted) { + val faVal = buf.getFieldAttributeAt(curAddr) + if (faIsProtected(faVal.toInt() and 0xFF) || buf.getCell(curAddr).isFieldAttribute) { + curAddr = buf.findNextUnprotected(curAddr) + buf.cursorAddress = curAddr + } + } + ip.typeCharacter(ch) + } + } + } + is TerminalInputAction.SendAid -> { + ip.setKeyboardLocked(false) + ip.sendAid(action.aidCode) + } + is TerminalInputAction.Backspace -> { + ip.isKeyboardLocked = false + ip.backspace() + } + is TerminalInputAction.Tab -> { + ip.tab() + } + is TerminalInputAction.BackTab -> { + ip.backTab() + } + is TerminalInputAction.CursorLeft -> { + ip.cursorLeft() + } + is TerminalInputAction.CursorRight -> { + ip.cursorRight() + } + is TerminalInputAction.CursorUp -> { + ip.cursorUp() + } + is TerminalInputAction.CursorDown -> { + ip.cursorDown() + } + is TerminalInputAction.Reset -> { + ip.reset() + } + is TerminalInputAction.SetCursor -> { + if (action.baddr in 0 until (buf.rows * buf.cols)) { + buf.cursorAddress = action.baddr + } + } + } + } catch (e: Exception) { + log.warning("Error processing input action: ${e.message}") + } + } + + fun updateSettings(maskHidden: Boolean, blink: Boolean) { + AppSettings.setMaskHiddenInputEnabled(getApplication(), maskHidden) + AppSettings.setCursorBlinkEnabled(getApplication(), blink) + _maskHiddenInput.value = maskHidden + _cursorBlink.value = blink + } + fun connect(host: String, port: Int = 23, modelNum: Int = 2, luName: String = "") { if (_connectionState.value.isConnected()) return currentHost = host @@ -186,175 +290,59 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application } fun typeChar(ch: Char) { - val c = client ?: return - viewModelScope.launch(inputDispatcher) { - try { - val ip = c.inputProcessor - ip.isKeyboardLocked = false // Ensure keyboard lock is cleared on user typing - val buf = c.screenBuffer - var curAddr = buf.cursorAddress - if (buf.isFormatted) { - val faVal = buf.getFieldAttributeAt(curAddr) - if (faIsProtected(faVal.toInt() and 0xFF) || buf.getCell(curAddr).isFieldAttribute) { - curAddr = buf.findNextUnprotected(curAddr) - buf.cursorAddress = curAddr - } - } - ip.typeCharacter(ch) - _cursorAddress.value = c.screenBuffer.cursorAddress - _screenVersion.value = System.currentTimeMillis() - } catch (e: Exception) { - log.warning("Error typing char: ${e.message}") - } + inputChannel.trySend(TerminalInputAction.TypeText(ch.toString())) + } + + fun typeString(text: String) { + if (text.isNotEmpty()) { + inputChannel.trySend(TerminalInputAction.TypeText(text)) } } fun pasteString(text: String) { - val c = client ?: return - viewModelScope.launch(inputDispatcher) { - try { - val ip = c.inputProcessor - ip.isKeyboardLocked = false - for (ch in text) { - if (ch == '\n' || ch == '\r') { - ip.tab() - } else if (ch >= ' ') { - ip.typeCharacter(ch) - } - } - _cursorAddress.value = c.screenBuffer.cursorAddress - _screenVersion.value = System.currentTimeMillis() - } catch (e: Exception) { - log.warning("Error pasting text: ${e.message}") - } + if (text.isNotEmpty()) { + inputChannel.trySend(TerminalInputAction.TypeText(text)) } } fun backspace() { - val c = client ?: return - viewModelScope.launch(inputDispatcher) { - try { - c.inputProcessor.isKeyboardLocked = false - c.inputProcessor.backspace() - _cursorAddress.value = c.screenBuffer.cursorAddress - _screenVersion.value = System.currentTimeMillis() - } catch (e: Exception) { - log.warning("Error backspacing: ${e.message}") - } - } + inputChannel.trySend(TerminalInputAction.Backspace) } fun tab() { - val c = client ?: return - viewModelScope.launch(inputDispatcher) { - try { - c.inputProcessor.tab() - _cursorAddress.value = c.screenBuffer.cursorAddress - _screenVersion.value = System.currentTimeMillis() - } catch (e: Exception) { - log.warning("Error tabbing: ${e.message}") - } - } + inputChannel.trySend(TerminalInputAction.Tab) } fun backTab() { - val c = client ?: return - viewModelScope.launch(inputDispatcher) { - try { - c.inputProcessor.backTab() - _cursorAddress.value = c.screenBuffer.cursorAddress - _screenVersion.value = System.currentTimeMillis() - } catch (e: Exception) { - log.warning("Error back-tabbing: ${e.message}") - } - } + inputChannel.trySend(TerminalInputAction.BackTab) } fun resetKeyboard() { - val c = client ?: return - viewModelScope.launch(inputDispatcher) { - try { - c.inputProcessor.reset() - _screenVersion.value = System.currentTimeMillis() - } catch (e: Exception) { - log.warning("Error resetting keyboard: ${e.message}") - } - } + inputChannel.trySend(TerminalInputAction.Reset) } fun sendAid(aidCode: Int) { - val c = client ?: return - viewModelScope.launch(inputDispatcher) { - try { - c.inputProcessor.setKeyboardLocked(false) - c.inputProcessor.sendAid(aidCode) - _cursorAddress.value = c.screenBuffer.cursorAddress - _screenVersion.value = System.currentTimeMillis() - } catch (e: Exception) { - log.warning("Error sending AID: ${e.message}") - } - } + inputChannel.trySend(TerminalInputAction.SendAid(aidCode)) } fun setCursor(baddr: Int) { - val buf = _screenBuffer.value ?: return - if (baddr in 0 until (buf.rows * buf.cols)) { - buf.cursorAddress = baddr - _cursorAddress.value = baddr - _screenVersion.value = System.currentTimeMillis() - } + inputChannel.trySend(TerminalInputAction.SetCursor(baddr)) } fun cursorLeft() { - val c = client ?: return - viewModelScope.launch(inputDispatcher) { - try { - c.inputProcessor.cursorLeft() - _cursorAddress.value = c.screenBuffer.cursorAddress - _screenVersion.value = System.currentTimeMillis() - } catch (e: Exception) { - log.warning("Error cursorLeft: ${e.message}") - } - } + inputChannel.trySend(TerminalInputAction.CursorLeft) } fun cursorUp() { - val c = client ?: return - viewModelScope.launch(inputDispatcher) { - try { - c.inputProcessor.cursorUp() - _cursorAddress.value = c.screenBuffer.cursorAddress - _screenVersion.value = System.currentTimeMillis() - } catch (e: Exception) { - log.warning("Error cursorUp: ${e.message}") - } - } + inputChannel.trySend(TerminalInputAction.CursorUp) } fun cursorDown() { - val c = client ?: return - viewModelScope.launch(inputDispatcher) { - try { - c.inputProcessor.cursorDown() - _cursorAddress.value = c.screenBuffer.cursorAddress - _screenVersion.value = System.currentTimeMillis() - } catch (e: Exception) { - log.warning("Error cursorDown: ${e.message}") - } - } + inputChannel.trySend(TerminalInputAction.CursorDown) } fun cursorRight() { - val c = client ?: return - viewModelScope.launch(inputDispatcher) { - try { - c.inputProcessor.cursorRight() - _cursorAddress.value = c.screenBuffer.cursorAddress - _screenVersion.value = System.currentTimeMillis() - } catch (e: Exception) { - log.warning("Error cursorRight: ${e.message}") - } - } + inputChannel.trySend(TerminalInputAction.CursorRight) } private fun extractScreenSnippet(buf: ScreenBuffer?): String { @@ -429,6 +417,7 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application override fun onCleared() { super.onCleared() client?.disconnect() + inputChannel.close() inputExecutor.shutdown() } }