diff --git a/build.gradle b/build.gradle index ed5f950..8947d6d 100644 --- a/build.gradle +++ b/build.gradle @@ -11,8 +11,8 @@ android { applicationId "haus.nightmare.a3270" minSdk 24 targetSdk 34 - versionCode 5 - versionName "1.0.0" + versionCode 6 + versionName "1.0.1" } buildTypes { diff --git a/src/main/java/haus/nightmare/a3270/MainActivity.kt b/src/main/java/haus/nightmare/a3270/MainActivity.kt index 6b81a97..96268fe 100644 --- a/src/main/java/haus/nightmare/a3270/MainActivity.kt +++ b/src/main/java/haus/nightmare/a3270/MainActivity.kt @@ -460,7 +460,12 @@ fun MainScreen( autoHost.tlsVerifyCert, autoHost.tn3270e, autoHost.graphicsMode, - autoHost.codePage + autoHost.codePage, + autoHost.proxyType, + autoHost.proxyHost, + autoHost.proxyPort, + autoHost.proxyUsername, + autoHost.proxyPassword ) } } @@ -638,9 +643,9 @@ fun MainScreen( if (showConnectDialog) { ConnectDialog( 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 - 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() } ) diff --git a/src/main/java/haus/nightmare/a3270/TerminalViewModel.kt b/src/main/java/haus/nightmare/a3270/TerminalViewModel.kt index 2e22f27..b2ed91b 100644 --- a/src/main/java/haus/nightmare/a3270/TerminalViewModel.kt +++ b/src/main/java/haus/nightmare/a3270/TerminalViewModel.kt @@ -396,7 +396,12 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application tlsVerifyCert: Boolean = true, tn3270e: Boolean = true, graphicsModeStr: String = "BOTH", - codePageStr: String = "037" + codePageStr: String = "037", + proxyTypeStr: String = "NONE", + proxyHost: String = "", + proxyPort: Int = 0, + proxyUser: String = "", + proxyPass: String = "" ) { currentHost = host activeHostType = hostType @@ -447,6 +452,16 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application isTn3270eEnabled = effectiveTn3270e graphicsMode = gMode 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 -> diff --git a/src/main/java/haus/nightmare/a3270/storage/HostStorage.kt b/src/main/java/haus/nightmare/a3270/storage/HostStorage.kt index 98685c7..bc0d99e 100644 --- a/src/main/java/haus/nightmare/a3270/storage/HostStorage.kt +++ b/src/main/java/haus/nightmare/a3270/storage/HostStorage.kt @@ -19,7 +19,12 @@ data class SavedHost( val tlsVerifyCert: Boolean = true, val tn3270e: Boolean = true, 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 { return JSONObject().apply { @@ -36,6 +41,11 @@ data class SavedHost( put("tn3270e", tn3270e) put("graphicsMode", graphicsMode) 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), tn3270e = json.optBoolean("tn3270e", true), 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", "") ) } } diff --git a/src/main/java/haus/nightmare/a3270/ui/ConnectDialog.kt b/src/main/java/haus/nightmare/a3270/ui/ConnectDialog.kt index 086b3ee..3f7bbe5 100644 --- a/src/main/java/haus/nightmare/a3270/ui/ConnectDialog.kt +++ b/src/main/java/haus/nightmare/a3270/ui/ConnectDialog.kt @@ -48,7 +48,12 @@ fun ConnectDialog( tlsVerifyCert: Boolean, tn3270e: Boolean, graphicsMode: String, - codePage: String + codePage: String, + proxyType: String, + proxyHost: String, + proxyPort: Int, + proxyUser: String, + proxyPass: String ) -> Unit ) { val context = LocalContext.current @@ -67,6 +72,12 @@ fun ConnectDialog( var tn3270e by remember { mutableStateOf(true) } var graphicsMode by remember { mutableStateOf("BOTH") } 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) { selectedHostId = saved.id @@ -82,6 +93,12 @@ fun ConnectDialog( tn3270e = saved.tn3270e graphicsMode = saved.graphicsMode 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() { @@ -98,6 +115,12 @@ fun ConnectDialog( tn3270e = true graphicsMode = "BOTH" codePage = haus.nightmare.a3270.storage.AppSettings.getDefaultCodePage(context) + proxyType = "NONE" + proxyHost = "" + proxyPortStr = "" + proxyUsername = "" + proxyPassword = "" + showProxySettings = false } fun saveCurrentProfile(): SavedHost? { @@ -118,7 +141,12 @@ fun ConnectDialog( tlsVerifyCert = tlsVerifyCert, tn3270e = tn3270e, 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) savedHosts = HostStorage.getSavedHosts(context) @@ -251,7 +279,12 @@ fun ConnectDialog( hostToConn.tlsVerifyCert, hostToConn.tn3270e, hostToConn.graphicsMode, - hostToConn.codePage + hostToConn.codePage, + hostToConn.proxyType, + hostToConn.proxyHost, + hostToConn.proxyPort, + hostToConn.proxyUsername, + hostToConn.proxyPassword ) }, modifier = Modifier.size(32.dp) @@ -611,6 +644,102 @@ fun ConnectDialog( Spacer(modifier = Modifier.width(4.dp)) 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.tn3270e, saved.graphicsMode, - saved.codePage + saved.codePage, + saved.proxyType, + saved.proxyHost, + saved.proxyPort, + saved.proxyUsername, + saved.proxyPassword ) } }, diff --git a/src/main/java/haus/nightmare/a3270/ui/ScriptDialog.kt b/src/main/java/haus/nightmare/a3270/ui/ScriptDialog.kt index 5375fed..38c8ff3 100644 --- a/src/main/java/haus/nightmare/a3270/ui/ScriptDialog.kt +++ b/src/main/java/haus/nightmare/a3270/ui/ScriptDialog.kt @@ -85,7 +85,8 @@ fun ScriptDialog( ) { listOf( "[enter]", "[tab]", "[backtab]", "[home]", "[clear]", - "[reset]", "[eraseeof]", "[eraseinpt]", "[attn]", "[sysreq]" + "[reset]", "[eraseeof]", "[eraseinpt]", "[attn]", "[sysreq]", + "[wordleft]", "[wordright]", "[fieldend]", "[deleteword]", "[cursel]", "[lightpen]" ).forEach { tok -> SuggestionChip( onClick = { insertToken(tok) }, diff --git a/src/test/java/haus/nightmare/a3270/CodePageAndStorageTest.kt b/src/test/java/haus/nightmare/a3270/CodePageAndStorageTest.kt index 2fe70b9..de2eb63 100644 --- a/src/test/java/haus/nightmare/a3270/CodePageAndStorageTest.kt +++ b/src/test/java/haus/nightmare/a3270/CodePageAndStorageTest.kt @@ -26,13 +26,23 @@ class CodePageAndStorageTest { tlsVerifyCert = false, tn3270e = true, graphicsMode = "BOTH", - codePage = "1047" + codePage = "1047", + proxyType = "SOCKS5", + proxyHost = "proxy.example.com", + proxyPort = 1080, + proxyUsername = "testuser", + proxyPassword = "secretpassword" ) val json = host.toJson() assertEquals("1047", json.getString("codePage")) assertEquals("Test Mainframe", json.getString("name")) 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) assertEquals(host.id, deserialized.id) @@ -42,17 +52,25 @@ class CodePageAndStorageTest { assertEquals(host.model, deserialized.model) assertEquals(host.codePage, deserialized.codePage) 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 fun testSavedHostDefaultCodePageFallback() { - // Old JSON without codePage field + // Old JSON without codePage or proxy fields val json = org.json.JSONObject().apply { put("name", "Legacy") put("host", "mvs.local") } val deserialized = SavedHost.fromJson(json) assertEquals("037", deserialized.codePage) + assertEquals("NONE", deserialized.proxyType) + assertEquals("", deserialized.proxyHost) + assertEquals(0, deserialized.proxyPort) } @Test