v1
This commit is contained in:
+2
-2
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId "haus.nightmare.a3270"
|
applicationId "haus.nightmare.a3270"
|
||||||
minSdk 24
|
minSdk 24
|
||||||
targetSdk 34
|
targetSdk 34
|
||||||
versionCode 4
|
versionCode 5
|
||||||
versionName "0.1.3"
|
versionName "1.0.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||||
val shift = event.isShiftPressed || (event.metaState and KeyEvent.META_SHIFT_ON != 0)
|
val shift = event.isShiftPressed || (event.metaState and KeyEvent.META_SHIFT_ON != 0)
|
||||||
if (isShiftPressedState.value != shift) {
|
if (isShiftPressedState.value != shift) {
|
||||||
@@ -506,8 +507,8 @@ fun MainScreen(
|
|||||||
viewModel.selectLightPen(addr)
|
viewModel.selectLightPen(addr)
|
||||||
terminalInputViewRef?.showSoftKeyboard()
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
},
|
},
|
||||||
onGraphicTouch = { px, py ->
|
onGraphicTouch = { clickAddr, px, py ->
|
||||||
viewModel.handleGraphicTouch(px, py)
|
viewModel.handleGraphicTouch(clickAddr, px, py)
|
||||||
},
|
},
|
||||||
onPasteText = { text ->
|
onPasteText = { text ->
|
||||||
viewModel.pasteString(text)
|
viewModel.pasteString(text)
|
||||||
|
|||||||
@@ -330,14 +330,14 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
val curRow = curAddr / cols
|
val curRow = curAddr / cols
|
||||||
val curCol = curAddr % cols
|
val curCol = curAddr % cols
|
||||||
|
|
||||||
var startRow = curRow
|
val startRow: Int
|
||||||
var startCol = curCol
|
val startCol: Int
|
||||||
if (forward) {
|
if (forward) {
|
||||||
startCol = (curCol + 1) % cols
|
startCol = (curCol + 1) % cols
|
||||||
if (startCol == 0) startRow = (curRow + 1) % rows
|
startRow = if (startCol == 0) (curRow + 1) % rows else curRow
|
||||||
} else {
|
} else {
|
||||||
startCol = (curCol - 1 + cols) % cols
|
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
|
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 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 c = client ?: return
|
||||||
val goca = c.gocaDecoder ?: return
|
val goca = c.gocaDecoder ?: return
|
||||||
|
if (clickAddr >= 0) {
|
||||||
|
c.screenBuffer?.cursorAddress = clickAddr
|
||||||
|
_cursorAddress.value = clickAddr
|
||||||
|
}
|
||||||
if (goca.isGraphicsCursorActive) {
|
if (goca.isGraphicsCursorActive) {
|
||||||
goca.setGraphicCursorFromPixel(px, py)
|
goca.setGraphicCursorFromPixel(px, py)
|
||||||
c.inputProcessor.sendGraphicMouseAid(
|
c.inputProcessor.sendGraphicMouseAid(
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ fun OiaStatusBar(
|
|||||||
isLightPenMode: Boolean = false,
|
isLightPenMode: Boolean = false,
|
||||||
modifier: Modifier = Modifier
|
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 col = if (cols > 0) (cursorAddr % cols) + 1 else 1
|
||||||
val posStr = String.format("%02d/%02d", row, col)
|
val posStr = String.format("%02d/%02d", row, col)
|
||||||
|
|
||||||
|
|||||||
@@ -416,7 +416,31 @@ private fun generateBitmap(
|
|||||||
currentProt = (faVal and FA_PROTECT) != 0
|
currentProt = (faVal and FA_PROTECT) != 0
|
||||||
currentHi = (faVal and FA_INT_HIGH_SEL) == FA_INT_HIGH_SEL
|
currentHi = (faVal and FA_INT_HIGH_SEL) == FA_INT_HIGH_SEL
|
||||||
currentHidden = (faVal and FA_INT_HIGH_SEL) == 0
|
currentHidden = (faVal and FA_INT_HIGH_SEL) == 0
|
||||||
} else if (!currentHidden || !maskHidden) {
|
} 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
|
val ch = cell.ucs4
|
||||||
if (ch in ' '..'~' || ch > '\u007F') {
|
if (ch in ' '..'~' || ch > '\u007F') {
|
||||||
textPaint.color = when {
|
textPaint.color = when {
|
||||||
@@ -429,6 +453,7 @@ private fun generateBitmap(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Overlay vector graphics plane if present
|
// Overlay vector graphics plane if present
|
||||||
if (graphicsPlane != null && graphicsPlane.hasContent()) {
|
if (graphicsPlane != null && graphicsPlane.hasContent()) {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ fun TerminalView(
|
|||||||
searchHighlightLen: Int = 0,
|
searchHighlightLen: Int = 0,
|
||||||
onTapAddress: (Int) -> Unit,
|
onTapAddress: (Int) -> Unit,
|
||||||
onLightPenSelect: ((Int) -> Unit)? = null,
|
onLightPenSelect: ((Int) -> Unit)? = null,
|
||||||
onGraphicTouch: ((Int, Int) -> Unit)? = null,
|
onGraphicTouch: ((Int, Int, Int) -> Unit)? = null,
|
||||||
onPasteText: (String) -> Unit = {},
|
onPasteText: (String) -> Unit = {},
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
@@ -162,7 +162,8 @@ fun TerminalView(
|
|||||||
val gHeight = graphicsPlane?.canvasHeight ?: gridH.toInt()
|
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 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()
|
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) {
|
} else if (isLightPenMode) {
|
||||||
onLightPenSelect?.invoke(clickAddr)
|
onLightPenSelect?.invoke(clickAddr)
|
||||||
} else {
|
} else {
|
||||||
@@ -351,7 +352,7 @@ fun TerminalView(
|
|||||||
val symHeight = slot.height
|
val symHeight = slot.height
|
||||||
val rgbArray = slot.getRgbPixels(fgColor.toArgb(), bgColor.toArgb())
|
val rgbArray = slot.getRgbPixels(fgColor.toArgb(), bgColor.toArgb())
|
||||||
if (rgbArray != null && symWidth > 0 && symHeight > 0) {
|
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(
|
drawContext.canvas.nativeCanvas.drawBitmap(
|
||||||
bmp,
|
bmp,
|
||||||
null,
|
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