Debugging v0.2 features from j3270
Build and Test a3270 / Build Android APK (push) Successful in 4m37s
Release a3270 / Build & Publish Release (push) Successful in 6m53s

This commit is contained in:
2026-08-21 12:52:29 -04:00
parent 9bf91f5c49
commit 17f8cae925
8 changed files with 164 additions and 55 deletions
@@ -36,6 +36,35 @@ import org.pubvm.a3270.ui.UntrustedCertificateDialog
class MainActivity : ComponentActivity() {
companion object {
init {
val rootLogger = java.util.logging.Logger.getLogger("")
rootLogger.level = java.util.logging.Level.ALL
for (h in rootLogger.handlers) {
rootLogger.removeHandler(h)
}
rootLogger.addHandler(object : java.util.logging.Handler() {
override fun publish(record: java.util.logging.LogRecord?) {
if (record == null) return
val msg = record.message ?: ""
val tag = "a3270-" + (record.loggerName?.substringAfterLast('.') ?: "lib3270j")
when {
record.level.intValue() >= java.util.logging.Level.SEVERE.intValue() ->
android.util.Log.e(tag, msg, record.thrown)
record.level.intValue() >= java.util.logging.Level.WARNING.intValue() ->
android.util.Log.w(tag, msg, record.thrown)
record.level.intValue() >= java.util.logging.Level.INFO.intValue() ->
android.util.Log.i(tag, msg, record.thrown)
else ->
android.util.Log.d(tag, msg, record.thrown)
}
}
override fun flush() {}
override fun close() {}
})
}
}
private val viewModel: TerminalViewModel by viewModels()
private val isShiftPressedState = mutableStateOf(false)
@@ -221,6 +250,7 @@ fun MainScreen(
cursorAddr = cursorAddr,
screenVersion = screenVersion,
programSymbolManager = viewModel.getClient()?.programSymbolManager,
graphicsPlane = viewModel.getClient()?.graphicsPlane,
maskHiddenFields = maskHiddenInput,
blinkCursor = cursorBlink,
onTapAddress = { addr ->
@@ -221,10 +221,14 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
}
fun resolveUntrustedCert(accept: Boolean) {
val prompt = _untrustedCertPrompt.value
if (prompt != null) {
prompt.deferred.complete(accept)
_untrustedCertPrompt.value = null
try {
val prompt = _untrustedCertPrompt.value
if (prompt != null) {
prompt.deferred.complete(accept)
_untrustedCertPrompt.value = null
}
} catch (t: Throwable) {
log.warning("Error resolving untrusted cert: ${t.message}")
}
}
@@ -251,7 +255,6 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
tlsVerifyCert: Boolean = true,
graphicsModeStr: String = "BOTH"
) {
if (_connectionState.value.isConnected()) return
currentHost = host
activeHostType = hostType
lastScreenContentHash = 0
@@ -261,6 +264,15 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
viewModelScope.launch(Dispatchers.IO) {
try {
// Always disconnect any existing client before establishing a new connection
if (client != null || _connectionState.value.isConnected()) {
try {
client?.disconnect()
} catch (_: Exception) {}
client = null
_connectionState.value = ConnectionState.NOT_CONNECTED
}
val model = when (modelNum) {
3 -> TerminalModel.IBM_3279_3
4 -> TerminalModel.IBM_3279_4
@@ -303,8 +315,14 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
exception = exception,
deferred = deferred
)
val accepted = kotlinx.coroutines.runBlocking { deferred.await() }
_untrustedCertPrompt.value = null
val accepted = try {
kotlinx.coroutines.runBlocking { deferred.await() }
} catch (t: Throwable) {
log.warning("Certificate verifier prompt interrupted or failed: ${t.message}")
false
} finally {
_untrustedCertPrompt.value = null
}
accepted
}
@@ -379,10 +397,10 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
_screenBuffer.value = newClient.screenBuffer
newClient.connect()
} catch (e: Exception) {
} catch (e: Throwable) {
log.severe("Failed to connect: ${e.message}")
_connectionState.value = ConnectionState.NOT_CONNECTED
_oiaText.value = "Failed: ${e.localizedMessage ?: e.message}"
_oiaText.value = "Failed: ${e.localizedMessage ?: e.message ?: "Connection error"}"
}
}
}
@@ -69,7 +69,13 @@ object HostStorage {
val jsonArray = JSONArray(jsonStr)
for (i in 0 until jsonArray.length()) {
val obj = jsonArray.getJSONObject(i)
list.add(SavedHost.fromJson(obj))
val saved = SavedHost.fromJson(obj)
val effective = if (saved.graphicsMode == "PROGRAMMED_SYMBOLS") {
saved.copy(graphicsMode = "BOTH")
} else {
saved
}
list.add(effective)
}
} catch (e: Exception) {
e.printStackTrace()
@@ -298,9 +298,9 @@ fun ConnectDialog(
) {
Text("Graphics:", fontSize = 11.sp, color = Color.Gray)
listOf(
"BOTH" to "Both",
"VECTOR_GRAPHICS" to "Vector",
"PROGRAMMED_SYMBOLS" to "Symbols",
"VECTOR_GRAPHICS" to "Vector",
"BOTH" to "Both",
"NONE" to "None"
).forEach { (modeKey, modeLabel) ->
FilterChip(
@@ -59,6 +59,7 @@ fun TerminalView(
cursorAddr: Int,
screenVersion: Long,
programSymbolManager: org.lib3270j.graphics.ProgramSymbolManager? = null,
graphicsPlane: org.lib3270j.graphics.GraphicsPlane? = null,
maskHiddenFields: Boolean = true,
blinkCursor: Boolean = true,
onTapAddress: (Int) -> Unit,
@@ -304,28 +305,8 @@ fun TerminalView(
if (slot != null) {
val symWidth = slot.width
val symHeight = slot.height
val pixelData = slot.pixelData
if (pixelData != null && symWidth > 0 && symHeight > 0) {
val fgArgb = fgColor.toArgb()
val bgArgb = bgColor.toArgb()
val isTriple = slot.isTriplePlane
val rgbArray = IntArray(symWidth * symHeight)
for (py in 0 until symHeight) {
for (px in 0 until symWidth) {
val pIdx = py * symWidth + px
val v = if (pIdx < pixelData.size) (pixelData[pIdx].toInt() and 0xFF) else 0
if (v == 0) {
rgbArray[pIdx] = bgArgb
} else if (!isTriple) {
rgbArray[pIdx] = fgArgb
} else {
val redVal = if ((v and 0x01) != 0) 255 else 0
val greenVal = if ((v and 0x02) != 0) 255 else 0
val blueVal = if ((v and 0x04) != 0) 255 else 0
rgbArray[pIdx] = (0xFF shl 24) or (redVal shl 16) or (greenVal shl 8) or blueVal
}
}
}
val rgbArray = slot.getRgbPixels(fgColor.toArgb(), bgColor.toArgb())
if (rgbArray != null && symWidth > 0 && symHeight > 0) {
val bmp = android.graphics.Bitmap.createBitmap(rgbArray, symWidth, symHeight, android.graphics.Bitmap.Config.ARGB_8888)
drawContext.canvas.nativeCanvas.drawBitmap(
bmp,
@@ -362,6 +343,25 @@ fun TerminalView(
}
}
}
// Draw Vector Graphics Plane overlay if present
if (graphicsPlane != null && graphicsPlane.hasContent()) {
val gridW = (cols * cellWidth).toInt()
val gridH = (rows * cellHeight).toInt()
if (gridW > 0 && gridH > 0) {
graphicsPlane.resize(gridW, gridH)
val rgb = graphicsPlane.rgbBuffer
if (rgb != null) {
val bmp = android.graphics.Bitmap.createBitmap(rgb, gridW, gridH, android.graphics.Bitmap.Config.ARGB_8888)
drawContext.canvas.nativeCanvas.drawBitmap(
bmp,
null,
android.graphics.RectF(0f, 0f, gridW.toFloat(), gridH.toFloat()),
null
)
}
}
}
}
// Context Menu Popup
@@ -42,30 +42,54 @@ fun UntrustedCertificateDialog(
append("\n\n")
}
if (cert != null) {
append("Subject:\n ")
append(cert.subjectX500Principal.name)
append("\n\n")
try {
append("Subject:\n ")
append(cert.subjectX500Principal?.name ?: cert.subjectDN?.name ?: "Unknown")
append("\n\n")
} catch (t: Throwable) {
append("Subject: ${t.message ?: "Unavailable"}\n\n")
}
append("Issuer:\n ")
append(cert.issuerX500Principal.name)
append("\n\n")
try {
append("Issuer:\n ")
append(cert.issuerX500Principal?.name ?: cert.issuerDN?.name ?: "Unknown")
append("\n\n")
} catch (t: Throwable) {
append("Issuer: ${t.message ?: "Unavailable"}\n\n")
}
append("Validity:\n From: ")
append(sdf.format(cert.notBefore))
append("\n To: ")
append(sdf.format(cert.notAfter))
append("\n\n")
try {
append("Validity:\n From: ")
append(cert.notBefore?.let { sdf.format(it) } ?: "Unknown")
append("\n To: ")
append(cert.notAfter?.let { sdf.format(it) } ?: "Unknown")
append("\n\n")
} catch (t: Throwable) {
append("Validity: ${t.message ?: "Unavailable"}\n\n")
}
append("Serial Number:\n ")
append(cert.serialNumber.toString(16).uppercase(Locale.US))
append("\n\n")
try {
append("Serial Number:\n ")
append(cert.serialNumber?.toString(16)?.uppercase(Locale.US) ?: "Unknown")
append("\n\n")
} catch (t: Throwable) {
append("Serial Number: ${t.message ?: "Unavailable"}\n\n")
}
append("SHA-256 Fingerprint:\n ")
append(computeFingerprint(cert, "SHA-256"))
append("\n\n")
try {
append("SHA-256 Fingerprint:\n ")
append(computeFingerprint(cert, "SHA-256"))
append("\n\n")
} catch (t: Throwable) {
append("SHA-256 Fingerprint: ${t.message ?: "Unavailable"}\n\n")
}
append("SHA-1 Fingerprint:\n ")
append(computeFingerprint(cert, "SHA-1"))
try {
append("SHA-1 Fingerprint:\n ")
append(computeFingerprint(cert, "SHA-1"))
} catch (t: Throwable) {
append("SHA-1 Fingerprint: ${t.message ?: "Unavailable"}")
}
} else {
append("No peer certificate information available.")
}
@@ -161,8 +185,8 @@ private fun computeFingerprint(cert: X509Certificate, algorithm: String): String
return try {
val md = MessageDigest.getInstance(algorithm)
val digest = md.digest(cert.encoded)
digest.joinToString(":") { "%02X".format(it) }
} catch (e: Exception) {
digest.joinToString(":") { "%02X".format(it.toInt() and 0xFF) }
} catch (e: Throwable) {
"Unable to compute fingerprint: ${e.message}"
}
}