v1
This commit is contained in:
+2
-2
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "haus.nightmare.a3270"
|
||||
minSdk 24
|
||||
targetSdk 34
|
||||
versionCode 4
|
||||
versionName "0.1.3"
|
||||
versionCode 5
|
||||
versionName "1.0.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
@@ -149,6 +149,7 @@ class MainActivity : ComponentActivity() {
|
||||
return 0
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||
val shift = event.isShiftPressed || (event.metaState and KeyEvent.META_SHIFT_ON != 0)
|
||||
if (isShiftPressedState.value != shift) {
|
||||
@@ -506,8 +507,8 @@ fun MainScreen(
|
||||
viewModel.selectLightPen(addr)
|
||||
terminalInputViewRef?.showSoftKeyboard()
|
||||
},
|
||||
onGraphicTouch = { px, py ->
|
||||
viewModel.handleGraphicTouch(px, py)
|
||||
onGraphicTouch = { clickAddr, px, py ->
|
||||
viewModel.handleGraphicTouch(clickAddr, px, py)
|
||||
},
|
||||
onPasteText = { text ->
|
||||
viewModel.pasteString(text)
|
||||
|
||||
@@ -330,14 +330,14 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
||||
val curRow = curAddr / cols
|
||||
val curCol = curAddr % cols
|
||||
|
||||
var startRow = curRow
|
||||
var startCol = curCol
|
||||
val startRow: Int
|
||||
val startCol: Int
|
||||
if (forward) {
|
||||
startCol = (curCol + 1) % cols
|
||||
if (startCol == 0) startRow = (curRow + 1) % rows
|
||||
startRow = if (startCol == 0) (curRow + 1) % rows else curRow
|
||||
} else {
|
||||
startCol = (curCol - 1 + cols) % cols
|
||||
if (startCol == cols - 1) startRow = (curRow - 1 + rows) % rows
|
||||
startRow = if (startCol == cols - 1) (curRow - 1 + rows) % rows else curRow
|
||||
}
|
||||
|
||||
val dir = if (forward) haus.nightmare.lib3270j.ecl.ECLConstants.SEARCH_FORWARD else haus.nightmare.lib3270j.ecl.ECLConstants.SEARCH_BACKWARD
|
||||
@@ -752,9 +752,13 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
||||
|
||||
fun getGocaDecoder(): haus.nightmare.lib3270j.graphics.GocaDecoder? = client?.gocaDecoder
|
||||
|
||||
fun handleGraphicTouch(px: Int, py: Int, button: Int = 1, shift: Boolean = false, ctrl: Boolean = false) {
|
||||
fun handleGraphicTouch(clickAddr: Int = -1, px: Int, py: Int, button: Int = 1, shift: Boolean = false, ctrl: Boolean = false) {
|
||||
val c = client ?: return
|
||||
val goca = c.gocaDecoder ?: return
|
||||
if (clickAddr >= 0) {
|
||||
c.screenBuffer?.cursorAddress = clickAddr
|
||||
_cursorAddress.value = clickAddr
|
||||
}
|
||||
if (goca.isGraphicsCursorActive) {
|
||||
goca.setGraphicCursorFromPixel(px, py)
|
||||
c.inputProcessor.sendGraphicMouseAid(
|
||||
|
||||
@@ -31,7 +31,7 @@ fun OiaStatusBar(
|
||||
isLightPenMode: Boolean = false,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val row = if (cols > 0) (cursorAddr / cols) + 1 else 1
|
||||
val row = if (cols > 0 && rows > 0) ((cursorAddr / cols) % rows) + 1 else 1
|
||||
val col = if (cols > 0) (cursorAddr % cols) + 1 else 1
|
||||
val posStr = String.format("%02d/%02d", row, col)
|
||||
|
||||
|
||||
@@ -416,15 +416,40 @@ private fun generateBitmap(
|
||||
currentProt = (faVal and FA_PROTECT) != 0
|
||||
currentHi = (faVal and FA_INT_HIGH_SEL) == FA_INT_HIGH_SEL
|
||||
currentHidden = (faVal and FA_INT_HIGH_SEL) == 0
|
||||
} else if (!currentHidden || !maskHidden) {
|
||||
val ch = cell.ucs4
|
||||
if (ch in ' '..'~' || ch > '\u007F') {
|
||||
textPaint.color = when {
|
||||
currentHi -> android.graphics.Color.WHITE
|
||||
currentProt -> android.graphics.Color.parseColor("#3399FF")
|
||||
else -> android.graphics.Color.parseColor("#00FF66")
|
||||
} else {
|
||||
val csVal = cell.cs.toInt() and 0xFF
|
||||
val ecVal = cell.ec.toInt() and 0xFF
|
||||
var drawnAsPs = false
|
||||
if (csVal >= 0x40 && programSymbolManager != null) {
|
||||
val slot = programSymbolManager.getSymbol(csVal, ecVal)
|
||||
if (slot != null) {
|
||||
val symWidth = slot.width
|
||||
val symHeight = slot.height
|
||||
val fgArgb = when {
|
||||
currentHi -> android.graphics.Color.WHITE
|
||||
currentProt -> android.graphics.Color.parseColor("#3399FF")
|
||||
else -> android.graphics.Color.parseColor("#00FF66")
|
||||
}
|
||||
val bgArgb = android.graphics.Color.parseColor("#0A0A0A")
|
||||
val rgbArray = slot.getRgbPixels(fgArgb, bgArgb)
|
||||
if (rgbArray != null && symWidth > 0 && symHeight > 0) {
|
||||
val symBmp = Bitmap.createBitmap(rgbArray, symWidth, symHeight, Bitmap.Config.ARGB_8888)
|
||||
canvas.drawBitmap(symBmp, null, android.graphics.RectF(left, top, left + charWidth, top + charHeight), null)
|
||||
drawnAsPs = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!drawnAsPs && (!currentHidden || !maskHidden)) {
|
||||
val ch = cell.ucs4
|
||||
if (ch in ' '..'~' || ch > '\u007F') {
|
||||
textPaint.color = when {
|
||||
currentHi -> android.graphics.Color.WHITE
|
||||
currentProt -> android.graphics.Color.parseColor("#3399FF")
|
||||
else -> android.graphics.Color.parseColor("#00FF66")
|
||||
}
|
||||
canvas.drawText(ch.toString(), left + 2f, top + 18f, textPaint)
|
||||
}
|
||||
canvas.drawText(ch.toString(), left + 2f, top + 18f, textPaint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ fun TerminalView(
|
||||
searchHighlightLen: Int = 0,
|
||||
onTapAddress: (Int) -> Unit,
|
||||
onLightPenSelect: ((Int) -> Unit)? = null,
|
||||
onGraphicTouch: ((Int, Int) -> Unit)? = null,
|
||||
onGraphicTouch: ((Int, Int, Int) -> Unit)? = null,
|
||||
onPasteText: (String) -> Unit = {},
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
@@ -162,7 +162,8 @@ fun TerminalView(
|
||||
val gHeight = graphicsPlane?.canvasHeight ?: gridH.toInt()
|
||||
val px = if (gridW > 0 && gWidth > 0) (((startPos.x - metrics.offsetX) * gWidth) / gridW).toInt().coerceIn(0, gWidth - 1) else (startPos.x - metrics.offsetX).toInt()
|
||||
val py = if (gridH > 0 && gHeight > 0) (((startPos.y - metrics.offsetY) * gHeight) / gridH).toInt().coerceIn(0, gHeight - 1) else (startPos.y - metrics.offsetY).toInt()
|
||||
onGraphicTouch?.invoke(px, py)
|
||||
screenBuffer?.cursorAddress = clickAddr
|
||||
onGraphicTouch?.invoke(clickAddr, px, py)
|
||||
} else if (isLightPenMode) {
|
||||
onLightPenSelect?.invoke(clickAddr)
|
||||
} else {
|
||||
@@ -351,7 +352,7 @@ fun TerminalView(
|
||||
val symHeight = slot.height
|
||||
val rgbArray = slot.getRgbPixels(fgColor.toArgb(), bgColor.toArgb())
|
||||
if (rgbArray != null && symWidth > 0 && symHeight > 0) {
|
||||
val bmp = android.graphics.Bitmap.createBitmap(rgbArray as IntArray, symWidth, symHeight, android.graphics.Bitmap.Config.ARGB_8888)
|
||||
val bmp = android.graphics.Bitmap.createBitmap(rgbArray, symWidth, symHeight, android.graphics.Bitmap.Config.ARGB_8888)
|
||||
drawContext.canvas.nativeCanvas.drawBitmap(
|
||||
bmp,
|
||||
null,
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
package haus.nightmare.a3270
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionConfig
|
||||
import haus.nightmare.lib3270j.ConnectionState
|
||||
import haus.nightmare.lib3270j.TerminalModel
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator
|
||||
import haus.nightmare.lib3270j.datastream.DataStreamProcessor
|
||||
import haus.nightmare.lib3270j.graphics.GocaDecoder
|
||||
import haus.nightmare.lib3270j.graphics.GraphicsPlane
|
||||
import haus.nightmare.lib3270j.input.InputProcessor
|
||||
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
||||
import haus.nightmare.lib3270j.protocol.TelnetConstants.*
|
||||
import haus.nightmare.lib3270j.protocol.TN3270EConstants.*
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer
|
||||
import haus.nightmare.lib3270j.telnet.TelnetConnection
|
||||
import haus.nightmare.lib3270j.telnet.TelnetFSM
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.ArrayList
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
class GraphicsAndSysReqTest {
|
||||
|
||||
private lateinit var screenBuffer: ScreenBuffer
|
||||
private lateinit var translator: EbcdicTranslator
|
||||
private lateinit var graphicsPlane: GraphicsPlane
|
||||
private lateinit var gocaDecoder: GocaDecoder
|
||||
|
||||
private class MockConnection(config: ConnectionConfig, fsm: TelnetFSM) : TelnetConnection(config, fsm) {
|
||||
val sentData: MutableList<ByteArray> = ArrayList()
|
||||
|
||||
override fun sendRaw(data: ByteArray) {
|
||||
sentData.add(data.clone())
|
||||
}
|
||||
|
||||
override fun sendRaw(data: ByteArray, offset: Int, length: Int) {
|
||||
val b = ByteArray(length)
|
||||
System.arraycopy(data, offset, b, 0, length)
|
||||
sentData.add(b)
|
||||
}
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
val model = TerminalModel.IBM_3279_2 // 24x80
|
||||
translator = EbcdicTranslator()
|
||||
screenBuffer = ScreenBuffer(model, translator)
|
||||
graphicsPlane = GraphicsPlane(800, 600)
|
||||
gocaDecoder = GocaDecoder(graphicsPlane)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testScreenBufferSetCursorPosition() {
|
||||
screenBuffer.setCursorPosition(5, 10)
|
||||
assertEquals(5 * 80 + 10, screenBuffer.cursorAddress)
|
||||
assertEquals(5, screenBuffer.cursorRow)
|
||||
assertEquals(10, screenBuffer.cursorCol)
|
||||
|
||||
// Clamping check
|
||||
screenBuffer.setCursorPosition(-5, -10)
|
||||
assertEquals(0, screenBuffer.cursorAddress)
|
||||
|
||||
screenBuffer.setCursorPosition(100, 200)
|
||||
assertEquals(23 * 80 + 79, screenBuffer.cursorAddress)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGraphicsCursorFramingOnSendAid() {
|
||||
screenBuffer.erase(false)
|
||||
screenBuffer.setCellFA(0, (FA_PRINTABLE or FA_MODIFY).toByte())
|
||||
screenBuffer.getCell(1).ec = 0xC1.toByte() // 'A'
|
||||
screenBuffer.setCellFA(5, (FA_PRINTABLE or FA_PROTECT).toByte())
|
||||
screenBuffer.cursorAddress = 2
|
||||
|
||||
gocaDecoder.isGraphicsCursorActive = true
|
||||
gocaDecoder.setGraphicCursorPosition(200, -100)
|
||||
|
||||
val sentRef = AtomicReference<ByteArray>()
|
||||
val inputProcessor = object : InputProcessor(screenBuffer, translator, null) {
|
||||
override fun sendAidResponse(data: ByteArray) {
|
||||
sentRef.set(data)
|
||||
}
|
||||
}
|
||||
inputProcessor.gocaDecoder = gocaDecoder
|
||||
|
||||
inputProcessor.sendAid(AID_ENTER)
|
||||
|
||||
val result = sentRef.get()
|
||||
assertNotNull(result)
|
||||
// Expected: AID_SF (0x88) + SF(56 bytes) + AID_ENTER(0x7D) + CursorAddr(2) + SBA(1) + FieldAddr(2) + Data 'A'(1) = 64 bytes
|
||||
assertEquals(64, result.size)
|
||||
assertEquals(AID_SF.toByte(), result[0])
|
||||
assertEquals(0x00.toByte(), result[1])
|
||||
assertEquals(0x34.toByte(), result[2]) // SF len 52
|
||||
assertEquals(0x0F.toByte(), result[3])
|
||||
assertEquals(0x0F.toByte(), result[4])
|
||||
|
||||
// Coordinates at index 25, 27
|
||||
val gx = ((result[25].toInt() and 0xFF) shl 8) or (result[26].toInt() and 0xFF)
|
||||
val gy = ((result[27].toInt() and 0xFF) shl 8) or (result[28].toInt() and 0xFF)
|
||||
assertEquals(200, gx.toShort().toInt())
|
||||
assertEquals(-100, gy.toShort().toInt())
|
||||
|
||||
// Trailing AID at index 57
|
||||
assertEquals(AID_ENTER.toByte(), result[57])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSysReqOutOfBandRoutingAndStateToggle() {
|
||||
val config = ConnectionConfig("127.0.0.1", 23, TerminalModel.IBM_3279_2).apply {
|
||||
isTn3270eEnabled = true
|
||||
}
|
||||
val dsProcessor = DataStreamProcessor(screenBuffer, translator)
|
||||
val fsm = TelnetFSM(config, screenBuffer, dsProcessor)
|
||||
val inputProcessor = InputProcessor(screenBuffer, translator, fsm)
|
||||
dsProcessor.inputProcessor = inputProcessor
|
||||
val connection = MockConnection(config, fsm)
|
||||
fsm.setConnection(connection)
|
||||
fsm.onConnected()
|
||||
|
||||
// 1. Complete TN3270E negotiation
|
||||
fsm.feedBytes(byteArrayOf(IAC.toByte(), DO.toByte(), TELOPT_TN3270E.toByte()), 0, 3)
|
||||
val devTypeIs = byteArrayOf(
|
||||
IAC.toByte(), SB.toByte(), TELOPT_TN3270E.toByte(),
|
||||
0x02, 0x04, 'I'.code.toByte(), 'B'.code.toByte(), 'M'.code.toByte(), '-'.code.toByte(),
|
||||
'3'.code.toByte(), '2'.code.toByte(), '7'.code.toByte(), '9'.code.toByte(), '-'.code.toByte(),
|
||||
'2'.code.toByte(), '-'.code.toByte(), 'E'.code.toByte(),
|
||||
IAC.toByte(), SE.toByte()
|
||||
)
|
||||
fsm.feedBytes(devTypeIs, 0, devTypeIs.size)
|
||||
|
||||
val funcsReq = byteArrayOf(
|
||||
IAC.toByte(), SB.toByte(), TELOPT_TN3270E.toByte(),
|
||||
OP_FUNCTIONS.toByte(), OP_REQUEST.toByte(),
|
||||
FUNC_BIND_IMAGE.toByte(), FUNC_RESPONSES.toByte(), FUNC_SYSREQ.toByte(),
|
||||
IAC.toByte(), SE.toByte()
|
||||
)
|
||||
fsm.feedBytes(funcsReq, 0, funcsReq.size)
|
||||
|
||||
// BIND image
|
||||
val bindPacket = ByteArray(EH_SIZE + 35).apply {
|
||||
this[0] = DT_BIND_IMAGE.toByte()
|
||||
this[4] = 1
|
||||
this[EH_SIZE + 24] = 0x02
|
||||
}
|
||||
val bStream = ByteArrayOutputStream().apply {
|
||||
write(bindPacket)
|
||||
write(IAC)
|
||||
write(EOR)
|
||||
}
|
||||
val bindBytes = bStream.toByteArray()
|
||||
fsm.feedBytes(bindBytes, 0, bindBytes.size)
|
||||
|
||||
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.connectionState)
|
||||
assertTrue(fsm.isTn3270eBound)
|
||||
connection.sentData.clear()
|
||||
|
||||
// 2. Lock keyboard and send SYSREQ
|
||||
inputProcessor.isKeyboardLocked = true
|
||||
inputProcessor.sendAid(AID_SYSREQ)
|
||||
|
||||
assertFalse(inputProcessor.isKeyboardLocked)
|
||||
var foundIacAo = false
|
||||
for (pkt in connection.sentData) {
|
||||
if (pkt.size == 2 && (pkt[0].toInt() and 0xFF) == IAC && (pkt[1].toInt() and 0xFF) == AO) {
|
||||
foundIacAo = true
|
||||
}
|
||||
}
|
||||
assertTrue("Expected IAC AO out-of-band telnet command", foundIacAo)
|
||||
assertEquals(ConnectionState.CONNECTED_SSCP, fsm.connectionState)
|
||||
|
||||
// 3. Send second SYSREQ to toggle back
|
||||
inputProcessor.sendAid(AID_SYSREQ)
|
||||
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.connectionState)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user