Update Status/FileTransfer
Build and Test a3270 / Build Android APK (push) Successful in 3m39s

This commit is contained in:
2026-09-06 09:30:26 -04:00
parent da289730db
commit 0eaab32e2f
6 changed files with 92 additions and 26 deletions
+6
View File
@@ -41,6 +41,12 @@ jobs:
run: | run: |
chmod +x ./gradlew ./build.sh chmod +x ./gradlew ./build.sh
- name: Run Unit Tests
run: |
./gradlew testDebugUnitTest --no-daemon
env:
J3270_DIR: ${{ github.workspace }}/j3270
- name: Build Android APK - name: Build Android APK
run: | run: |
./gradlew assembleDebug --no-daemon ./gradlew assembleDebug --no-daemon
@@ -814,6 +814,7 @@ fun MainScreen(
inhibitReason = viewModel.getClient()?.oia?.inputInhibited ?: 0, inhibitReason = viewModel.getClient()?.oia?.inputInhibited ?: 0,
luName = viewModel.getClient()?.telnetFSM?.connectedLu ?: "", luName = viewModel.getClient()?.telnetFSM?.connectedLu ?: "",
isNumericField = isNumericField, isNumericField = isNumericField,
uiTheme = uiTheme,
onToggleDocMode = { viewModel.toggleDocMode() }, onToggleDocMode = { viewModel.toggleDocMode() },
onToggleWordWrap = { viewModel.toggleWordWrap() }, onToggleWordWrap = { viewModel.toggleWordWrap() },
onToggleAplMode = { viewModel.toggleAplMode() }, onToggleAplMode = { viewModel.toggleAplMode() },
@@ -51,6 +51,42 @@ data class LastTransferState(
) )
private var lastTransferState: LastTransferState? = null private var lastTransferState: LastTransferState? = null
fun formatHostFilename(localName: String, hostType: FTConfig.HostType): String {
if (hostType == FTConfig.HostType.CMS) {
val lastDot = localName.lastIndexOf('.')
return if (lastDot > 0) {
val fn = localName.substring(0, lastDot).replace(Regex("[^a-zA-Z0-9]"), "").uppercase()
val ft = localName.substring(lastDot + 1).replace(Regex("[^a-zA-Z0-9]"), "").uppercase()
"${fn.ifBlank { "FILE" }} ${ft.ifBlank { "TEXT" }} A"
} else {
val clean = localName.replace(Regex("[^a-zA-Z0-9]"), "").uppercase()
"${clean.ifBlank { "FILE" }} FILE A"
}
} else if (hostType == FTConfig.HostType.TSO) {
val cleanBase = localName.substringBeforeLast('.').replace(Regex("[^a-zA-Z0-9]"), "").uppercase()
return "'${cleanBase.ifBlank { "DATA" }}'"
}
return localName
}
fun formatLocalFilenameFromHost(hostFile: String, hostType: FTConfig.HostType): String {
val cleanName = hostFile.trim('\'', '"')
if (hostType == FTConfig.HostType.CMS) {
val tokens = cleanName.trim().split(Regex("\\s+"))
if (tokens.size >= 2) {
return "${tokens[0].lowercase()}.${tokens[1].lowercase()}"
} else if (tokens.size == 1 && tokens[0].isNotBlank()) {
return "${tokens[0].lowercase()}.txt"
}
} else {
val lastDot = cleanName.lastIndexOf('.')
if (lastDot > 0) {
return "${cleanName.substring(lastDot + 1).lowercase()}.txt"
}
}
return if (cleanName.isNotBlank()) "${cleanName.lowercase()}.txt" else "download.txt"
}
@Composable @Composable
fun FileTransferDialog( fun FileTransferDialog(
initialHostType: String = "TSO", initialHostType: String = "TSO",
@@ -102,8 +138,7 @@ fun FileTransferDialog(
localFile = tempFile.absolutePath localFile = tempFile.absolutePath
localDisplayName = displayName localDisplayName = displayName
if (hostFile.isBlank()) { if (hostFile.isBlank()) {
val cleanBase = displayName.substringBeforeLast('.').replace(Regex("[^a-zA-Z0-9]"), "").uppercase() hostFile = formatHostFilename(displayName, hostType)
hostFile = if (hostType == FTConfig.HostType.TSO) "'$cleanBase'" else "$cleanBase FILE A"
} }
Toast.makeText(context, "Selected: $displayName", Toast.LENGTH_SHORT).show() Toast.makeText(context, "Selected: $displayName", Toast.LENGTH_SHORT).show()
} catch (e: Exception) { } catch (e: Exception) {
@@ -159,8 +194,7 @@ fun FileTransferDialog(
onSelectDataset = { selected -> onSelectDataset = { selected ->
hostFile = selected hostFile = selected
if (localFile.isBlank()) { if (localFile.isBlank()) {
val base = selected.trim('\'').substringAfterLast('.').substringAfterLast(' ') val safeName = formatLocalFilenameFromHost(selected, hostType)
val safeName = if (base.isNotBlank()) "$base.txt" else "download.txt"
val tempDir = File(context.cacheDir, "ft_download").apply { if (!exists()) mkdirs() } val tempDir = File(context.cacheDir, "ft_download").apply { if (!exists()) mkdirs() }
val tempFile = File(tempDir, safeName) val tempFile = File(tempDir, safeName)
localFile = tempFile.absolutePath localFile = tempFile.absolutePath
@@ -377,8 +411,7 @@ fun FileTransferDialog(
onClick = { onClick = {
if (isReceive) { if (isReceive) {
val defName = if (hostFile.isNotBlank()) { val defName = if (hostFile.isNotBlank()) {
val base = hostFile.trim('\'').substringAfterLast('.').substringAfterLast(' ') formatLocalFilenameFromHost(hostFile, hostType)
if (base.isNotBlank()) "$base.txt" else "download.txt"
} else "download.txt" } else "download.txt"
createDocumentLauncher.launch(defName) createDocumentLauncher.launch(defName)
} else { } else {
@@ -40,8 +40,17 @@ fun OiaStatusBar(
inhibitReason: Int = 0, inhibitReason: Int = 0,
luName: String = "", luName: String = "",
isNumericField: Boolean = false, isNumericField: Boolean = false,
uiTheme: String = "DARK",
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val isLight = uiTheme.equals("LIGHT", ignoreCase = true)
val barBg = if (isLight) Color(0xFFE9ECEF) else Color(0xFF161719)
val defaultTextClr = if (isLight) Color(0xFF212529) else Color.White
val sysAvailClr = if (isLight) Color(0xFF005AC8) else Color(0xFF7890F0)
val inputInhibitedClr = if (isLight) Color(0xFF141414) else Color(0xFFFFFFFF)
val attentionClr = if (isLight) Color(0xFFB46400) else Color(0xFFFFFF00)
val commCheckClr = if (isLight) Color(0xFFBE1414) else Color(0xFFFF0000)
val row = if (cols > 0 && rows > 0) ((cursorAddr / cols) % rows) + 1 else 1 val row = if (cols > 0 && rows > 0) ((cursorAddr / cols) % rows) + 1 else 1
val col = if (cols > 0) (cursorAddr % cols) + 1 else 1 val col = if (cols > 0) (cursorAddr % cols) + 1 else 1
val posStr = String.format("%03d/%03d [%04d]", row, col, cursorAddr) val posStr = String.format("%03d/%03d [%04d]", row, col, cursorAddr)
@@ -72,22 +81,24 @@ fun OiaStatusBar(
else -> "X LOCKED" else -> "X LOCKED"
} }
val clr = when (inhibitReason) { val clr = when (inhibitReason) {
haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_COMM_CHECK -> Color(0xFFFF0000) haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_COMM_CHECK -> commCheckClr
haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_OPERATOR_DUE -> Color(0xFFFFFF00) haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_OPERATOR_DUE -> attentionClr
else -> Color(0xFFFFFFFF) haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_NUMERIC_ONLY -> attentionClr
haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_OVERFLOW -> attentionClr
else -> inputInhibitedClr
} }
txt to clr txt to clr
} else if (isKeyboardLocked) { } else if (isKeyboardLocked) {
"X SYSTEM" to Color(0xFFFFFFFF) "X SYSTEM" to inputInhibitedClr
} else { } else {
"READY" to Color(0xFF7890F0) "READY" to sysAvailClr
} }
Row( Row(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.height(26.dp) .height(26.dp)
.background(Color(0xFF161719)) .background(barBg)
.padding(horizontal = 8.dp), .padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
@@ -105,7 +116,7 @@ fun OiaStatusBar(
Spacer(modifier = Modifier.width(6.dp)) Spacer(modifier = Modifier.width(6.dp))
Text( Text(
text = hostText, text = hostText,
color = Color.White, color = defaultTextClr,
fontSize = 11.sp, fontSize = 11.sp,
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
@@ -316,7 +327,7 @@ fun OiaStatusBar(
// Cursor Position & Buffer Address // Cursor Position & Buffer Address
Text( Text(
text = posStr, text = posStr,
color = Color(0xFF22B8CF), color = if (isLight) Color(0xFF0C8599) else Color(0xFF22B8CF),
fontSize = 10.sp, fontSize = 10.sp,
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
@@ -257,18 +257,17 @@ fun TerminalView(
if (graphicsPlane != null && graphicsPlane.hasContent()) { if (graphicsPlane != null && graphicsPlane.hasContent()) {
val gridW = metrics.gridWidth.toInt() val gridW = metrics.gridWidth.toInt()
val gridH = metrics.gridHeight.toInt() val gridH = metrics.gridHeight.toInt()
if (gridW > 0 && gridH > 0) { val gWidth = graphicsPlane.canvasWidth
graphicsPlane.resize(gridW, gridH) val gHeight = graphicsPlane.canvasHeight
val rgb = graphicsPlane.rgbBuffer val rgb = graphicsPlane.rgbBuffer
if (rgb != null) { if (gridW > 0 && gridH > 0 && rgb != null && gWidth > 0 && gHeight > 0) {
val bmp = android.graphics.Bitmap.createBitmap(rgb, gridW, gridH, android.graphics.Bitmap.Config.ARGB_8888) val bmp = android.graphics.Bitmap.createBitmap(rgb, gWidth, gHeight, android.graphics.Bitmap.Config.ARGB_8888)
drawContext.canvas.nativeCanvas.drawBitmap( drawContext.canvas.nativeCanvas.drawBitmap(
bmp, bmp,
null, null,
android.graphics.RectF(offsetX, offsetY, offsetX + gridW.toFloat(), offsetY + gridH.toFloat()), android.graphics.RectF(offsetX, offsetY, offsetX + gridW.toFloat(), offsetY + gridH.toFloat()),
null null
) )
}
} }
} }
@@ -236,4 +236,20 @@ class EntryAssistAndModesTest {
assertEquals("false", parsed["modes.numericlock"]) assertEquals("false", parsed["modes.numericlock"])
assertEquals("true", parsed["modes.autoskip"]) assertEquals("true", parsed["modes.autoskip"])
} }
@Test
fun testCmsFilenameFormatting() {
// Upload (local -> host)
assertEquals("PROFILE EXEC A", haus.nightmare.a3270.ui.formatHostFilename("profile.exec", haus.nightmare.lib3270j.ft.FTConfig.HostType.CMS))
assertEquals("TEST TXT A", haus.nightmare.a3270.ui.formatHostFilename("test.txt", haus.nightmare.lib3270j.ft.FTConfig.HostType.CMS))
assertEquals("README FILE A", haus.nightmare.a3270.ui.formatHostFilename("README", haus.nightmare.lib3270j.ft.FTConfig.HostType.CMS))
// Download (host -> local)
assertEquals("profile.exec", haus.nightmare.a3270.ui.formatLocalFilenameFromHost("PROFILE EXEC A", haus.nightmare.lib3270j.ft.FTConfig.HostType.CMS))
assertEquals("test.data", haus.nightmare.a3270.ui.formatLocalFilenameFromHost("TEST DATA A1", haus.nightmare.lib3270j.ft.FTConfig.HostType.CMS))
// TSO
assertEquals("'MYDATA'", haus.nightmare.a3270.ui.formatHostFilename("mydata.txt", haus.nightmare.lib3270j.ft.FTConfig.HostType.TSO))
assertEquals("dataset.txt", haus.nightmare.a3270.ui.formatLocalFilenameFromHost("'USER.DATASET'", haus.nightmare.lib3270j.ft.FTConfig.HostType.TSO))
}
} }