diff --git a/build.gradle b/build.gradle index 2d47810..a58e628 100644 --- a/build.gradle +++ b/build.gradle @@ -69,4 +69,7 @@ dependencies { implementation 'androidx.compose.ui:ui-tooling-preview' implementation 'androidx.compose.material3:material3' implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0' + + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3' } diff --git a/src/main/java/org/pubvm/a3270/MainActivity.kt b/src/main/java/org/pubvm/a3270/MainActivity.kt index d7347a5..3afd959 100644 --- a/src/main/java/org/pubvm/a3270/MainActivity.kt +++ b/src/main/java/org/pubvm/a3270/MainActivity.kt @@ -21,7 +21,9 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.core.view.WindowCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver -import org.lib3270j.protocol.DS3270Constants.AID_ENTER +import android.content.Context +import android.view.KeyCharacterMap +import org.lib3270j.protocol.DS3270Constants.* import org.pubvm.a3270.service.TerminalService import org.pubvm.a3270.storage.HostStorage import org.pubvm.a3270.ui.ConnectDialog @@ -97,68 +99,280 @@ class MainActivity : ComponentActivity() { } } + private val log = java.util.logging.Logger.getLogger("a3270-MainActivity") + private var deadKey: Int = 0 + + private fun getEffectiveUnicodeChar(event: KeyEvent): Int { + val cleanMeta = event.metaState and ( + KeyEvent.META_SHIFT_ON or + KeyEvent.META_SHIFT_LEFT_ON or + KeyEvent.META_SHIFT_RIGHT_ON or + KeyEvent.META_CAPS_LOCK_ON + ) + + // 1. Try getUnicodeChar with cleanMeta + var u = event.getUnicodeChar(cleanMeta) + if (u > 0) return u + + // 2. Try unicodeChar property + u = event.unicodeChar + if (u > 0) return u + + // 3. Try KeyCharacterMap with cleanMeta + val kcm = event.keyCharacterMap ?: KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD) + u = kcm.get(event.keyCode, cleanMeta) + if (u > 0) return u + + // 4. Try KeyCharacterMap with meta = 0 + u = kcm.get(event.keyCode, 0) + if (u > 0) { + val shift = event.isShiftPressed || (event.metaState and KeyEvent.META_SHIFT_ON != 0) + if (shift) { + val c = u.toChar() + if (c.isLowerCase()) return c.uppercaseChar().code + } + return u + } + + // 5. Try displayLabel as fallback + val label = event.displayLabel + if (label >= ' ') { + val shift = event.isShiftPressed || (event.metaState and KeyEvent.META_SHIFT_ON != 0) + return if (shift) label.uppercaseChar().code else label.lowercaseChar().code + } + + return 0 + } + override fun dispatchKeyEvent(event: KeyEvent): Boolean { val shift = event.isShiftPressed || (event.metaState and KeyEvent.META_SHIFT_ON != 0) if (isShiftPressedState.value != shift) { isShiftPressedState.value = shift } + // Handle string / multi-character input (e.g. adb input text, barcode scanners, fast text input) + if (event.action == KeyEvent.ACTION_MULTIPLE) { + if (event.keyCode == KeyEvent.KEYCODE_UNKNOWN) { + val chars = event.characters + if (!chars.isNullOrEmpty()) { + viewModel.typeString(chars) + return true + } + } else { + val unicode = getEffectiveUnicodeChar(event) + if (unicode >= 32) { + val count = maxOf(1, event.repeatCount) + viewModel.typeString(unicode.toChar().toString().repeat(count)) + return true + } + } + } + if (event.action == KeyEvent.ACTION_DOWN) { val keyCode = event.keyCode + val ctrl = event.isCtrlPressed || (event.metaState and KeyEvent.META_CTRL_ON != 0) + val alt = event.isAltPressed || (event.metaState and KeyEvent.META_ALT_ON != 0) + + // 1. Function Keys (F1..F12 -> PF1..PF12 or PF13..PF24 with Shift or Alt) if (keyCode in KeyEvent.KEYCODE_F1..KeyEvent.KEYCODE_F12) { val fNum = keyCode - KeyEvent.KEYCODE_F1 + 1 - val pfNum = if (shift) fNum + 12 else fNum + val pfNum = if (shift || alt) fNum + 12 else fNum val pfAids = intArrayOf( - org.lib3270j.protocol.DS3270Constants.AID_PF1, org.lib3270j.protocol.DS3270Constants.AID_PF2, - org.lib3270j.protocol.DS3270Constants.AID_PF3, org.lib3270j.protocol.DS3270Constants.AID_PF4, - org.lib3270j.protocol.DS3270Constants.AID_PF5, org.lib3270j.protocol.DS3270Constants.AID_PF6, - org.lib3270j.protocol.DS3270Constants.AID_PF7, org.lib3270j.protocol.DS3270Constants.AID_PF8, - org.lib3270j.protocol.DS3270Constants.AID_PF9, org.lib3270j.protocol.DS3270Constants.AID_PF10, - org.lib3270j.protocol.DS3270Constants.AID_PF11, org.lib3270j.protocol.DS3270Constants.AID_PF12, - org.lib3270j.protocol.DS3270Constants.AID_PF13, org.lib3270j.protocol.DS3270Constants.AID_PF14, - org.lib3270j.protocol.DS3270Constants.AID_PF15, org.lib3270j.protocol.DS3270Constants.AID_PF16, - org.lib3270j.protocol.DS3270Constants.AID_PF17, org.lib3270j.protocol.DS3270Constants.AID_PF18, - org.lib3270j.protocol.DS3270Constants.AID_PF19, org.lib3270j.protocol.DS3270Constants.AID_PF20, - org.lib3270j.protocol.DS3270Constants.AID_PF21, org.lib3270j.protocol.DS3270Constants.AID_PF22, - org.lib3270j.protocol.DS3270Constants.AID_PF23, org.lib3270j.protocol.DS3270Constants.AID_PF24 + AID_PF1, AID_PF2, AID_PF3, AID_PF4, AID_PF5, AID_PF6, + AID_PF7, AID_PF8, AID_PF9, AID_PF10, AID_PF11, AID_PF12, + AID_PF13, AID_PF14, AID_PF15, AID_PF16, AID_PF17, AID_PF18, + AID_PF19, AID_PF20, AID_PF21, AID_PF22, AID_PF23, AID_PF24 ) viewModel.sendAid(pfAids[pfNum - 1]) if (shift) { isShiftPressedState.value = false } return true - } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { - viewModel.cursorLeft() - return true - } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { - viewModel.cursorUp() - return true - } else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { - viewModel.cursorDown() - return true - } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { - viewModel.cursorRight() - return true - } else if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER) { - viewModel.sendAid(AID_ENTER) - return true - } else if (keyCode == KeyEvent.KEYCODE_TAB) { - if (event.isShiftPressed) { - viewModel.backTab() - } else { - viewModel.tab() + } + + // 2. Ctrl Shortcuts + if (ctrl) { + when (keyCode) { + KeyEvent.KEYCODE_V -> { + pasteFromClipboard() + return true + } + KeyEvent.KEYCODE_R -> { + viewModel.resetKeyboard() + return true + } + KeyEvent.KEYCODE_L -> { + viewModel.sendAid(AID_CLEAR) + return true + } + KeyEvent.KEYCODE_Z -> { + viewModel.sendAid(AID_PA1) + return true + } + KeyEvent.KEYCODE_HOME -> { + viewModel.cursorHome() + return true + } + KeyEvent.KEYCODE_MOVE_END -> { + viewModel.eraseEof() + return true + } } - return true - } else if (keyCode == KeyEvent.KEYCODE_DEL) { - viewModel.backspace() - return true - } else if (keyCode == KeyEvent.KEYCODE_ESCAPE) { - viewModel.resetKeyboard() + } + + // 3. Alt Shortcuts (PA keys, Clear, Reset) + if (alt) { + when (keyCode) { + KeyEvent.KEYCODE_1 -> { + viewModel.sendAid(AID_PA1) + return true + } + KeyEvent.KEYCODE_2 -> { + viewModel.sendAid(AID_PA2) + return true + } + KeyEvent.KEYCODE_3 -> { + viewModel.sendAid(AID_PA3) + return true + } + KeyEvent.KEYCODE_C -> { + viewModel.sendAid(AID_CLEAR) + return true + } + KeyEvent.KEYCODE_R -> { + viewModel.resetKeyboard() + return true + } + } + } + + // 4. Shift Shortcuts + if (shift && keyCode == KeyEvent.KEYCODE_INSERT) { + pasteFromClipboard() return true } + + // 5. Standard Navigation & Special Keys + when (keyCode) { + KeyEvent.KEYCODE_DPAD_LEFT -> { + viewModel.cursorLeft() + return true + } + KeyEvent.KEYCODE_DPAD_UP -> { + viewModel.cursorUp() + return true + } + KeyEvent.KEYCODE_DPAD_DOWN -> { + viewModel.cursorDown() + return true + } + KeyEvent.KEYCODE_DPAD_RIGHT -> { + viewModel.cursorRight() + return true + } + KeyEvent.KEYCODE_MOVE_HOME -> { + viewModel.cursorHome() + return true + } + KeyEvent.KEYCODE_MOVE_END -> { + viewModel.eraseEof() + return true + } + KeyEvent.KEYCODE_PAGE_UP -> { + viewModel.sendAid(AID_PF7) + return true + } + KeyEvent.KEYCODE_PAGE_DOWN -> { + viewModel.sendAid(AID_PF8) + return true + } + KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER -> { + viewModel.sendAid(AID_ENTER) + return true + } + KeyEvent.KEYCODE_TAB -> { + if (shift) { + viewModel.backTab() + isShiftPressedState.value = false + } else { + viewModel.tab() + } + return true + } + KeyEvent.KEYCODE_DEL -> { + viewModel.backspace() + return true + } + KeyEvent.KEYCODE_FORWARD_DEL -> { + viewModel.deleteChar() + return true + } + KeyEvent.KEYCODE_ESCAPE -> { + if (shift) { + viewModel.sendAid(AID_CLEAR) + isShiftPressedState.value = false + } else { + viewModel.resetKeyboard() + } + return true + } + KeyEvent.KEYCODE_BREAK -> { + viewModel.sendAid(AID_PA1) + return true + } + } + + // 6. Alphanumeric & Symbol Character Input + val unicode = getEffectiveUnicodeChar(event) + log.info("dispatchKeyEvent DOWN: keyCode=$keyCode, metaState=${event.metaState}, unicode=$unicode, char='${if (unicode > 0) unicode.toChar() else ' '}'") + if (unicode > 0) { + if ((unicode and KeyCharacterMap.COMBINING_ACCENT) != 0) { + deadKey = unicode and KeyCharacterMap.COMBINING_ACCENT_MASK + return true + } + val charToType = if (deadKey != 0) { + val combined = KeyCharacterMap.getDeadChar(deadKey, unicode) + deadKey = 0 + if (combined != 0) combined.toChar() else unicode.toChar() + } else { + unicode.toChar() + } + if (charToType >= ' ') { + log.info("dispatchKeyEvent typing char: '$charToType'") + viewModel.typeString(charToType.toString()) + return true + } + } else { + log.info("dispatchKeyEvent unhandled DOWN keyCode=$keyCode, metaState=${event.metaState}") + } } + + if (event.action == KeyEvent.ACTION_UP) { + when (event.keyCode) { + KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_SHIFT_RIGHT, + KeyEvent.KEYCODE_ALT_LEFT, KeyEvent.KEYCODE_ALT_RIGHT, + KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.KEYCODE_CTRL_RIGHT -> { + // Let super handle modifier tracking + } + else -> { + return true + } + } + } + return super.dispatchKeyEvent(event) } + + private fun pasteFromClipboard() { + val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as? android.content.ClipboardManager + val clip = clipboard?.primaryClip + if (clip != null && clip.itemCount > 0) { + val text = clip.getItemAt(0)?.coerceToText(this)?.toString() + if (!text.isNullOrEmpty()) { + viewModel.pasteString(text) + } + } + } } @Composable diff --git a/src/main/java/org/pubvm/a3270/TerminalViewModel.kt b/src/main/java/org/pubvm/a3270/TerminalViewModel.kt index 4f1afba..87e9d2e 100644 --- a/src/main/java/org/pubvm/a3270/TerminalViewModel.kt +++ b/src/main/java/org/pubvm/a3270/TerminalViewModel.kt @@ -32,12 +32,17 @@ sealed interface TerminalInputAction { data class TypeText(val text: String) : TerminalInputAction data class SendAid(val aidCode: Int) : TerminalInputAction data object Backspace : TerminalInputAction + data object DeleteChar : TerminalInputAction data object Tab : TerminalInputAction data object BackTab : TerminalInputAction data object CursorLeft : TerminalInputAction data object CursorRight : TerminalInputAction data object CursorUp : TerminalInputAction data object CursorDown : TerminalInputAction + data object CursorHome : TerminalInputAction + data object EraseEof : TerminalInputAction + data object EraseInput : TerminalInputAction + data object Newline : TerminalInputAction data object Reset : TerminalInputAction data class SetCursor(val baddr: Int) : TerminalInputAction } @@ -124,6 +129,7 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application private val _ftState = MutableStateFlow(FTProgressState()) val ftState: StateFlow = _ftState.asStateFlow() + @Volatile private var client: Telnet3270Client? = null var currentHost: String = "" private set @@ -162,6 +168,7 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application try { when (action) { is TerminalInputAction.TypeText -> { + log.info("TypeText: text='${action.text}', curAddr=${buf.cursorAddress}, formatted=${buf.isFormatted}") ip.isKeyboardLocked = false for (ch in action.text) { if (ch == '\n' || ch == '\r') { @@ -177,6 +184,7 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application } } ip.typeCharacter(ch) + log.info("After typeCharacter('$ch'): newAddr=${buf.cursorAddress}, cellChar='${buf.getCell(curAddr).ucs4}'") } } } @@ -188,6 +196,10 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application ip.isKeyboardLocked = false ip.backspace() } + is TerminalInputAction.DeleteChar -> { + ip.isKeyboardLocked = false + ip.deleteChar() + } is TerminalInputAction.Tab -> { ip.tab() } @@ -206,6 +218,20 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application is TerminalInputAction.CursorDown -> { ip.cursorDown() } + is TerminalInputAction.CursorHome -> { + ip.cursorHome() + } + is TerminalInputAction.EraseEof -> { + ip.isKeyboardLocked = false + ip.eraseEof() + } + is TerminalInputAction.EraseInput -> { + ip.isKeyboardLocked = false + ip.eraseInput() + } + is TerminalInputAction.Newline -> { + ip.newline() + } is TerminalInputAction.Reset -> { ip.reset() } @@ -545,6 +571,10 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application inputChannel.trySend(TerminalInputAction.Backspace) } + fun deleteChar() { + inputChannel.trySend(TerminalInputAction.DeleteChar) + } + fun tab() { inputChannel.trySend(TerminalInputAction.Tab) } @@ -557,6 +587,18 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application inputChannel.trySend(TerminalInputAction.Reset) } + fun eraseEof() { + inputChannel.trySend(TerminalInputAction.EraseEof) + } + + fun eraseInput() { + inputChannel.trySend(TerminalInputAction.EraseInput) + } + + fun newline() { + inputChannel.trySend(TerminalInputAction.Newline) + } + fun sendAid(aidCode: Int) { inputChannel.trySend(TerminalInputAction.SendAid(aidCode)) } @@ -581,6 +623,10 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application inputChannel.trySend(TerminalInputAction.CursorRight) } + fun cursorHome() { + inputChannel.trySend(TerminalInputAction.CursorHome) + } + private fun extractScreenSnippet(buf: ScreenBuffer?): String { if (buf == null) return "Mainframe screen update received" val rows = buf.rows diff --git a/src/main/java/org/pubvm/a3270/ui/TerminalView.kt b/src/main/java/org/pubvm/a3270/ui/TerminalView.kt index 305255c..97a33ae 100644 --- a/src/main/java/org/pubvm/a3270/ui/TerminalView.kt +++ b/src/main/java/org/pubvm/a3270/ui/TerminalView.kt @@ -70,6 +70,9 @@ fun TerminalView( val clipboardManager = LocalClipboardManager.current val density = LocalDensity.current + val currentScreenVersion by rememberUpdatedState(screenVersion) + val currentCursorAddr by rememberUpdatedState(cursorAddr) + var selectionStart by remember { mutableStateOf(null) } var selectionEnd by remember { mutableStateOf(null) } var showContextMenu by remember { mutableStateOf(false) } @@ -160,6 +163,10 @@ fun TerminalView( } } ) { + val version = currentScreenVersion + val activeCursorAddr = currentCursorAddr + if (version == -1L) return@Canvas + val width = size.width val height = size.height val cellWidth = width / cols @@ -290,7 +297,7 @@ fun TerminalView( } // Cursor indicator (respects blinking toggle & timer) - if (addr == cursorAddr && !isSelected && cursorVisible) { + if (addr == activeCursorAddr && !isSelected && cursorVisible) { drawRect( color = HOST_COLORS[HOST_COLOR_TURQUOISE].copy(alpha = 0.5f), topLeft = Offset(left, top), diff --git a/src/test/java/org/pubvm/a3270/HardwareKeyboardInputTest.kt b/src/test/java/org/pubvm/a3270/HardwareKeyboardInputTest.kt new file mode 100644 index 0000000..223e865 --- /dev/null +++ b/src/test/java/org/pubvm/a3270/HardwareKeyboardInputTest.kt @@ -0,0 +1,168 @@ +package org.pubvm.a3270 + +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.lib3270j.TerminalModel +import org.lib3270j.charset.EbcdicTranslator +import org.lib3270j.input.InputProcessor +import org.lib3270j.protocol.DS3270Constants.* +import org.lib3270j.screen.ScreenBuffer + +class HardwareKeyboardInputTest { + + private lateinit var screenBuffer: ScreenBuffer + private lateinit var translator: EbcdicTranslator + private lateinit var inputProcessor: InputProcessor + + @Before + fun setUp() { + val model = TerminalModel.IBM_3279_2 // 24x80 + translator = EbcdicTranslator() + screenBuffer = ScreenBuffer(model, translator) + inputProcessor = InputProcessor(screenBuffer, translator, null) + } + + @Test + fun testUnformattedTypingAndNavigation() { + assertEquals(0, screenBuffer.cursorAddress) + + // Type alphanumeric string + val text = "LOGON APPLID(TSO)" + for (ch in text) { + inputProcessor.typeCharacter(ch) + } + + assertEquals(text.length, screenBuffer.cursorAddress) + for (i in text.indices) { + assertEquals(text[i], screenBuffer.getCell(i).ucs4) + } + + // Navigation + inputProcessor.cursorLeft() + assertEquals(text.length - 1, screenBuffer.cursorAddress) + + inputProcessor.cursorRight() + assertEquals(text.length, screenBuffer.cursorAddress) + + inputProcessor.cursorDown() + assertEquals(80 + text.length, screenBuffer.cursorAddress) + + inputProcessor.cursorUp() + assertEquals(text.length, screenBuffer.cursorAddress) + + inputProcessor.cursorHome() + assertEquals(0, screenBuffer.cursorAddress) + } + + @Test + fun testBackspaceAndForwardDelete() { + val text = "HELLO" + for (ch in text) { + inputProcessor.typeCharacter(ch) + } + assertEquals(5, screenBuffer.cursorAddress) + + // Backspace moves left and deletes char + inputProcessor.backspace() + assertEquals(4, screenBuffer.cursorAddress) + } + + @Test + fun testFormattedFieldInputAndTab() { + // Create formatted screen with protected label and unprotected input field + // Position 0: Protected FA + screenBuffer.setCellFA(0, (FA_PRINTABLE or FA_PROTECT).toByte()) + // Set label "USERID:" at positions 1..7 + val label = "USERID:" + for (i in label.indices) { + screenBuffer.getCell(i + 1).ucs4 = label[i] + screenBuffer.getCell(i + 1).ec = translator.unicodeToEbcdic(label[i]).toByte() + } + + // Position 10: Unprotected FA + screenBuffer.setCellFA(10, FA_PRINTABLE.toByte()) + + // Position 20: Protected FA + screenBuffer.setCellFA(20, (FA_PRINTABLE or FA_PROTECT).toByte()) + + // Position 30: Unprotected FA + screenBuffer.setCellFA(30, FA_PRINTABLE.toByte()) + + screenBuffer.cursorAddress = 11 + + // Type into first field + val userid = "IBMUSER" + for (ch in userid) { + inputProcessor.typeCharacter(ch) + } + assertEquals(18, screenBuffer.cursorAddress) + for (i in userid.indices) { + assertEquals(userid[i], screenBuffer.getCell(11 + i).ucs4) + } + + // Tab should jump to position 31 (next unprotected field) + inputProcessor.tab() + assertEquals(31, screenBuffer.cursorAddress) + + // BackTab should jump back to position 11 + inputProcessor.backTab() + assertEquals(11, screenBuffer.cursorAddress) + + // Home should jump to first unprotected field (position 11) + screenBuffer.cursorAddress = 50 + inputProcessor.cursorHome() + assertEquals(11, screenBuffer.cursorAddress) + } + + @Test + fun testFormattedDeleteCharAndEraseEof() { + // Position 0: Unprotected FA + screenBuffer.setCellFA(0, FA_PRINTABLE.toByte()) + // Position 10: Protected FA + screenBuffer.setCellFA(10, (FA_PRINTABLE or FA_PROTECT).toByte()) + + screenBuffer.cursorAddress = 1 + val text = "ABCDE" + for (ch in text) { + inputProcessor.typeCharacter(ch) + } + + // Move cursor to 'C' at pos 3 + screenBuffer.cursorAddress = 3 + inputProcessor.deleteChar() + + // "ABDE " + assertEquals('A', screenBuffer.getCell(1).ucs4) + assertEquals('B', screenBuffer.getCell(2).ucs4) + assertEquals('D', screenBuffer.getCell(3).ucs4) + assertEquals('E', screenBuffer.getCell(4).ucs4) + assertEquals(0, screenBuffer.getCell(5).ec.toInt()) + + // Erase EOF from pos 3 + inputProcessor.eraseEof() + assertEquals('A', screenBuffer.getCell(1).ucs4) + assertEquals('B', screenBuffer.getCell(2).ucs4) + assertEquals(0, screenBuffer.getCell(3).ec.toInt()) + assertEquals(0, screenBuffer.getCell(4).ec.toInt()) + } + + @Test + fun testPfKeyAids() { + val pfAids = intArrayOf( + AID_PF1, AID_PF2, AID_PF3, AID_PF4, AID_PF5, AID_PF6, + AID_PF7, AID_PF8, AID_PF9, AID_PF10, AID_PF11, AID_PF12, + AID_PF13, AID_PF14, AID_PF15, AID_PF16, AID_PF17, AID_PF18, + AID_PF19, AID_PF20, AID_PF21, AID_PF22, AID_PF23, AID_PF24 + ) + + for (i in 1..24) { + val aid = pfAids[i - 1] + inputProcessor.sendAid(aid) + assertEquals(aid, inputProcessor.lastAid) + assertTrue(inputProcessor.isKeyboardLocked) + inputProcessor.reset() + assertFalse(inputProcessor.isKeyboardLocked) + } + } +}