diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/LICENSE @@ -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 diff --git a/build.gradle b/build.gradle index c098801..2d47810 100644 --- a/build.gradle +++ b/build.gradle @@ -42,6 +42,13 @@ android { if (findProject(':lib3270j') != null) { project(':lib3270j') { apply plugin: 'java-library' + dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + } + test { + useJUnitPlatform() + } java { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 diff --git a/src/main/java/org/pubvm/a3270/MainActivity.kt b/src/main/java/org/pubvm/a3270/MainActivity.kt index 7b66439..0785736 100644 --- a/src/main/java/org/pubvm/a3270/MainActivity.kt +++ b/src/main/java/org/pubvm/a3270/MainActivity.kt @@ -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 -> diff --git a/src/main/java/org/pubvm/a3270/TerminalViewModel.kt b/src/main/java/org/pubvm/a3270/TerminalViewModel.kt index ba25df4..5f5e4ff 100644 --- a/src/main/java/org/pubvm/a3270/TerminalViewModel.kt +++ b/src/main/java/org/pubvm/a3270/TerminalViewModel.kt @@ -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"}" } } } diff --git a/src/main/java/org/pubvm/a3270/storage/HostStorage.kt b/src/main/java/org/pubvm/a3270/storage/HostStorage.kt index 6383ab6..f37477a 100644 --- a/src/main/java/org/pubvm/a3270/storage/HostStorage.kt +++ b/src/main/java/org/pubvm/a3270/storage/HostStorage.kt @@ -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() diff --git a/src/main/java/org/pubvm/a3270/ui/ConnectDialog.kt b/src/main/java/org/pubvm/a3270/ui/ConnectDialog.kt index c9130f2..680ed1c 100644 --- a/src/main/java/org/pubvm/a3270/ui/ConnectDialog.kt +++ b/src/main/java/org/pubvm/a3270/ui/ConnectDialog.kt @@ -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( diff --git a/src/main/java/org/pubvm/a3270/ui/TerminalView.kt b/src/main/java/org/pubvm/a3270/ui/TerminalView.kt index 13464a1..305255c 100644 --- a/src/main/java/org/pubvm/a3270/ui/TerminalView.kt +++ b/src/main/java/org/pubvm/a3270/ui/TerminalView.kt @@ -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 diff --git a/src/main/java/org/pubvm/a3270/ui/UntrustedCertificateDialog.kt b/src/main/java/org/pubvm/a3270/ui/UntrustedCertificateDialog.kt index 16f5901..e23b663 100644 --- a/src/main/java/org/pubvm/a3270/ui/UntrustedCertificateDialog.kt +++ b/src/main/java/org/pubvm/a3270/ui/UntrustedCertificateDialog.kt @@ -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}" } }