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 }