This commit is contained in:
+2
-2
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
+2
-1
@@ -1,2 +1,3 @@
|
||||
android.useAndroidX=true
|
||||
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
||||
org.gradle.jvmargs=-Xmx2048m -XX:+IgnoreUnrecognizedVMOptions -XX:MaxMetaspaceSize=512m
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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<TerminalInputAction>(Channel.UNLIMITED)
|
||||
|
||||
private val _connectionState = MutableStateFlow(ConnectionState.NOT_CONNECTED)
|
||||
val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
|
||||
|
||||
@@ -62,13 +81,6 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
||||
private val _cursorBlink = MutableStateFlow(AppSettings.isCursorBlinkEnabled(application))
|
||||
val cursorBlink: StateFlow<Boolean> = _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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user