Begin v1.0.1 commits
Build and Test a3270 / Build Android APK (push) Successful in 7m26s

This commit is contained in:
2026-08-31 10:30:32 -04:00
parent a7cc4b9d50
commit 9f96555c9d
7 changed files with 203 additions and 15 deletions
+2 -2
View File
@@ -11,8 +11,8 @@ android {
applicationId "haus.nightmare.a3270" applicationId "haus.nightmare.a3270"
minSdk 24 minSdk 24
targetSdk 34 targetSdk 34
versionCode 5 versionCode 6
versionName "1.0.0" versionName "1.0.1"
} }
buildTypes { buildTypes {
@@ -460,7 +460,12 @@ fun MainScreen(
autoHost.tlsVerifyCert, autoHost.tlsVerifyCert,
autoHost.tn3270e, autoHost.tn3270e,
autoHost.graphicsMode, autoHost.graphicsMode,
autoHost.codePage autoHost.codePage,
autoHost.proxyType,
autoHost.proxyHost,
autoHost.proxyPort,
autoHost.proxyUsername,
autoHost.proxyPassword
) )
} }
} }
@@ -638,9 +643,9 @@ fun MainScreen(
if (showConnectDialog) { if (showConnectDialog) {
ConnectDialog( ConnectDialog(
onDismiss = { showConnectDialog = false }, onDismiss = { showConnectDialog = false },
onConnect = { host, port, model, luName, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, cp -> onConnect = { host, port, model, luName, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, cp, pType, pHost, pPort, pUser, pPass ->
showConnectDialog = false showConnectDialog = false
viewModel.connect(host, port, model, luName, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, cp) viewModel.connect(host, port, model, luName, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, cp, pType, pHost, pPort, pUser, pPass)
terminalInputViewRef?.showSoftKeyboard() terminalInputViewRef?.showSoftKeyboard()
} }
) )
@@ -396,7 +396,12 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
tlsVerifyCert: Boolean = true, tlsVerifyCert: Boolean = true,
tn3270e: Boolean = true, tn3270e: Boolean = true,
graphicsModeStr: String = "BOTH", graphicsModeStr: String = "BOTH",
codePageStr: String = "037" codePageStr: String = "037",
proxyTypeStr: String = "NONE",
proxyHost: String = "",
proxyPort: Int = 0,
proxyUser: String = "",
proxyPass: String = ""
) { ) {
currentHost = host currentHost = host
activeHostType = hostType activeHostType = hostType
@@ -447,6 +452,16 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
isTn3270eEnabled = effectiveTn3270e isTn3270eEnabled = effectiveTn3270e
graphicsMode = gMode graphicsMode = gMode
codePage = codePageStr codePage = codePageStr
val pType = try {
ConnectionConfig.ProxyType.valueOf(proxyTypeStr.uppercase())
} catch (_: Exception) {
ConnectionConfig.ProxyType.NONE
}
if (pType != ConnectionConfig.ProxyType.NONE && proxyHost.isNotBlank()) {
val defaultPort = if (pType == ConnectionConfig.ProxyType.HTTP) 8080 else 1080
setProxy(pType, proxyHost.trim(), if (proxyPort > 0) proxyPort else defaultPort, proxyUser.ifBlank { null }, proxyPass.ifBlank { null })
}
} }
config.certificateVerifier = haus.nightmare.lib3270j.tls.TlsCertificateVerifier { chain, _, exception -> config.certificateVerifier = haus.nightmare.lib3270j.tls.TlsCertificateVerifier { chain, _, exception ->
@@ -19,7 +19,12 @@ data class SavedHost(
val tlsVerifyCert: Boolean = true, val tlsVerifyCert: Boolean = true,
val tn3270e: Boolean = true, val tn3270e: Boolean = true,
val graphicsMode: String = "BOTH", val graphicsMode: String = "BOTH",
val codePage: String = "037" val codePage: String = "037",
val proxyType: String = "NONE",
val proxyHost: String = "",
val proxyPort: Int = 0,
val proxyUsername: String = "",
val proxyPassword: String = ""
) { ) {
fun toJson(): JSONObject { fun toJson(): JSONObject {
return JSONObject().apply { return JSONObject().apply {
@@ -36,6 +41,11 @@ data class SavedHost(
put("tn3270e", tn3270e) put("tn3270e", tn3270e)
put("graphicsMode", graphicsMode) put("graphicsMode", graphicsMode)
put("codePage", codePage) put("codePage", codePage)
put("proxyType", proxyType)
put("proxyHost", proxyHost)
put("proxyPort", proxyPort)
put("proxyUsername", proxyUsername)
put("proxyPassword", proxyPassword)
} }
} }
@@ -54,7 +64,12 @@ data class SavedHost(
tlsVerifyCert = json.optBoolean("tlsVerifyCert", true), tlsVerifyCert = json.optBoolean("tlsVerifyCert", true),
tn3270e = json.optBoolean("tn3270e", true), tn3270e = json.optBoolean("tn3270e", true),
graphicsMode = json.optString("graphicsMode", "BOTH"), graphicsMode = json.optString("graphicsMode", "BOTH"),
codePage = json.optString("codePage", "037") codePage = json.optString("codePage", "037"),
proxyType = json.optString("proxyType", "NONE"),
proxyHost = json.optString("proxyHost", ""),
proxyPort = json.optInt("proxyPort", 0),
proxyUsername = json.optString("proxyUsername", ""),
proxyPassword = json.optString("proxyPassword", "")
) )
} }
} }
@@ -48,7 +48,12 @@ fun ConnectDialog(
tlsVerifyCert: Boolean, tlsVerifyCert: Boolean,
tn3270e: Boolean, tn3270e: Boolean,
graphicsMode: String, graphicsMode: String,
codePage: String codePage: String,
proxyType: String,
proxyHost: String,
proxyPort: Int,
proxyUser: String,
proxyPass: String
) -> Unit ) -> Unit
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -67,6 +72,12 @@ fun ConnectDialog(
var tn3270e by remember { mutableStateOf(true) } var tn3270e by remember { mutableStateOf(true) }
var graphicsMode by remember { mutableStateOf("BOTH") } var graphicsMode by remember { mutableStateOf("BOTH") }
var codePage by remember { mutableStateOf(haus.nightmare.a3270.storage.AppSettings.getDefaultCodePage(context)) } var codePage by remember { mutableStateOf(haus.nightmare.a3270.storage.AppSettings.getDefaultCodePage(context)) }
var proxyType by remember { mutableStateOf("NONE") }
var proxyHost by remember { mutableStateOf("") }
var proxyPortStr by remember { mutableStateOf("") }
var proxyUsername by remember { mutableStateOf("") }
var proxyPassword by remember { mutableStateOf("") }
var showProxySettings by remember { mutableStateOf(false) }
fun loadProfile(saved: SavedHost) { fun loadProfile(saved: SavedHost) {
selectedHostId = saved.id selectedHostId = saved.id
@@ -82,6 +93,12 @@ fun ConnectDialog(
tn3270e = saved.tn3270e tn3270e = saved.tn3270e
graphicsMode = saved.graphicsMode graphicsMode = saved.graphicsMode
codePage = saved.codePage codePage = saved.codePage
proxyType = saved.proxyType
proxyHost = saved.proxyHost
proxyPortStr = if (saved.proxyPort > 0) saved.proxyPort.toString() else ""
proxyUsername = saved.proxyUsername
proxyPassword = saved.proxyPassword
showProxySettings = saved.proxyType != "NONE"
} }
fun clearFields() { fun clearFields() {
@@ -98,6 +115,12 @@ fun ConnectDialog(
tn3270e = true tn3270e = true
graphicsMode = "BOTH" graphicsMode = "BOTH"
codePage = haus.nightmare.a3270.storage.AppSettings.getDefaultCodePage(context) codePage = haus.nightmare.a3270.storage.AppSettings.getDefaultCodePage(context)
proxyType = "NONE"
proxyHost = ""
proxyPortStr = ""
proxyUsername = ""
proxyPassword = ""
showProxySettings = false
} }
fun saveCurrentProfile(): SavedHost? { fun saveCurrentProfile(): SavedHost? {
@@ -118,7 +141,12 @@ fun ConnectDialog(
tlsVerifyCert = tlsVerifyCert, tlsVerifyCert = tlsVerifyCert,
tn3270e = tn3270e, tn3270e = tn3270e,
graphicsMode = graphicsMode, graphicsMode = graphicsMode,
codePage = codePage codePage = codePage,
proxyType = proxyType,
proxyHost = proxyHost.trim(),
proxyPort = proxyPortStr.trim().toIntOrNull() ?: 0,
proxyUsername = proxyUsername.trim(),
proxyPassword = proxyPassword.trim()
) )
HostStorage.saveHost(context, hostToSave) HostStorage.saveHost(context, hostToSave)
savedHosts = HostStorage.getSavedHosts(context) savedHosts = HostStorage.getSavedHosts(context)
@@ -251,7 +279,12 @@ fun ConnectDialog(
hostToConn.tlsVerifyCert, hostToConn.tlsVerifyCert,
hostToConn.tn3270e, hostToConn.tn3270e,
hostToConn.graphicsMode, hostToConn.graphicsMode,
hostToConn.codePage hostToConn.codePage,
hostToConn.proxyType,
hostToConn.proxyHost,
hostToConn.proxyPort,
hostToConn.proxyUsername,
hostToConn.proxyPassword
) )
}, },
modifier = Modifier.size(32.dp) modifier = Modifier.size(32.dp)
@@ -611,6 +644,102 @@ fun ConnectDialog(
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
Text("Auto-connect on app startup", fontSize = 12.sp, color = Color.White) Text("Auto-connect on app startup", fontSize = 12.sp, color = Color.White)
} }
// Proxy Settings Expandable Section
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable { showProxySettings = !showProxySettings }
) {
Checkbox(
checked = showProxySettings || proxyType != "NONE",
onCheckedChange = { checked ->
showProxySettings = checked
if (!checked) proxyType = "NONE"
}
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = if (proxyType != "NONE") "Proxy Active ($proxyType)" else "Configure Network Proxy",
fontSize = 12.sp,
color = if (proxyType != "NONE") Color(0xFF339AF0) else Color.White
)
}
if (showProxySettings || proxyType != "NONE") {
Card(
colors = CardDefaults.cardColors(containerColor = Color(0xFF25262B)),
modifier = Modifier.fillMaxWidth().padding(start = 8.dp, end = 4.dp, top = 4.dp)
) {
Column(
modifier = Modifier.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Text("Proxy Protocol:", fontSize = 11.sp, color = Color.Gray)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
listOf("NONE", "HTTP", "SOCKS4", "SOCKS5").forEach { pType ->
val isSel = proxyType == pType
FilterChip(
selected = isSel,
onClick = { proxyType = pType },
label = { Text(pType, fontSize = 10.sp) }
)
}
}
if (proxyType != "NONE") {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
OutlinedTextField(
value = proxyHost,
onValueChange = { proxyHost = it },
label = { Text("Proxy Host") },
singleLine = true,
keyboardOptions = TerminalKeyboardOptions,
modifier = Modifier.weight(0.7f)
)
OutlinedTextField(
value = proxyPortStr,
onValueChange = { proxyPortStr = it },
label = { Text("Port") },
placeholder = { Text(if (proxyType == "HTTP") "8080" else "1080") },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.weight(0.3f)
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
OutlinedTextField(
value = proxyUsername,
onValueChange = { proxyUsername = it },
label = { Text("User (Opt)") },
singleLine = true,
keyboardOptions = TerminalKeyboardOptions,
modifier = Modifier.weight(0.5f)
)
OutlinedTextField(
value = proxyPassword,
onValueChange = { proxyPassword = it },
label = { Text("Pass (Opt)") },
singleLine = true,
keyboardOptions = TerminalKeyboardOptions,
modifier = Modifier.weight(0.5f)
)
}
}
}
}
}
} }
} }
} }
@@ -645,7 +774,12 @@ fun ConnectDialog(
saved.tlsVerifyCert, saved.tlsVerifyCert,
saved.tn3270e, saved.tn3270e,
saved.graphicsMode, saved.graphicsMode,
saved.codePage saved.codePage,
saved.proxyType,
saved.proxyHost,
saved.proxyPort,
saved.proxyUsername,
saved.proxyPassword
) )
} }
}, },
@@ -85,7 +85,8 @@ fun ScriptDialog(
) { ) {
listOf( listOf(
"[enter]", "[tab]", "[backtab]", "[home]", "[clear]", "[enter]", "[tab]", "[backtab]", "[home]", "[clear]",
"[reset]", "[eraseeof]", "[eraseinpt]", "[attn]", "[sysreq]" "[reset]", "[eraseeof]", "[eraseinpt]", "[attn]", "[sysreq]",
"[wordleft]", "[wordright]", "[fieldend]", "[deleteword]", "[cursel]", "[lightpen]"
).forEach { tok -> ).forEach { tok ->
SuggestionChip( SuggestionChip(
onClick = { insertToken(tok) }, onClick = { insertToken(tok) },
@@ -26,13 +26,23 @@ class CodePageAndStorageTest {
tlsVerifyCert = false, tlsVerifyCert = false,
tn3270e = true, tn3270e = true,
graphicsMode = "BOTH", graphicsMode = "BOTH",
codePage = "1047" codePage = "1047",
proxyType = "SOCKS5",
proxyHost = "proxy.example.com",
proxyPort = 1080,
proxyUsername = "testuser",
proxyPassword = "secretpassword"
) )
val json = host.toJson() val json = host.toJson()
assertEquals("1047", json.getString("codePage")) assertEquals("1047", json.getString("codePage"))
assertEquals("Test Mainframe", json.getString("name")) assertEquals("Test Mainframe", json.getString("name"))
assertTrue(json.getBoolean("autoConnect")) assertTrue(json.getBoolean("autoConnect"))
assertEquals("SOCKS5", json.getString("proxyType"))
assertEquals("proxy.example.com", json.getString("proxyHost"))
assertEquals(1080, json.getInt("proxyPort"))
assertEquals("testuser", json.getString("proxyUsername"))
assertEquals("secretpassword", json.getString("proxyPassword"))
val deserialized = SavedHost.fromJson(json) val deserialized = SavedHost.fromJson(json)
assertEquals(host.id, deserialized.id) assertEquals(host.id, deserialized.id)
@@ -42,17 +52,25 @@ class CodePageAndStorageTest {
assertEquals(host.model, deserialized.model) assertEquals(host.model, deserialized.model)
assertEquals(host.codePage, deserialized.codePage) assertEquals(host.codePage, deserialized.codePage)
assertEquals(host.graphicsMode, deserialized.graphicsMode) assertEquals(host.graphicsMode, deserialized.graphicsMode)
assertEquals(host.proxyType, deserialized.proxyType)
assertEquals(host.proxyHost, deserialized.proxyHost)
assertEquals(host.proxyPort, deserialized.proxyPort)
assertEquals(host.proxyUsername, deserialized.proxyUsername)
assertEquals(host.proxyPassword, deserialized.proxyPassword)
} }
@Test @Test
fun testSavedHostDefaultCodePageFallback() { fun testSavedHostDefaultCodePageFallback() {
// Old JSON without codePage field // Old JSON without codePage or proxy fields
val json = org.json.JSONObject().apply { val json = org.json.JSONObject().apply {
put("name", "Legacy") put("name", "Legacy")
put("host", "mvs.local") put("host", "mvs.local")
} }
val deserialized = SavedHost.fromJson(json) val deserialized = SavedHost.fromJson(json)
assertEquals("037", deserialized.codePage) assertEquals("037", deserialized.codePage)
assertEquals("NONE", deserialized.proxyType)
assertEquals("", deserialized.proxyHost)
assertEquals(0, deserialized.proxyPort)
} }
@Test @Test