Debugging v0.2 features from j3270
This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
This is free and unencumbered software released into the public domain.
|
||||||
|
|
||||||
|
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||||
|
distribute this software, either in source code form or as a compiled
|
||||||
|
binary, for any purpose, commercial or non-commercial, and by any
|
||||||
|
means.
|
||||||
|
|
||||||
|
In jurisdictions that recognize copyright laws, the author or authors
|
||||||
|
of this software dedicate any and all copyright interest in the
|
||||||
|
software to the public domain. We make this dedication for the benefit
|
||||||
|
of the public at large and to the detriment of our heirs and
|
||||||
|
successors. We intend this dedication to be an overt act of
|
||||||
|
relinquishment in perpetuity of all present and future rights to this
|
||||||
|
software under copyright law.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||||
|
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
For more information, please refer to <https://unlicense.org>
|
||||||
@@ -42,6 +42,13 @@ android {
|
|||||||
if (findProject(':lib3270j') != null) {
|
if (findProject(':lib3270j') != null) {
|
||||||
project(':lib3270j') {
|
project(':lib3270j') {
|
||||||
apply plugin: 'java-library'
|
apply plugin: 'java-library'
|
||||||
|
dependencies {
|
||||||
|
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
|
||||||
|
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||||
|
}
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
java {
|
java {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
|||||||
@@ -36,6 +36,35 @@ import org.pubvm.a3270.ui.UntrustedCertificateDialog
|
|||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
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 viewModel: TerminalViewModel by viewModels()
|
||||||
private val isShiftPressedState = mutableStateOf(false)
|
private val isShiftPressedState = mutableStateOf(false)
|
||||||
|
|
||||||
@@ -221,6 +250,7 @@ fun MainScreen(
|
|||||||
cursorAddr = cursorAddr,
|
cursorAddr = cursorAddr,
|
||||||
screenVersion = screenVersion,
|
screenVersion = screenVersion,
|
||||||
programSymbolManager = viewModel.getClient()?.programSymbolManager,
|
programSymbolManager = viewModel.getClient()?.programSymbolManager,
|
||||||
|
graphicsPlane = viewModel.getClient()?.graphicsPlane,
|
||||||
maskHiddenFields = maskHiddenInput,
|
maskHiddenFields = maskHiddenInput,
|
||||||
blinkCursor = cursorBlink,
|
blinkCursor = cursorBlink,
|
||||||
onTapAddress = { addr ->
|
onTapAddress = { addr ->
|
||||||
|
|||||||
@@ -221,11 +221,15 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun resolveUntrustedCert(accept: Boolean) {
|
fun resolveUntrustedCert(accept: Boolean) {
|
||||||
|
try {
|
||||||
val prompt = _untrustedCertPrompt.value
|
val prompt = _untrustedCertPrompt.value
|
||||||
if (prompt != null) {
|
if (prompt != null) {
|
||||||
prompt.deferred.complete(accept)
|
prompt.deferred.complete(accept)
|
||||||
_untrustedCertPrompt.value = null
|
_untrustedCertPrompt.value = null
|
||||||
}
|
}
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
log.warning("Error resolving untrusted cert: ${t.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateSettings(maskHidden: Boolean, blink: Boolean, haptic: Boolean, verifyCertsVal: Boolean = true, defaultGraphicsVal: String = "BOTH") {
|
fun updateSettings(maskHidden: Boolean, blink: Boolean, haptic: Boolean, verifyCertsVal: Boolean = true, defaultGraphicsVal: String = "BOTH") {
|
||||||
@@ -251,7 +255,6 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
tlsVerifyCert: Boolean = true,
|
tlsVerifyCert: Boolean = true,
|
||||||
graphicsModeStr: String = "BOTH"
|
graphicsModeStr: String = "BOTH"
|
||||||
) {
|
) {
|
||||||
if (_connectionState.value.isConnected()) return
|
|
||||||
currentHost = host
|
currentHost = host
|
||||||
activeHostType = hostType
|
activeHostType = hostType
|
||||||
lastScreenContentHash = 0
|
lastScreenContentHash = 0
|
||||||
@@ -261,6 +264,15 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
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) {
|
val model = when (modelNum) {
|
||||||
3 -> TerminalModel.IBM_3279_3
|
3 -> TerminalModel.IBM_3279_3
|
||||||
4 -> TerminalModel.IBM_3279_4
|
4 -> TerminalModel.IBM_3279_4
|
||||||
@@ -303,8 +315,14 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
exception = exception,
|
exception = exception,
|
||||||
deferred = deferred
|
deferred = deferred
|
||||||
)
|
)
|
||||||
val accepted = kotlinx.coroutines.runBlocking { deferred.await() }
|
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
|
_untrustedCertPrompt.value = null
|
||||||
|
}
|
||||||
accepted
|
accepted
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,10 +397,10 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
_screenBuffer.value = newClient.screenBuffer
|
_screenBuffer.value = newClient.screenBuffer
|
||||||
|
|
||||||
newClient.connect()
|
newClient.connect()
|
||||||
} catch (e: Exception) {
|
} catch (e: Throwable) {
|
||||||
log.severe("Failed to connect: ${e.message}")
|
log.severe("Failed to connect: ${e.message}")
|
||||||
_connectionState.value = ConnectionState.NOT_CONNECTED
|
_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)
|
val jsonArray = JSONArray(jsonStr)
|
||||||
for (i in 0 until jsonArray.length()) {
|
for (i in 0 until jsonArray.length()) {
|
||||||
val obj = jsonArray.getJSONObject(i)
|
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) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|||||||
@@ -298,9 +298,9 @@ fun ConnectDialog(
|
|||||||
) {
|
) {
|
||||||
Text("Graphics:", fontSize = 11.sp, color = Color.Gray)
|
Text("Graphics:", fontSize = 11.sp, color = Color.Gray)
|
||||||
listOf(
|
listOf(
|
||||||
"BOTH" to "Both",
|
|
||||||
"VECTOR_GRAPHICS" to "Vector",
|
|
||||||
"PROGRAMMED_SYMBOLS" to "Symbols",
|
"PROGRAMMED_SYMBOLS" to "Symbols",
|
||||||
|
"VECTOR_GRAPHICS" to "Vector",
|
||||||
|
"BOTH" to "Both",
|
||||||
"NONE" to "None"
|
"NONE" to "None"
|
||||||
).forEach { (modeKey, modeLabel) ->
|
).forEach { (modeKey, modeLabel) ->
|
||||||
FilterChip(
|
FilterChip(
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ fun TerminalView(
|
|||||||
cursorAddr: Int,
|
cursorAddr: Int,
|
||||||
screenVersion: Long,
|
screenVersion: Long,
|
||||||
programSymbolManager: org.lib3270j.graphics.ProgramSymbolManager? = null,
|
programSymbolManager: org.lib3270j.graphics.ProgramSymbolManager? = null,
|
||||||
|
graphicsPlane: org.lib3270j.graphics.GraphicsPlane? = null,
|
||||||
maskHiddenFields: Boolean = true,
|
maskHiddenFields: Boolean = true,
|
||||||
blinkCursor: Boolean = true,
|
blinkCursor: Boolean = true,
|
||||||
onTapAddress: (Int) -> Unit,
|
onTapAddress: (Int) -> Unit,
|
||||||
@@ -304,28 +305,8 @@ fun TerminalView(
|
|||||||
if (slot != null) {
|
if (slot != null) {
|
||||||
val symWidth = slot.width
|
val symWidth = slot.width
|
||||||
val symHeight = slot.height
|
val symHeight = slot.height
|
||||||
val pixelData = slot.pixelData
|
val rgbArray = slot.getRgbPixels(fgColor.toArgb(), bgColor.toArgb())
|
||||||
if (pixelData != null && symWidth > 0 && symHeight > 0) {
|
if (rgbArray != 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 bmp = android.graphics.Bitmap.createBitmap(rgbArray, 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,
|
||||||
@@ -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
|
// Context Menu Popup
|
||||||
|
|||||||
@@ -42,30 +42,54 @@ fun UntrustedCertificateDialog(
|
|||||||
append("\n\n")
|
append("\n\n")
|
||||||
}
|
}
|
||||||
if (cert != null) {
|
if (cert != null) {
|
||||||
|
try {
|
||||||
append("Subject:\n ")
|
append("Subject:\n ")
|
||||||
append(cert.subjectX500Principal.name)
|
append(cert.subjectX500Principal?.name ?: cert.subjectDN?.name ?: "Unknown")
|
||||||
append("\n\n")
|
append("\n\n")
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
append("Subject: ${t.message ?: "Unavailable"}\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
append("Issuer:\n ")
|
append("Issuer:\n ")
|
||||||
append(cert.issuerX500Principal.name)
|
append(cert.issuerX500Principal?.name ?: cert.issuerDN?.name ?: "Unknown")
|
||||||
append("\n\n")
|
append("\n\n")
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
append("Issuer: ${t.message ?: "Unavailable"}\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
append("Validity:\n From: ")
|
append("Validity:\n From: ")
|
||||||
append(sdf.format(cert.notBefore))
|
append(cert.notBefore?.let { sdf.format(it) } ?: "Unknown")
|
||||||
append("\n To: ")
|
append("\n To: ")
|
||||||
append(sdf.format(cert.notAfter))
|
append(cert.notAfter?.let { sdf.format(it) } ?: "Unknown")
|
||||||
append("\n\n")
|
append("\n\n")
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
append("Validity: ${t.message ?: "Unavailable"}\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
append("Serial Number:\n ")
|
append("Serial Number:\n ")
|
||||||
append(cert.serialNumber.toString(16).uppercase(Locale.US))
|
append(cert.serialNumber?.toString(16)?.uppercase(Locale.US) ?: "Unknown")
|
||||||
append("\n\n")
|
append("\n\n")
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
append("Serial Number: ${t.message ?: "Unavailable"}\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
append("SHA-256 Fingerprint:\n ")
|
append("SHA-256 Fingerprint:\n ")
|
||||||
append(computeFingerprint(cert, "SHA-256"))
|
append(computeFingerprint(cert, "SHA-256"))
|
||||||
append("\n\n")
|
append("\n\n")
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
append("SHA-256 Fingerprint: ${t.message ?: "Unavailable"}\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
append("SHA-1 Fingerprint:\n ")
|
append("SHA-1 Fingerprint:\n ")
|
||||||
append(computeFingerprint(cert, "SHA-1"))
|
append(computeFingerprint(cert, "SHA-1"))
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
append("SHA-1 Fingerprint: ${t.message ?: "Unavailable"}")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
append("No peer certificate information available.")
|
append("No peer certificate information available.")
|
||||||
}
|
}
|
||||||
@@ -161,8 +185,8 @@ private fun computeFingerprint(cert: X509Certificate, algorithm: String): String
|
|||||||
return try {
|
return try {
|
||||||
val md = MessageDigest.getInstance(algorithm)
|
val md = MessageDigest.getInstance(algorithm)
|
||||||
val digest = md.digest(cert.encoded)
|
val digest = md.digest(cert.encoded)
|
||||||
digest.joinToString(":") { "%02X".format(it) }
|
digest.joinToString(":") { "%02X".format(it.toInt() and 0xFF) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Throwable) {
|
||||||
"Unable to compute fingerprint: ${e.message}"
|
"Unable to compute fingerprint: ${e.message}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user