This commit is contained in:
@@ -32,6 +32,8 @@ import org.pubvm.a3270.ui.TerminalInputView
|
|||||||
import org.pubvm.a3270.ui.TerminalView
|
import org.pubvm.a3270.ui.TerminalView
|
||||||
import org.pubvm.a3270.ui.TwoRowKeyBar
|
import org.pubvm.a3270.ui.TwoRowKeyBar
|
||||||
|
|
||||||
|
import org.pubvm.a3270.ui.UntrustedCertificateDialog
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
private val viewModel: TerminalViewModel by viewModels()
|
private val viewModel: TerminalViewModel by viewModels()
|
||||||
@@ -148,6 +150,11 @@ fun MainScreen(
|
|||||||
val maskHiddenInput by viewModel.maskHiddenInput.collectAsState()
|
val maskHiddenInput by viewModel.maskHiddenInput.collectAsState()
|
||||||
val cursorBlink by viewModel.cursorBlink.collectAsState()
|
val cursorBlink by viewModel.cursorBlink.collectAsState()
|
||||||
val hapticFeedback by viewModel.hapticFeedback.collectAsState()
|
val hapticFeedback by viewModel.hapticFeedback.collectAsState()
|
||||||
|
val verifyCerts by viewModel.verifyCerts.collectAsState()
|
||||||
|
val defaultGraphicsMode by viewModel.defaultGraphicsMode.collectAsState()
|
||||||
|
val isTlsActive by viewModel.isTlsActive.collectAsState()
|
||||||
|
val isTlsVerified by viewModel.isTlsVerified.collectAsState()
|
||||||
|
val untrustedCertPrompt by viewModel.untrustedCertPrompt.collectAsState()
|
||||||
val ftState by viewModel.ftState.collectAsState()
|
val ftState by viewModel.ftState.collectAsState()
|
||||||
|
|
||||||
var showConnectDialog by remember { mutableStateOf(false) }
|
var showConnectDialog by remember { mutableStateOf(false) }
|
||||||
@@ -174,7 +181,16 @@ fun MainScreen(
|
|||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
val autoHost = HostStorage.getAutoConnectHost(context)
|
val autoHost = HostStorage.getAutoConnectHost(context)
|
||||||
if (autoHost != null && !connectionState.isConnected()) {
|
if (autoHost != null && !connectionState.isConnected()) {
|
||||||
viewModel.connect(autoHost.host, autoHost.port, autoHost.model, autoHost.luName, autoHost.hostType)
|
viewModel.connect(
|
||||||
|
autoHost.host,
|
||||||
|
autoHost.port,
|
||||||
|
autoHost.model,
|
||||||
|
autoHost.luName,
|
||||||
|
autoHost.hostType,
|
||||||
|
autoHost.useTls,
|
||||||
|
autoHost.tlsVerifyCert,
|
||||||
|
autoHost.graphicsMode
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,6 +220,7 @@ fun MainScreen(
|
|||||||
cols = cols,
|
cols = cols,
|
||||||
cursorAddr = cursorAddr,
|
cursorAddr = cursorAddr,
|
||||||
screenVersion = screenVersion,
|
screenVersion = screenVersion,
|
||||||
|
programSymbolManager = viewModel.getClient()?.programSymbolManager,
|
||||||
maskHiddenFields = maskHiddenInput,
|
maskHiddenFields = maskHiddenInput,
|
||||||
blinkCursor = cursorBlink,
|
blinkCursor = cursorBlink,
|
||||||
onTapAddress = { addr ->
|
onTapAddress = { addr ->
|
||||||
@@ -298,7 +315,10 @@ fun MainScreen(
|
|||||||
oiaText = oiaText,
|
oiaText = oiaText,
|
||||||
cursorAddr = cursorAddr,
|
cursorAddr = cursorAddr,
|
||||||
rows = rows,
|
rows = rows,
|
||||||
cols = cols
|
cols = cols,
|
||||||
|
isTls = isTlsActive,
|
||||||
|
isTlsVerified = isTlsVerified,
|
||||||
|
graphicsMode = defaultGraphicsMode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -306,9 +326,9 @@ fun MainScreen(
|
|||||||
if (showConnectDialog) {
|
if (showConnectDialog) {
|
||||||
ConnectDialog(
|
ConnectDialog(
|
||||||
onDismiss = { showConnectDialog = false },
|
onDismiss = { showConnectDialog = false },
|
||||||
onConnect = { host, port, model, luName, hostType ->
|
onConnect = { host, port, model, luName, hostType, useTls, tlsVerifyCert, graphicsMode ->
|
||||||
showConnectDialog = false
|
showConnectDialog = false
|
||||||
viewModel.connect(host, port, model, luName, hostType)
|
viewModel.connect(host, port, model, luName, hostType, useTls, tlsVerifyCert, graphicsMode)
|
||||||
terminalInputViewRef?.showSoftKeyboard()
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -337,12 +357,30 @@ fun MainScreen(
|
|||||||
initialMaskHiddenInput = maskHiddenInput,
|
initialMaskHiddenInput = maskHiddenInput,
|
||||||
initialCursorBlink = cursorBlink,
|
initialCursorBlink = cursorBlink,
|
||||||
initialHapticFeedback = hapticFeedback,
|
initialHapticFeedback = hapticFeedback,
|
||||||
|
initialVerifyCerts = verifyCerts,
|
||||||
|
initialDefaultGraphicsMode = defaultGraphicsMode,
|
||||||
onDismiss = { showSettingsDialog = false },
|
onDismiss = { showSettingsDialog = false },
|
||||||
onSave = { mask, blink, haptic ->
|
onSave = { mask, blink, haptic, verify, gfx ->
|
||||||
showSettingsDialog = false
|
showSettingsDialog = false
|
||||||
viewModel.updateSettings(mask, blink, haptic)
|
viewModel.updateSettings(mask, blink, haptic, verify, gfx)
|
||||||
terminalInputViewRef?.showSoftKeyboard()
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val prompt = untrustedCertPrompt
|
||||||
|
if (prompt != null) {
|
||||||
|
UntrustedCertificateDialog(
|
||||||
|
host = prompt.host,
|
||||||
|
port = prompt.port,
|
||||||
|
chain = prompt.chain,
|
||||||
|
exception = prompt.exception,
|
||||||
|
onAccept = {
|
||||||
|
viewModel.resolveUntrustedCert(true)
|
||||||
|
},
|
||||||
|
onReject = {
|
||||||
|
viewModel.resolveUntrustedCert(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,14 @@ sealed interface TerminalInputAction {
|
|||||||
data class SetCursor(val baddr: Int) : TerminalInputAction
|
data class SetCursor(val baddr: Int) : TerminalInputAction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class UntrustedCertPromptState(
|
||||||
|
val host: String,
|
||||||
|
val port: Int,
|
||||||
|
val chain: Array<java.security.cert.X509Certificate>?,
|
||||||
|
val exception: java.security.cert.CertificateException?,
|
||||||
|
val deferred: kotlinx.coroutines.CompletableDeferred<Boolean>
|
||||||
|
)
|
||||||
|
|
||||||
data class FTProgressState(
|
data class FTProgressState(
|
||||||
val isActive: Boolean = false,
|
val isActive: Boolean = false,
|
||||||
val isRunning: Boolean = false,
|
val isRunning: Boolean = false,
|
||||||
@@ -96,6 +104,21 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
private val _hapticFeedback = MutableStateFlow(AppSettings.isHapticFeedbackEnabled(application))
|
private val _hapticFeedback = MutableStateFlow(AppSettings.isHapticFeedbackEnabled(application))
|
||||||
val hapticFeedback: StateFlow<Boolean> = _hapticFeedback.asStateFlow()
|
val hapticFeedback: StateFlow<Boolean> = _hapticFeedback.asStateFlow()
|
||||||
|
|
||||||
|
private val _verifyCerts = MutableStateFlow(AppSettings.isVerifyCertsEnabled(application))
|
||||||
|
val verifyCerts: StateFlow<Boolean> = _verifyCerts.asStateFlow()
|
||||||
|
|
||||||
|
private val _defaultGraphicsMode = MutableStateFlow(AppSettings.getDefaultGraphicsMode(application))
|
||||||
|
val defaultGraphicsMode: StateFlow<String> = _defaultGraphicsMode.asStateFlow()
|
||||||
|
|
||||||
|
private val _isTlsActive = MutableStateFlow(false)
|
||||||
|
val isTlsActive: StateFlow<Boolean> = _isTlsActive.asStateFlow()
|
||||||
|
|
||||||
|
private val _isTlsVerified = MutableStateFlow(true)
|
||||||
|
val isTlsVerified: StateFlow<Boolean> = _isTlsVerified.asStateFlow()
|
||||||
|
|
||||||
|
private val _untrustedCertPrompt = MutableStateFlow<UntrustedCertPromptState?>(null)
|
||||||
|
val untrustedCertPrompt: StateFlow<UntrustedCertPromptState?> = _untrustedCertPrompt.asStateFlow()
|
||||||
|
|
||||||
// File Transfer State
|
// File Transfer State
|
||||||
private var fileTransferCoordinator: FileTransfer? = null
|
private var fileTransferCoordinator: FileTransfer? = null
|
||||||
private val _ftState = MutableStateFlow(FTProgressState())
|
private val _ftState = MutableStateFlow(FTProgressState())
|
||||||
@@ -107,6 +130,8 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
var activeHostType: String = "TSO"
|
var activeHostType: String = "TSO"
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
fun getClient(): Telnet3270Client? = client
|
||||||
|
|
||||||
private var lastScreenContentHash: Int = 0
|
private var lastScreenContentHash: Int = 0
|
||||||
private var hasInitialScreenLoaded: Boolean = false
|
private var hasInitialScreenLoaded: Boolean = false
|
||||||
|
|
||||||
@@ -195,16 +220,37 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateSettings(maskHidden: Boolean, blink: Boolean, haptic: Boolean) {
|
fun resolveUntrustedCert(accept: Boolean) {
|
||||||
|
val prompt = _untrustedCertPrompt.value
|
||||||
|
if (prompt != null) {
|
||||||
|
prompt.deferred.complete(accept)
|
||||||
|
_untrustedCertPrompt.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateSettings(maskHidden: Boolean, blink: Boolean, haptic: Boolean, verifyCertsVal: Boolean = true, defaultGraphicsVal: String = "BOTH") {
|
||||||
AppSettings.setMaskHiddenInputEnabled(getApplication(), maskHidden)
|
AppSettings.setMaskHiddenInputEnabled(getApplication(), maskHidden)
|
||||||
AppSettings.setCursorBlinkEnabled(getApplication(), blink)
|
AppSettings.setCursorBlinkEnabled(getApplication(), blink)
|
||||||
AppSettings.setHapticFeedbackEnabled(getApplication(), haptic)
|
AppSettings.setHapticFeedbackEnabled(getApplication(), haptic)
|
||||||
|
AppSettings.setVerifyCertsEnabled(getApplication(), verifyCertsVal)
|
||||||
|
AppSettings.setDefaultGraphicsMode(getApplication(), defaultGraphicsVal)
|
||||||
_maskHiddenInput.value = maskHidden
|
_maskHiddenInput.value = maskHidden
|
||||||
_cursorBlink.value = blink
|
_cursorBlink.value = blink
|
||||||
_hapticFeedback.value = haptic
|
_hapticFeedback.value = haptic
|
||||||
|
_verifyCerts.value = verifyCertsVal
|
||||||
|
_defaultGraphicsMode.value = defaultGraphicsVal
|
||||||
}
|
}
|
||||||
|
|
||||||
fun connect(host: String, port: Int = 23, modelNum: Int = 2, luName: String = "", hostType: String = "TSO") {
|
fun connect(
|
||||||
|
host: String,
|
||||||
|
port: Int = 23,
|
||||||
|
modelNum: Int = 2,
|
||||||
|
luName: String = "",
|
||||||
|
hostType: String = "TSO",
|
||||||
|
useTls: Boolean = false,
|
||||||
|
tlsVerifyCert: Boolean = true,
|
||||||
|
graphicsModeStr: String = "BOTH"
|
||||||
|
) {
|
||||||
if (_connectionState.value.isConnected()) return
|
if (_connectionState.value.isConnected()) return
|
||||||
currentHost = host
|
currentHost = host
|
||||||
activeHostType = hostType
|
activeHostType = hostType
|
||||||
@@ -215,8 +261,6 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
_oiaText.value = "Connecting to $host:$port..."
|
|
||||||
|
|
||||||
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
|
||||||
@@ -224,10 +268,44 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
else -> TerminalModel.IBM_3279_2
|
else -> TerminalModel.IBM_3279_2
|
||||||
}
|
}
|
||||||
|
|
||||||
val config = ConnectionConfig(host, port, model).apply {
|
val parsedConfig = ConnectionConfig.parseHostString(host, port, model)
|
||||||
|
val effectiveHost = parsedConfig.host
|
||||||
|
val effectivePort = parsedConfig.port
|
||||||
|
val effectiveTls = useTls || parsedConfig.isUseTls
|
||||||
|
val globalVerify = AppSettings.isVerifyCertsEnabled(getApplication())
|
||||||
|
val effectiveVerify = tlsVerifyCert && globalVerify
|
||||||
|
val gMode = org.lib3270j.graphics.GraphicsMode.fromString(graphicsModeStr)
|
||||||
|
|
||||||
|
_isTlsActive.value = effectiveTls
|
||||||
|
_isTlsVerified.value = effectiveVerify
|
||||||
|
_oiaText.value = "Connecting to $effectiveHost:$effectivePort" + (if (effectiveTls) " [TLS]" else "") + "..."
|
||||||
|
|
||||||
|
val config = ConnectionConfig(effectiveHost, effectivePort, model).apply {
|
||||||
if (luName.isNotBlank()) {
|
if (luName.isNotBlank()) {
|
||||||
setLuName(luName)
|
setLuName(luName)
|
||||||
}
|
}
|
||||||
|
isUseTls = effectiveTls
|
||||||
|
isTlsVerifyCert = effectiveVerify
|
||||||
|
graphicsMode = gMode
|
||||||
|
}
|
||||||
|
|
||||||
|
config.certificateVerifier = org.lib3270j.tls.TlsCertificateVerifier { chain, _, exception ->
|
||||||
|
if (!AppSettings.isVerifyCertsEnabled(getApplication())) {
|
||||||
|
log.info("Certificate verification bypassed via global AppSettings")
|
||||||
|
return@TlsCertificateVerifier true
|
||||||
|
}
|
||||||
|
log.info("Prompting user for untrusted certificate: $effectiveHost:$effectivePort")
|
||||||
|
val deferred = kotlinx.coroutines.CompletableDeferred<Boolean>()
|
||||||
|
_untrustedCertPrompt.value = UntrustedCertPromptState(
|
||||||
|
host = effectiveHost,
|
||||||
|
port = effectivePort,
|
||||||
|
chain = chain,
|
||||||
|
exception = exception,
|
||||||
|
deferred = deferred
|
||||||
|
)
|
||||||
|
val accepted = kotlinx.coroutines.runBlocking { deferred.await() }
|
||||||
|
_untrustedCertPrompt.value = null
|
||||||
|
accepted
|
||||||
}
|
}
|
||||||
|
|
||||||
val newClient = Telnet3270Client(config)
|
val newClient = Telnet3270Client(config)
|
||||||
@@ -236,8 +314,11 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
newClient.addConnectionListener(object : ConnectionListener {
|
newClient.addConnectionListener(object : ConnectionListener {
|
||||||
override fun onConnectionStateChanged(oldState: ConnectionState, newState: ConnectionState) {
|
override fun onConnectionStateChanged(oldState: ConnectionState, newState: ConnectionState) {
|
||||||
_connectionState.value = newState
|
_connectionState.value = newState
|
||||||
|
val tlsSuffix = if (effectiveTls) {
|
||||||
|
if (effectiveVerify) " [🔒 TLS]" else " [🔓 TLS/Unverified]"
|
||||||
|
} else ""
|
||||||
_oiaText.value = if (newState.isFullSession()) {
|
_oiaText.value = if (newState.isFullSession()) {
|
||||||
"3270 Connected ($host - $activeHostType)"
|
"3270 Connected ($effectiveHost - $activeHostType)$tlsSuffix"
|
||||||
} else if (newState.isHalfConnected()) {
|
} else if (newState.isHalfConnected()) {
|
||||||
"Connecting..."
|
"Connecting..."
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ object AppSettings {
|
|||||||
private const val KEY_CURSOR_BLINK = "cursor_blink"
|
private const val KEY_CURSOR_BLINK = "cursor_blink"
|
||||||
private const val KEY_HAPTIC_FEEDBACK = "haptic_feedback"
|
private const val KEY_HAPTIC_FEEDBACK = "haptic_feedback"
|
||||||
|
|
||||||
|
private const val KEY_VERIFY_CERTS = "verify_certs"
|
||||||
|
private const val KEY_DEFAULT_GRAPHICS_MODE = "default_graphics_mode"
|
||||||
|
|
||||||
private fun getPrefs(context: Context): SharedPreferences {
|
private fun getPrefs(context: Context): SharedPreferences {
|
||||||
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
}
|
}
|
||||||
@@ -36,4 +39,20 @@ object AppSettings {
|
|||||||
fun setHapticFeedbackEnabled(context: Context, enabled: Boolean) {
|
fun setHapticFeedbackEnabled(context: Context, enabled: Boolean) {
|
||||||
getPrefs(context).edit().putBoolean(KEY_HAPTIC_FEEDBACK, enabled).apply()
|
getPrefs(context).edit().putBoolean(KEY_HAPTIC_FEEDBACK, enabled).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isVerifyCertsEnabled(context: Context): Boolean {
|
||||||
|
return getPrefs(context).getBoolean(KEY_VERIFY_CERTS, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setVerifyCertsEnabled(context: Context, enabled: Boolean) {
|
||||||
|
getPrefs(context).edit().putBoolean(KEY_VERIFY_CERTS, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDefaultGraphicsMode(context: Context): String {
|
||||||
|
return getPrefs(context).getString(KEY_DEFAULT_GRAPHICS_MODE, "BOTH") ?: "BOTH"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDefaultGraphicsMode(context: Context, mode: String) {
|
||||||
|
getPrefs(context).edit().putString(KEY_DEFAULT_GRAPHICS_MODE, mode).apply()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ data class SavedHost(
|
|||||||
val model: Int = 2,
|
val model: Int = 2,
|
||||||
val luName: String = "",
|
val luName: String = "",
|
||||||
val autoConnect: Boolean = false,
|
val autoConnect: Boolean = false,
|
||||||
val hostType: String = "TSO"
|
val hostType: String = "TSO",
|
||||||
|
val useTls: Boolean = false,
|
||||||
|
val tlsVerifyCert: Boolean = true,
|
||||||
|
val graphicsMode: String = "BOTH"
|
||||||
) {
|
) {
|
||||||
fun toJson(): JSONObject {
|
fun toJson(): JSONObject {
|
||||||
return JSONObject().apply {
|
return JSONObject().apply {
|
||||||
@@ -26,6 +29,9 @@ data class SavedHost(
|
|||||||
put("luName", luName)
|
put("luName", luName)
|
||||||
put("autoConnect", autoConnect)
|
put("autoConnect", autoConnect)
|
||||||
put("hostType", hostType)
|
put("hostType", hostType)
|
||||||
|
put("useTls", useTls)
|
||||||
|
put("tlsVerifyCert", tlsVerifyCert)
|
||||||
|
put("graphicsMode", graphicsMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +45,10 @@ data class SavedHost(
|
|||||||
model = json.optInt("model", 2),
|
model = json.optInt("model", 2),
|
||||||
luName = json.optString("luName", ""),
|
luName = json.optString("luName", ""),
|
||||||
autoConnect = json.optBoolean("autoConnect", false),
|
autoConnect = json.optBoolean("autoConnect", false),
|
||||||
hostType = json.optString("hostType", "TSO")
|
hostType = json.optString("hostType", "TSO"),
|
||||||
|
useTls = json.optBoolean("useTls", false),
|
||||||
|
tlsVerifyCert = json.optBoolean("tlsVerifyCert", true),
|
||||||
|
graphicsMode = json.optString("graphicsMode", "BOTH")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import org.pubvm.a3270.storage.SavedHost
|
|||||||
@Composable
|
@Composable
|
||||||
fun ConnectDialog(
|
fun ConnectDialog(
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onConnect: (host: String, port: Int, model: Int, luName: String, hostType: String) -> Unit
|
onConnect: (host: String, port: Int, model: Int, luName: String, hostType: String, useTls: Boolean, tlsVerifyCert: Boolean, graphicsMode: String) -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var savedHosts by remember { mutableStateOf(HostStorage.getSavedHosts(context)) }
|
var savedHosts by remember { mutableStateOf(HostStorage.getSavedHosts(context)) }
|
||||||
@@ -38,6 +38,9 @@ fun ConnectDialog(
|
|||||||
var luName by remember { mutableStateOf("") }
|
var luName by remember { mutableStateOf("") }
|
||||||
var autoConnect by remember { mutableStateOf(false) }
|
var autoConnect by remember { mutableStateOf(false) }
|
||||||
var hostType by remember { mutableStateOf("TSO") }
|
var hostType by remember { mutableStateOf("TSO") }
|
||||||
|
var useTls by remember { mutableStateOf(false) }
|
||||||
|
var tlsVerifyCert by remember { mutableStateOf(true) }
|
||||||
|
var graphicsMode by remember { mutableStateOf("BOTH") }
|
||||||
|
|
||||||
fun loadProfile(saved: SavedHost) {
|
fun loadProfile(saved: SavedHost) {
|
||||||
selectedHostId = saved.id
|
selectedHostId = saved.id
|
||||||
@@ -48,6 +51,9 @@ fun ConnectDialog(
|
|||||||
luName = saved.luName
|
luName = saved.luName
|
||||||
autoConnect = saved.autoConnect
|
autoConnect = saved.autoConnect
|
||||||
hostType = saved.hostType
|
hostType = saved.hostType
|
||||||
|
useTls = saved.useTls
|
||||||
|
tlsVerifyCert = saved.tlsVerifyCert
|
||||||
|
graphicsMode = saved.graphicsMode
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearFields() {
|
fun clearFields() {
|
||||||
@@ -59,10 +65,13 @@ fun ConnectDialog(
|
|||||||
luName = ""
|
luName = ""
|
||||||
autoConnect = false
|
autoConnect = false
|
||||||
hostType = "TSO"
|
hostType = "TSO"
|
||||||
|
useTls = false
|
||||||
|
tlsVerifyCert = true
|
||||||
|
graphicsMode = "BOTH"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveCurrentProfile(): SavedHost? {
|
fun saveCurrentProfile(): SavedHost? {
|
||||||
val port = portStr.toIntOrNull() ?: 23
|
val port = portStr.toIntOrNull() ?: if (useTls) 992 else 23
|
||||||
if (host.isBlank()) return null
|
if (host.isBlank()) return null
|
||||||
val nameToSave = profileName.ifBlank { "${host.trim()}:$port" }
|
val nameToSave = profileName.ifBlank { "${host.trim()}:$port" }
|
||||||
val hostToSave = SavedHost(
|
val hostToSave = SavedHost(
|
||||||
@@ -73,7 +82,10 @@ fun ConnectDialog(
|
|||||||
model = modelNum,
|
model = modelNum,
|
||||||
luName = luName.trim(),
|
luName = luName.trim(),
|
||||||
autoConnect = autoConnect,
|
autoConnect = autoConnect,
|
||||||
hostType = hostType
|
hostType = hostType,
|
||||||
|
useTls = useTls,
|
||||||
|
tlsVerifyCert = tlsVerifyCert,
|
||||||
|
graphicsMode = graphicsMode
|
||||||
)
|
)
|
||||||
HostStorage.saveHost(context, hostToSave)
|
HostStorage.saveHost(context, hostToSave)
|
||||||
savedHosts = HostStorage.getSavedHosts(context)
|
savedHosts = HostStorage.getSavedHosts(context)
|
||||||
@@ -124,7 +136,7 @@ fun ConnectDialog(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.heightIn(max = 440.dp)
|
.heightIn(max = 460.dp)
|
||||||
) {
|
) {
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
Column(
|
Column(
|
||||||
@@ -142,6 +154,11 @@ fun ConnectDialog(
|
|||||||
) {
|
) {
|
||||||
savedHosts.forEach { saved ->
|
savedHosts.forEach { saved ->
|
||||||
val isSelected = (saved.id == selectedHostId)
|
val isSelected = (saved.id == selectedHostId)
|
||||||
|
val tlsTag = if (saved.useTls) {
|
||||||
|
if (saved.tlsVerifyCert) " [🔒 TLS]" else " [🔓 TLS/Unverified]"
|
||||||
|
} else ""
|
||||||
|
val gfxTag = if (saved.graphicsMode != "NONE") " [GFX: ${saved.graphicsMode}]" else ""
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(6.dp),
|
shape = RoundedCornerShape(6.dp),
|
||||||
color = if (isSelected) Color(0xFF2C3E50) else Color(0xFF25262B),
|
color = if (isSelected) Color(0xFF2C3E50) else Color(0xFF25262B),
|
||||||
@@ -165,8 +182,8 @@ fun ConnectDialog(
|
|||||||
color = Color.White
|
color = Color.White
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "${saved.host}:${saved.port} (M${saved.model} - ${saved.hostType})" +
|
text = "${saved.host}:${saved.port} (M${saved.model} - ${saved.hostType})$tlsTag$gfxTag" +
|
||||||
if (saved.autoConnect) " [Auto-Connect]" else "",
|
if (saved.autoConnect) " [Auto]" else "",
|
||||||
fontSize = 10.sp,
|
fontSize = 10.sp,
|
||||||
color = Color.LightGray
|
color = Color.LightGray
|
||||||
)
|
)
|
||||||
@@ -176,7 +193,16 @@ fun ConnectDialog(
|
|||||||
onClick = {
|
onClick = {
|
||||||
loadProfile(saved)
|
loadProfile(saved)
|
||||||
val hostToConn = saveCurrentProfile() ?: saved
|
val hostToConn = saveCurrentProfile() ?: saved
|
||||||
onConnect(hostToConn.host, hostToConn.port, hostToConn.model, hostToConn.luName, hostToConn.hostType)
|
onConnect(
|
||||||
|
hostToConn.host,
|
||||||
|
hostToConn.port,
|
||||||
|
hostToConn.model,
|
||||||
|
hostToConn.luName,
|
||||||
|
hostToConn.hostType,
|
||||||
|
hostToConn.useTls,
|
||||||
|
hostToConn.tlsVerifyCert,
|
||||||
|
hostToConn.graphicsMode
|
||||||
|
)
|
||||||
},
|
},
|
||||||
modifier = Modifier.size(28.dp)
|
modifier = Modifier.size(28.dp)
|
||||||
) {
|
) {
|
||||||
@@ -230,7 +256,7 @@ fun ConnectDialog(
|
|||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = portStr,
|
value = portStr,
|
||||||
onValueChange = { portStr = it },
|
onValueChange = { portStr = it },
|
||||||
label = { Text("Port (default 23)") },
|
label = { Text("Port (default 23 or 992)") },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
@@ -265,6 +291,74 @@ fun ConnectDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Graphics Mode
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text("Graphics:", fontSize = 11.sp, color = Color.Gray)
|
||||||
|
listOf(
|
||||||
|
"BOTH" to "Both",
|
||||||
|
"VECTOR_GRAPHICS" to "Vector",
|
||||||
|
"PROGRAMMED_SYMBOLS" to "Symbols",
|
||||||
|
"NONE" to "None"
|
||||||
|
).forEach { (modeKey, modeLabel) ->
|
||||||
|
FilterChip(
|
||||||
|
selected = (graphicsMode.equals(modeKey, ignoreCase = true)),
|
||||||
|
onClick = { graphicsMode = modeKey },
|
||||||
|
label = { Text(modeLabel, fontSize = 11.sp) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLS / SSL Checkbox
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
val newTls = !useTls
|
||||||
|
useTls = newTls
|
||||||
|
if (newTls && portStr.trim() == "23") {
|
||||||
|
portStr = "992"
|
||||||
|
} else if (!newTls && portStr.trim() == "992") {
|
||||||
|
portStr = "23"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = useTls,
|
||||||
|
onCheckedChange = { checked ->
|
||||||
|
useTls = checked
|
||||||
|
if (checked && portStr.trim() == "23") {
|
||||||
|
portStr = "992"
|
||||||
|
} else if (!checked && portStr.trim() == "992") {
|
||||||
|
portStr = "23"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text("Enable TLS / SSL Connection", fontSize = 12.sp, color = Color.White)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify Certificate Checkbox
|
||||||
|
if (useTls) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 16.dp)
|
||||||
|
.clickable { tlsVerifyCert = !tlsVerifyCert }
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = tlsVerifyCert,
|
||||||
|
onCheckedChange = { tlsVerifyCert = it }
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text("Verify Server Certificate", fontSize = 12.sp, color = if (tlsVerifyCert) Color.LightGray else Color(0xFFFFB450))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = luName,
|
value = luName,
|
||||||
onValueChange = { luName = it },
|
onValueChange = { luName = it },
|
||||||
@@ -309,7 +403,16 @@ fun ConnectDialog(
|
|||||||
onClick = {
|
onClick = {
|
||||||
val saved = saveCurrentProfile()
|
val saved = saveCurrentProfile()
|
||||||
if (saved != null) {
|
if (saved != null) {
|
||||||
onConnect(saved.host, saved.port, saved.model, saved.luName, saved.hostType)
|
onConnect(
|
||||||
|
saved.host,
|
||||||
|
saved.port,
|
||||||
|
saved.model,
|
||||||
|
saved.luName,
|
||||||
|
saved.hostType,
|
||||||
|
saved.useTls,
|
||||||
|
saved.tlsVerifyCert,
|
||||||
|
saved.graphicsMode
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ fun OiaStatusBar(
|
|||||||
cursorAddr: Int,
|
cursorAddr: Int,
|
||||||
rows: Int,
|
rows: Int,
|
||||||
cols: Int,
|
cols: Int,
|
||||||
|
isTls: Boolean = false,
|
||||||
|
isTlsVerified: Boolean = true,
|
||||||
|
graphicsMode: String = "NONE",
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val row = if (cols > 0) (cursorAddr / cols) + 1 else 1
|
val row = if (cols > 0) (cursorAddr / cols) + 1 else 1
|
||||||
@@ -76,6 +79,46 @@ fun OiaStatusBar(
|
|||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
maxLines = 1
|
maxLines = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TLS Badge
|
||||||
|
if (connectionState.isConnected() && isTls) {
|
||||||
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
Surface(
|
||||||
|
color = if (isTlsVerified) Color(0xFF51CF66).copy(alpha = 0.2f) else Color(0xFFFFB450).copy(alpha = 0.2f),
|
||||||
|
shape = RoundedCornerShape(4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (isTlsVerified) "🔒 TLS" else "🔓 TLS",
|
||||||
|
color = if (isTlsVerified) Color(0xFF51CF66) else Color(0xFFFFB450),
|
||||||
|
fontSize = 9.sp,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GFX Mode Badge
|
||||||
|
if (connectionState.isConnected() && graphicsMode != "NONE" && graphicsMode.isNotBlank()) {
|
||||||
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
Surface(
|
||||||
|
color = Color(0xFF22B8CF).copy(alpha = 0.2f),
|
||||||
|
shape = RoundedCornerShape(4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = when (graphicsMode) {
|
||||||
|
"VECTOR_GRAPHICS" -> "GOCA"
|
||||||
|
"PROGRAMMED_SYMBOLS" -> "PS"
|
||||||
|
else -> "GFX"
|
||||||
|
},
|
||||||
|
color = Color(0xFF22B8CF),
|
||||||
|
fontSize = 9.sp,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Terminal Keyboard Lock Status Badge (X SYSTEM vs READY)
|
// Terminal Keyboard Lock Status Badge (X SYSTEM vs READY)
|
||||||
|
|||||||
@@ -19,12 +19,16 @@ fun SettingsDialog(
|
|||||||
initialMaskHiddenInput: Boolean,
|
initialMaskHiddenInput: Boolean,
|
||||||
initialCursorBlink: Boolean,
|
initialCursorBlink: Boolean,
|
||||||
initialHapticFeedback: Boolean = true,
|
initialHapticFeedback: Boolean = true,
|
||||||
|
initialVerifyCerts: Boolean = true,
|
||||||
|
initialDefaultGraphicsMode: String = "BOTH",
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onSave: (maskHiddenInput: Boolean, cursorBlink: Boolean, hapticFeedback: Boolean) -> Unit
|
onSave: (maskHiddenInput: Boolean, cursorBlink: Boolean, hapticFeedback: Boolean, verifyCerts: Boolean, defaultGraphicsMode: String) -> Unit
|
||||||
) {
|
) {
|
||||||
var maskHiddenInput by remember { mutableStateOf(initialMaskHiddenInput) }
|
var maskHiddenInput by remember { mutableStateOf(initialMaskHiddenInput) }
|
||||||
var cursorBlink by remember { mutableStateOf(initialCursorBlink) }
|
var cursorBlink by remember { mutableStateOf(initialCursorBlink) }
|
||||||
var hapticFeedback by remember { mutableStateOf(initialHapticFeedback) }
|
var hapticFeedback by remember { mutableStateOf(initialHapticFeedback) }
|
||||||
|
var verifyCerts by remember { mutableStateOf(initialVerifyCerts) }
|
||||||
|
var defaultGraphicsMode by remember { mutableStateOf(initialDefaultGraphicsMode) }
|
||||||
|
|
||||||
Dialog(onDismissRequest = onDismiss) {
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
Card(
|
Card(
|
||||||
@@ -144,6 +148,76 @@ fun SettingsDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
HorizontalDivider(color = Color(0xFF2C2D30))
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Setting 4: Overall TLS Certificate Verification
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
|
Text(
|
||||||
|
text = "Verify TLS Certificates",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Enforce SSL/TLS certificate validation. Turn off to allow self-signed or unverified certificates without security prompts.",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color.LightGray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = verifyCerts,
|
||||||
|
onCheckedChange = { verifyCerts = it },
|
||||||
|
colors = SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = Color.White,
|
||||||
|
checkedTrackColor = Color(0xFF2B8A3E)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
HorizontalDivider(color = Color(0xFF2C2D30))
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Setting 5: Default Graphics Support Mode
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text(
|
||||||
|
text = "Default Graphics Mode",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Configure default vector graphics (GOCA) and Programmed Symbols (APL) mode.",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color.LightGray
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
listOf(
|
||||||
|
"BOTH" to "Both",
|
||||||
|
"VECTOR_GRAPHICS" to "Vector",
|
||||||
|
"PROGRAMMED_SYMBOLS" to "Symbols",
|
||||||
|
"NONE" to "None"
|
||||||
|
).forEach { (modeKey, modeLabel) ->
|
||||||
|
FilterChip(
|
||||||
|
selected = defaultGraphicsMode.equals(modeKey, ignoreCase = true),
|
||||||
|
onClick = { defaultGraphicsMode = modeKey },
|
||||||
|
label = { Text(modeLabel, fontSize = 11.sp) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
// Action Buttons
|
// Action Buttons
|
||||||
@@ -157,7 +231,7 @@ fun SettingsDialog(
|
|||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
onSave(maskHiddenInput, cursorBlink, hapticFeedback)
|
onSave(maskHiddenInput, cursorBlink, hapticFeedback, verifyCerts, defaultGraphicsMode)
|
||||||
},
|
},
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E))
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E))
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ fun TerminalView(
|
|||||||
cols: Int,
|
cols: Int,
|
||||||
cursorAddr: Int,
|
cursorAddr: Int,
|
||||||
screenVersion: Long,
|
screenVersion: Long,
|
||||||
|
programSymbolManager: org.lib3270j.graphics.ProgramSymbolManager? = null,
|
||||||
maskHiddenFields: Boolean = true,
|
maskHiddenFields: Boolean = true,
|
||||||
blinkCursor: Boolean = true,
|
blinkCursor: Boolean = true,
|
||||||
onTapAddress: (Int) -> Unit,
|
onTapAddress: (Int) -> Unit,
|
||||||
@@ -211,6 +212,8 @@ fun TerminalView(
|
|||||||
var isBold = false
|
var isBold = false
|
||||||
var isUnderline = false
|
var isUnderline = false
|
||||||
var isReverse = false
|
var isReverse = false
|
||||||
|
var csVal = 0
|
||||||
|
var ecVal = 0
|
||||||
|
|
||||||
if (buf != null) {
|
if (buf != null) {
|
||||||
val ea = buf.getCell(addr)
|
val ea = buf.getCell(addr)
|
||||||
@@ -225,6 +228,10 @@ fun TerminalView(
|
|||||||
fgColor = getFgColorForAttribute(ea, currentFieldEa, currentFA)
|
fgColor = getFgColorForAttribute(ea, currentFieldEa, currentFA)
|
||||||
bgColor = getBgColorForAttribute(ea, currentFieldEa)
|
bgColor = getBgColorForAttribute(ea, currentFieldEa)
|
||||||
|
|
||||||
|
csVal = if (ea.cs != 0.toByte()) (ea.cs.toInt() and 0xFF)
|
||||||
|
else (currentFieldEa?.cs?.toInt()?.and(0xFF) ?: 0)
|
||||||
|
ecVal = ea.ec.toInt() and 0xFF
|
||||||
|
|
||||||
// Intensity
|
// Intensity
|
||||||
if (faIsHigh(currentFA.toInt() and 0xFF)) {
|
if (faIsHigh(currentFA.toInt() and 0xFF)) {
|
||||||
isBold = true
|
isBold = true
|
||||||
@@ -290,7 +297,48 @@ fun TerminalView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (charVal != ' ') {
|
// Draw Programmed Symbol (PS / APL) if defined
|
||||||
|
var drawnAsPs = false
|
||||||
|
if (csVal >= 0x40 && programSymbolManager != null) {
|
||||||
|
val slot = programSymbolManager.getSymbol(csVal, ecVal)
|
||||||
|
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 bmp = android.graphics.Bitmap.createBitmap(rgbArray, symWidth, symHeight, android.graphics.Bitmap.Config.ARGB_8888)
|
||||||
|
drawContext.canvas.nativeCanvas.drawBitmap(
|
||||||
|
bmp,
|
||||||
|
null,
|
||||||
|
android.graphics.RectF(left, top, left + cellWidth, top + cellHeight),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
drawnAsPs = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!drawnAsPs && charVal != ' ') {
|
||||||
paint.color = fgColor.toArgb()
|
paint.color = fgColor.toArgb()
|
||||||
paint.isFakeBoldText = isBold
|
paint.isFakeBoldText = isBold
|
||||||
val fontMetrics = paint.fontMetrics
|
val fontMetrics = paint.fontMetrics
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package org.pubvm.a3270.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.security.cert.CertificateException
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UntrustedCertificateDialog(
|
||||||
|
host: String,
|
||||||
|
port: Int,
|
||||||
|
chain: Array<X509Certificate>?,
|
||||||
|
exception: CertificateException?,
|
||||||
|
onAccept: () -> Unit,
|
||||||
|
onReject: () -> Unit
|
||||||
|
) {
|
||||||
|
val cert = chain?.firstOrNull()
|
||||||
|
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss z", Locale.US)
|
||||||
|
|
||||||
|
val detailsText = buildString {
|
||||||
|
if (exception != null) {
|
||||||
|
append("Validation Error:\n ")
|
||||||
|
append(exception.localizedMessage ?: exception.message ?: exception.toString())
|
||||||
|
append("\n\n")
|
||||||
|
}
|
||||||
|
if (cert != null) {
|
||||||
|
append("Subject:\n ")
|
||||||
|
append(cert.subjectX500Principal.name)
|
||||||
|
append("\n\n")
|
||||||
|
|
||||||
|
append("Issuer:\n ")
|
||||||
|
append(cert.issuerX500Principal.name)
|
||||||
|
append("\n\n")
|
||||||
|
|
||||||
|
append("Validity:\n From: ")
|
||||||
|
append(sdf.format(cert.notBefore))
|
||||||
|
append("\n To: ")
|
||||||
|
append(sdf.format(cert.notAfter))
|
||||||
|
append("\n\n")
|
||||||
|
|
||||||
|
append("Serial Number:\n ")
|
||||||
|
append(cert.serialNumber.toString(16).uppercase(Locale.US))
|
||||||
|
append("\n\n")
|
||||||
|
|
||||||
|
append("SHA-256 Fingerprint:\n ")
|
||||||
|
append(computeFingerprint(cert, "SHA-256"))
|
||||||
|
append("\n\n")
|
||||||
|
|
||||||
|
append("SHA-1 Fingerprint:\n ")
|
||||||
|
append(computeFingerprint(cert, "SHA-1"))
|
||||||
|
} else {
|
||||||
|
append("No peer certificate information available.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onReject,
|
||||||
|
properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = false)
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = Color(0xFF1E1E1E),
|
||||||
|
tonalElevation = 6.dp,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.96f)
|
||||||
|
.wrapContentHeight()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
|
) {
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "⚠️",
|
||||||
|
fontSize = 24.sp
|
||||||
|
)
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = "Untrusted SSL/TLS Certificate",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFFFFB450)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "The server certificate for $host:$port could not be verified.",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color.LightGray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrollable Certificate Details Box
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(max = 280.dp)
|
||||||
|
.background(Color(0xFF121212), RoundedCornerShape(6.dp))
|
||||||
|
.border(1.dp, Color(0xFF333333), RoundedCornerShape(6.dp))
|
||||||
|
.padding(10.dp)
|
||||||
|
) {
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
Text(
|
||||||
|
text = detailsText,
|
||||||
|
color = Color(0xFFDCDCDC),
|
||||||
|
fontSize = 11.sp,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
lineHeight = 15.sp,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action Buttons
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
TextButton(onClick = onReject) {
|
||||||
|
Text("Cancel Connection", color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Button(
|
||||||
|
onClick = onAccept,
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFB46428))
|
||||||
|
) {
|
||||||
|
Text("Connect Anyway", color = Color.White, fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
"Unable to compute fingerprint: ${e.message}"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user