Skip to content

Commit

Permalink
Implement 'QEMU Extended Key Event' support
Browse files Browse the repository at this point in the history
- If available, AVNC will send raw XT scancodes to servers.
- Servers support for extended key event is required for this to work.

Re: #129 & #149
  • Loading branch information
gujjwal00 committed Mar 10, 2023
1 parent 4b118e8 commit 06e5407
Show file tree
Hide file tree
Showing 8 changed files with 641 additions and 31 deletions.
29 changes: 26 additions & 3 deletions app/src/androidTest/java/com/gaurav/avnc/ui/vnc/KeyHandlerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,28 @@ class KeyHandlerTest {
private lateinit var mockDispatcher: Dispatcher
private lateinit var dispatchedKeyDowns: ArrayList<Int>
private lateinit var dispatchedKeyUps: ArrayList<Int>
private lateinit var dispatchedXTDowns: ArrayList<Int>
private lateinit var dispatchedXTUps: ArrayList<Int>

@Before
fun before() {
instrumentation.runOnMainSync { prefs = AppPreferences(targetContext) }

dispatchedKeyDowns = arrayListOf()
dispatchedKeyUps = arrayListOf()
dispatchedXTDowns = arrayListOf()
dispatchedXTUps = arrayListOf()
mockDispatcher = mockk()
every { mockDispatcher.onXKeySym(any(), true) } answers { dispatchedKeyDowns.add(firstArg()); true }
every { mockDispatcher.onXKeySym(any(), false) } answers { dispatchedKeyUps.add(firstArg()); true }

every { mockDispatcher.onXKey(any(), any(), true) } answers {
dispatchedKeyDowns.add(firstArg())
dispatchedXTUps.add(secondArg())
true
}
every { mockDispatcher.onXKey(any(), any(), false) } answers {
dispatchedKeyUps.add(firstArg())
dispatchedXTUps.add(secondArg())
true
}
keyHandler = KeyHandler(mockDispatcher, true, prefs)
}

Expand Down Expand Up @@ -91,6 +102,11 @@ class KeyHandlerTest {
keyHandler.onKeyEvent(KeyEvent(0, 0, KeyEvent.ACTION_UP, keyCode, 0, metaState))
}

private fun sendKeyWithScancode(keyCode: Int, scancode: Int) {
keyHandler.onKeyEvent(KeyEvent(0, 0, KeyEvent.ACTION_DOWN, keyCode, 0, 0, 0, scancode))
keyHandler.onKeyEvent(KeyEvent(0, 0, KeyEvent.ACTION_UP, keyCode, 0, 0, 0, scancode))
}


/**************************************************************************/
@Test
Expand Down Expand Up @@ -159,6 +175,13 @@ class KeyHandlerTest {
assertTrue(dispatchedKeyDowns.isEmpty())
}

fun rawKeys() {
val scLeft = 105
val xtLeft = 203
sendKeyWithScancode(KeyEvent.KEYCODE_DPAD_LEFT, scLeft)
assertEquals(xtLeft, dispatchedXTDowns.first())
assertEquals(xtLeft, dispatchedXTUps.first())
}

/**************************************************************************/
private val ACCENT_TILDE = 0x02DC
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class TouchHandlerTest {
// Internally, mocks seems to be lazily initialized, and the initialization can take some time.
// This is problematic here because gesture detection is very sensitive to timing of events.
// So we eagerly trigger the initialization, to avoid messing with timings in actual tests.
mockDispatcher.onXKeySym(0, false)
mockDispatcher.onXKey(0, 0, false)
}


Expand Down
10 changes: 8 additions & 2 deletions app/src/main/cpp/native-vnc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -358,8 +358,14 @@ Java_com_gaurav_avnc_vnc_VncClient_nativeGetLastErrorStr(JNIEnv *env, jobject th
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_gaurav_avnc_vnc_VncClient_nativeSendKeyEvent(JNIEnv *env, jobject thiz, jlong client_ptr,
jlong key, jboolean is_down) {
return (jboolean) SendKeyEvent((rfbClient *) client_ptr, (uint32_t) key, is_down ? TRUE : FALSE);
jint key_sym, jint xt_code, jboolean is_down) {
auto client = (rfbClient *) client_ptr;
rfbBool down = is_down ? TRUE : FALSE;

if (xt_code > 0 && SendExtendedKeyEvent(client, key_sym, xt_code, down))
return JNI_TRUE;
else
return SendKeyEvent(client, key_sym, down);
}

extern "C"
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/com/gaurav/avnc/ui/vnc/Dispatcher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ class Dispatcher(private val activity: VncActivity) {
fun onStylusLongPress(p: PointF) = directMode.doClick(PointerButton.Right, p)
fun onStylusScroll(p: PointF) = directMode.doButtonDown(PointerButton.Left, p)

fun onXKeySym(keySym: Int, isDown: Boolean) = messenger.sendKey(keySym, isDown)
fun onXKey(keySym: Int, xtCode: Int, isDown: Boolean) = messenger.sendKey(keySym, xtCode, isDown)

fun onGestureStyleChanged() {
config = Config()
Expand Down
45 changes: 26 additions & 19 deletions app/src/main/java/com/gaurav/avnc/ui/vnc/KeyHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.gaurav.avnc.vnc.XKeySym
import com.gaurav.avnc.vnc.XKeySymAndroid
import com.gaurav.avnc.vnc.XKeySymAndroid.updateKeyMap
import com.gaurav.avnc.vnc.XKeySymUnicode
import com.gaurav.avnc.vnc.XTKeyCode

/**
* Handler for key events
Expand All @@ -25,6 +26,10 @@ import com.gaurav.avnc.vnc.XKeySymUnicode
* to compensate for this & maximize portability. Our implementation is derived after
* testing with some popular servers. It might not handle all the edge cases.
*
* There is an extension to RFB protocol (ExtendedKeyEvent) implemented by some servers.
* It includes support for sending XT keycodes along with key symbol. This extension
* greatly reduces the key handling complexity. Unfortunately, as soft keyboards are
* more common on Android, most [KeyEvent]s don't provide raw scan codes.
*
* Basically, job of this class is to convert the received [KeyEvent] into a 'KeySym'.
* That KeySym will be sent to the server.
Expand All @@ -37,14 +42,14 @@ import com.gaurav.avnc.vnc.XKeySymUnicode
*
* 1. X KeySym - Individual symbols defined by X Windows System
* 2. Unicode KeySym - Unicode code points encoded as X KeySym
* 2. Legacy X KeySym - Old KeySyms which are now superseded by their Unicode KeySym equivalents
* 3. Legacy X KeySym - Old KeySyms which are now superseded by their Unicode KeySym equivalents
*
*
* To decide which one to emit, we look at following things:
*
* a. Key code of [KeyEvent] (may not be available, e.g. in case of [KeyEvent.ACTION_MULTIPLE])
* b. Unicode character of [KeyEvent] (may not be available, e.g. in case of [KeyEvent.KEYCODE_F1])
* c. Current [compatMode]
* c. Current [cfLegacyKeysym]
*
*
*- [KeyEvent]
Expand Down Expand Up @@ -78,7 +83,7 @@ import com.gaurav.avnc.vnc.XKeySymUnicode
* [X Windows System Protocol](https://www.x.org/releases/X11R7.7/doc/xproto/x11protocol.html#keysym_encoding)
*
*/
class KeyHandler(private val dispatcher: Dispatcher, private val compatMode: Boolean, prefs: AppPreferences) {
class KeyHandler(private val dispatcher: Dispatcher, private val cfLegacyKeysym: Boolean, prefs: AppPreferences) {

/**
* Shortcut to send both up & down events. Useful for Virtual Keys.
Expand Down Expand Up @@ -110,8 +115,8 @@ class KeyHandler(private val dispatcher: Dispatcher, private val compatMode: Boo
@Suppress("DEPRECATION")
when (event.action) {

KeyEvent.ACTION_DOWN -> return emitForKeyEvent(event.keyCode, getUnicodeChar(event), true)
KeyEvent.ACTION_UP -> return emitForKeyEvent(event.keyCode, getUnicodeChar(event), false)
KeyEvent.ACTION_DOWN -> return emitForKeyEvent(event.keyCode, getUnicodeChar(event), true, event.scanCode)
KeyEvent.ACTION_UP -> return emitForKeyEvent(event.keyCode, getUnicodeChar(event), false, event.scanCode)

KeyEvent.ACTION_MULTIPLE -> {
if (event.keyCode == KeyEvent.KEYCODE_UNKNOWN) {
Expand All @@ -138,10 +143,11 @@ class KeyHandler(private val dispatcher: Dispatcher, private val compatMode: Boo
}

/**
* Emits a KeySym for given event details.
* Emits an event for given details.
* It will call [emitForAndroidKeyCode] or [emitForUnicodeChar] depending on arguments.
*/
private fun emitForKeyEvent(keyCode: Int, unicodeChar: Int, isDown: Boolean): Boolean {
private fun emitForKeyEvent(keyCode: Int, unicodeChar: Int, isDown: Boolean, scanCode: Int = 0): Boolean {
val xtCode = if (scanCode == 0) 0 else XTKeyCode.fromAndroidScancode(scanCode)

if (handleDiacritics(keyCode, unicodeChar, isDown))
return true
Expand All @@ -153,7 +159,7 @@ class KeyHandler(private val dispatcher: Dispatcher, private val compatMode: Boo
KeyEvent.KEYCODE_NUMPAD_ENTER,
KeyEvent.KEYCODE_SPACE,
KeyEvent.KEYCODE_TAB ->
return emitForAndroidKeyCode(keyCode, isDown)
return emitForAndroidKeyCode(keyCode, isDown, xtCode)
}

// We prefer to use unicodeChar even when keyCode is available because
Expand All @@ -162,26 +168,26 @@ class KeyHandler(private val dispatcher: Dispatcher, private val compatMode: Boo
// it works well with these servers.

if (unicodeChar != 0)
return emitForUnicodeChar(unicodeChar, isDown)
return emitForUnicodeChar(unicodeChar, isDown, xtCode)
else
return emitForAndroidKeyCode(keyCode, isDown)
return emitForAndroidKeyCode(keyCode, isDown, xtCode)
}

/**
* Emits X KeySym corresponding to [keyCode]
*/
private fun emitForAndroidKeyCode(keyCode: Int, isDown: Boolean): Boolean {
private fun emitForAndroidKeyCode(keyCode: Int, isDown: Boolean, xtCode: Int = 0): Boolean {
val keySym = XKeySymAndroid.getKeySymForAndroidKeyCode(keyCode)
return emit(keySym, isDown)
return emit(keySym, isDown, xtCode)
}

/**
* Emits either Unicode KeySym or legacy KeySym for [uChar], depending on [compatMode].
* Emits either Unicode KeySym or legacy KeySym for [uChar], depending on [cfLegacyKeysym].
*/
private fun emitForUnicodeChar(uChar: Int, isDown: Boolean): Boolean {
private fun emitForUnicodeChar(uChar: Int, isDown: Boolean, xtCode: Int = 0): Boolean {
var uKeySym = 0

if (compatMode)
if (cfLegacyKeysym)
uKeySym = XKeySymUnicode.getLegacyKeySymForUnicodeChar(uChar)

if (uKeySym == 0)
Expand All @@ -196,22 +202,23 @@ class KeyHandler(private val dispatcher: Dispatcher, private val compatMode: Boo
if (shouldFakeShift)
emitForAndroidKeyCode(KeyEvent.KEYCODE_SHIFT_LEFT, true)

emit(uKeySym, isDown)
emit(uKeySym, isDown, xtCode)

if (shouldFakeShift)
emitForAndroidKeyCode(KeyEvent.KEYCODE_SHIFT_LEFT, false)

return true
}


/**
* Sends given [keySym] to [dispatcher].
* Sends given X key to [dispatcher].
*/
private fun emit(keySym: Int, isDown: Boolean): Boolean {
private fun emit(keySym: Int, isDown: Boolean, xtCode: Int = 0): Boolean {
if (keySym == 0)
return false

return dispatcher.onXKeySym(keySym, isDown)
return dispatcher.onXKey(keySym, xtCode, isDown)
}


Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/com/gaurav/avnc/vnc/Messenger.kt
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,11 @@ class Messenger(private val client: VncClient) {
}
}

fun sendKey(keySym: Int, isDown: Boolean): Boolean {
fun sendKey(keySym: Int, xtCode: Int, isDown: Boolean): Boolean {
if (!client.connected)
return false

execute { client.sendKeyEvent(keySym, isDown) }
execute { client.sendKeyEvent(keySym, xtCode, isDown) }
return true
}

Expand Down
7 changes: 4 additions & 3 deletions app/src/main/java/com/gaurav/avnc/vnc/VncClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,11 @@ class VncClient(private val observer: Observer) {
* Sends Key event to remote server.
*
* @param keySym Key symbol
* @param xtCode Key code from [XTKeyCode]
* @param isDown true for key down, false for key up
*/
fun sendKeyEvent(keySym: Int, isDown: Boolean) = executeSend {
nativeSendKeyEvent(nativePtr, keySym.toLong(), isDown)
fun sendKeyEvent(keySym: Int, xtCode: Int, isDown: Boolean) = executeSend {
nativeSendKeyEvent(nativePtr, keySym, xtCode, isDown)
}

/**
Expand Down Expand Up @@ -224,7 +225,7 @@ class VncClient(private val observer: Observer) {
private external fun nativeInit(clientPtr: Long, host: String, port: Int): Boolean
private external fun nativeSetDest(clientPtr: Long, host: String, port: Int)
private external fun nativeProcessServerMessage(clientPtr: Long, uSecTimeout: Int): Boolean
private external fun nativeSendKeyEvent(clientPtr: Long, key: Long, isDown: Boolean): Boolean
private external fun nativeSendKeyEvent(clientPtr: Long, keySym: Int, xtCode: Int, isDown: Boolean): Boolean
private external fun nativeSendPointerEvent(clientPtr: Long, x: Int, y: Int, mask: Int): Boolean
private external fun nativeSendCutText(clientPtr: Long, bytes: ByteArray): Boolean
private external fun nativeRefreshFrameBuffer(clientPtr: Long): Boolean
Expand Down
Loading

0 comments on commit 06e5407

Please sign in to comment.