249 lines
7.8 KiB
Kotlin
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
|
|
}
|