Files
a3270/src/main/java/haus/nightmare/a3270/ft/FileTransfer.kt
T
rudi f6d69bf0ff
Build and Test a3270 / Build Android APK (push) Successful in 6m54s
Refactor for consistency
2026-08-25 16:05:28 +00:00

249 lines
7.8 KiB
Kotlin

package haus.nightmare.a3270.ft
import android.os.Handler
import android.os.Looper
import haus.nightmare.lib3270j.Telnet3270Client
import haus.nightmare.lib3270j.ft.FTConfig
import haus.nightmare.lib3270j.ft.FTConstants.FTState
import haus.nightmare.lib3270j.ft.FTCut
import haus.nightmare.lib3270j.ft.FTDft
import java.io.File
import java.io.IOException
import java.util.Timer
import java.util.TimerTask
import java.util.logging.Logger
/**
* Android coordinator for IND$FILE file transfers.
* Manages the transfer state machine, timeouts, and coordinates between
* UI, Telnet3270Client, and lower-level CUT/DFT protocol handlers.
*/
class FileTransfer(
private val client: Telnet3270Client,
private val callback: FileTransferCallback
) : FTCut.FTCutListener, FTDft.FTDftListener {
private val log = Logger.getLogger(FileTransfer::class.java.name)
private val mainHandler = Handler(Looper.getMainLooper())
enum class FTMode {
UNKNOWN, CUT, DFT
}
interface FileTransferCallback {
fun onTransferStarted()
fun onTransferRunning()
fun onBytesTransferred(bytes: Long)
fun onTransferComplete(message: String)
fun onTransferAborted(error: String)
}
private var currentConfig: FTConfig? = null
private var localFile: File? = null
private var state = FTState.NONE
private var activeMode = FTMode.UNKNOWN
private var cutHandler: FTCut? = null
private var dftHandler: FTDft? = null
private var timeoutTimer: Timer? = null
private val startTimeoutMs = 30000L // 30 seconds
fun startTransfer(config: FTConfig): String? {
if (state != FTState.NONE) {
return "A transfer is already in progress."
}
val validationError = config.validate()
if (validationError != null) {
return validationError
}
this.currentConfig = config
this.localFile = File(config.localFilename)
this.activeMode = FTMode.UNKNOWN
// Check overwrite
if (config.isReceive && !config.isAppend && !config.isOverwrite) {
if (localFile?.exists() == true) {
return "Local file already exists and overwrite is not permitted."
}
}
// Initialize protocol handlers lazily
if (cutHandler == null) {
cutHandler = FTCut(
client.screenBuffer,
client.inputProcessor,
client.translator,
this
)
}
if (dftHandler == null) {
dftHandler = FTDft(
client.inputProcessor,
client.translator,
this
)
client.dataStreamProcessor.setFTDft(dftHandler)
}
// Build and type the IND$FILE command
val command = config.buildCommand()
log.info("Starting IND\$FILE transfer with command: $command")
// Erase field and verify it can hold the command
val capacity = client.inputProcessor.kybdPrime()
if (capacity < 0) {
cleanupHandlers(false)
return when (capacity) {
-1 -> "Keyboard is locked."
-3 -> "No unprotected input field found on screen."
else -> "Cannot start transfer from current screen state."
}
}
if (capacity < command.length) {
cleanupHandlers(false)
return "Current input field is too small for IND\$FILE command ($capacity chars max)."
}
setState(FTState.AWAIT_ACK)
client.emulateInput(command + "\n")
startTimeout()
mainHandler.post { callback.onTransferStarted() }
return null
}
fun cancel() {
if (state == FTState.RUNNING || state == FTState.AWAIT_ACK) {
log.info("User cancelled transfer")
setState(FTState.ABORT_WAIT)
} else if (state != FTState.NONE) {
log.info("Forcing cancel from state $state")
completeTransfer("Transfer cancelled.")
mainHandler.post { callback.onTransferAborted("Cancelled by user.") }
}
}
fun reset() {
log.info("Force resetting FileTransfer state from $state")
completeTransfer("Reset")
}
fun isTransferActive(): Boolean = state != FTState.NONE
fun getActiveMode(): FTMode = activeMode
fun onScreenUpdated() {
if ((activeMode == FTMode.CUT || activeMode == FTMode.UNKNOWN) &&
(state == FTState.AWAIT_ACK || state == FTState.RUNNING || state == FTState.ABORT_WAIT)) {
cutHandler?.processScreenUpdate()
}
}
private fun startTimeout() {
cancelTimeout()
timeoutTimer = Timer("FTTimeout", true).apply {
schedule(object : TimerTask() {
override fun run() {
mainHandler.post {
if (state == FTState.AWAIT_ACK) {
log.warning("Transfer start timeout")
completeTransfer("Transfer failed to start within 30 seconds.")
callback.onTransferAborted("Transfer start timeout.")
}
}
}
}, startTimeoutMs)
}
}
private fun cancelTimeout() {
timeoutTimer?.cancel()
timeoutTimer = null
}
private fun cleanupHandlers(success: Boolean) {
cutHandler?.cleanup()
dftHandler?.cleanup()
if (!success && currentConfig != null && currentConfig?.isReceive == true && !currentConfig!!.isAppend) {
if (state != FTState.NONE && state != FTState.AWAIT_ACK && localFile != null && localFile!!.exists()) {
log.info("Cleaning up incomplete download: ${localFile!!.absolutePath}")
localFile!!.delete()
}
}
}
private fun completeTransfer(errorMessage: String?) {
cancelTimeout()
val success = (errorMessage == null)
cleanupHandlers(success)
setState(FTState.NONE)
activeMode = FTMode.UNKNOWN
currentConfig = null
}
override fun onCutRunning() {
handleTransferRunning(FTMode.CUT)
}
override fun onDftRunning() {
handleTransferRunning(FTMode.DFT)
}
private fun handleTransferRunning(mode: FTMode) {
if (activeMode == FTMode.UNKNOWN) {
activeMode = mode
log.info("FT mode established: $activeMode")
try {
if (activeMode == FTMode.DFT) {
dftHandler?.initTransfer(localFile)
} else {
cutHandler?.initTransfer(localFile)
}
} catch (e: IOException) {
log.warning("Failed to open local file for $activeMode: ${e.message}")
onTransferAborted("Failed to open local file: ${e.message}")
return
}
}
cancelTimeout()
setState(FTState.RUNNING)
mainHandler.post { callback.onTransferRunning() }
}
override fun onTransferComplete(errorMessage: String?) {
completeTransfer(errorMessage)
mainHandler.post {
if (errorMessage == null) {
callback.onTransferComplete("File transfer complete.")
} else {
callback.onTransferAborted(errorMessage)
}
}
}
override fun onTransferAborted(errorMessage: String?) {
completeTransfer(errorMessage)
mainHandler.post { callback.onTransferAborted(errorMessage ?: "Transfer aborted") }
}
override fun onBytesTransferred(bytes: Long) {
mainHandler.post { callback.onBytesTransferred(bytes) }
}
override fun getCurrentState(): FTState = state
override fun setState(state: FTState) {
this.state = state
}
override fun getConfig(): FTConfig? = currentConfig
override fun getLocalFile(): File? = localFile
}