Skip to content

Commit

Permalink
Merge pull request #9 from Animeshz/0.1.x
Browse files Browse the repository at this point in the history
Fix frozen Windows JVM handler, and a few minor bugs.
  • Loading branch information
Animeshz committed Jan 1, 2021
2 parents 633a396 + 841378d commit 7ce33be
Show file tree
Hide file tree
Showing 14 changed files with 199 additions and 102 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
Expand Down Expand Up @@ -72,10 +72,11 @@ internal abstract class NativeKeyboardHandlerBase : NativeKeyboardHandler {
eventsInternal.subscriptionCount
.map { it > 0 }
.distinctUntilChanged()
.filter { it }
.onEach { readEvents() }
.drop(1) // Drop first false event
.onEach { if (it) startReadingEvents() else stopReadingEvents() }
.launchIn(unconfinedScope)
}

protected abstract fun readEvents()
protected abstract fun startReadingEvents()
protected abstract fun stopReadingEvents()
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import com.github.animeshz.keyboard.entity.Key
import com.github.animeshz.keyboard.events.KeyEvent
import com.github.animeshz.keyboard.events.KeyState
import io.kotest.matchers.comparables.shouldNotBeEqualComparingTo
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlin.test.Test

@ExperimentalKeyIO
Expand All @@ -25,4 +30,25 @@ class NativeKeyboardHandlerTest {

finalState shouldNotBeEqualComparingTo initialState
}

@Test
fun `Test send and receive event`() = runBlockingTest {
val handler = nativeKbHandlerForPlatform()

launch {
handler.sendEvent(KeyEvent(Key.LeftCtrl, KeyState.KeyDown))
handler.sendEvent(KeyEvent(Key.LeftCtrl, KeyState.KeyUp))
}

val events = handler.events.take(2).toList()

events[0] should {
it.key shouldBe Key.LeftCtrl
it.state shouldBe KeyState.KeyDown
}
events[1] should {
it.key shouldBe Key.LeftCtrl
it.state shouldBe KeyState.KeyUp
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.github.animeshz.keyboard

import kotlinx.coroutines.CoroutineScope

expect fun runBlockingTest(block: suspend CoroutineScope.() -> Unit)
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.github.animeshz.keyboard
package examples

/**
* Tests can be tried out after enabling granular source-set metadata in gradle.properties
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.github.animeshz.keyboard
package examples

/**
* Tests can be tried out after enabling granular source-set metadata in gradle.properties
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

91 changes: 53 additions & 38 deletions keyboard/src/jvmMain/jni/windows-x64/JvmKeyboardHandler.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include <stdio.h>
#include <windows.h>
#include <winuser.h>

Expand All @@ -11,23 +12,22 @@ extern "C" {

#define FAKE_ALT LLKHF_INJECTED | 0x20

HHOOK hook;

JavaVM *jvm;
jobject JvmKeyboardHandler;
jmethodID emitEvent;
DWORD threadId = 0;
JavaVM *jvm = NULL;
jobject JvmKeyboardHandler = NULL;
jmethodID emitEvent = NULL;

LRESULT CALLBACK LowLevelKeyboardProc(_In_ int nCode, _In_ WPARAM wParam, _In_ LPARAM lParam) {
tagKBDLLHOOKSTRUCT *keyInfo = (tagKBDLLHOOKSTRUCT *)lParam;
jint vk = keyInfo->vkCode;

if (vk != VK_PACKET && keyInfo->flags & FAKE_ALT != FAKE_ALT) {
jboolean isPressed = wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN;
jboolean extended = keyInfo->flags and 1;
jboolean extended = keyInfo->flags & 1;

JNIEnv *env;
if (jvm->AttachCurrentThread((void **)&env, nullptr) >= JNI_OK) {
int scanCode = keyInfo->scanCode;
if (jvm->AttachCurrentThread((void **)&env, NULL) >= JNI_OK) {
jint scanCode = keyInfo->scanCode;
switch (vk) {
case 0x21:
scanCode = 104;
Expand Down Expand Up @@ -70,23 +70,7 @@ LRESULT CALLBACK LowLevelKeyboardProc(_In_ int nCode, _In_ WPARAM wParam, _In_ L
}
}

return CallNextHookEx(nullptr, nCode, wParam, lParam);
}

JNIEXPORT jint JNICALL Java_com_github_animeshz_keyboard_JvmKeyboardHandler_nativeInit(JNIEnv *env, jobject obj) {
env->GetJavaVM(&jvm);
JvmKeyboardHandler = env->NewGlobalRef(obj);
emitEvent = env->GetMethodID(env->GetObjectClass(obj), "emitEvent", "(IZ)V");

hook = SetWindowsHookExW(WH_KEYBOARD_LL, LowLevelKeyboardProc, GetModuleHandleW(nullptr), 0);

if (hook == nullptr) return GetLastError();
return 0;
}

JNIEXPORT void JNICALL Java_com_github_animeshz_keyboard_JvmKeyboardHandler_nativeShutdown(JNIEnv *env, jobject obj) {
UnhookWindowsHookEx(hook);
env->DeleteGlobalRef(JvmKeyboardHandler);
return CallNextHookEx(NULL, nCode, wParam, lParam);
}

JNIEXPORT jboolean JNICALL Java_com_github_animeshz_keyboard_JvmKeyboardHandler_isCapsLockOn(JNIEnv *env, jobject obj) { return GetKeyState(0x14) & 1; }
Expand All @@ -102,28 +86,27 @@ JNIEXPORT void JNICALL Java_com_github_animeshz_keyboard_JvmKeyboardHandler_nati
input.ki.dwExtraInfo = 0;

// Send Windows/Super key with virtual code, because there's no particular scan code for that.
jint extended = 0;
switch(scanCode) {
case 54:
case 97:
case 100:
case 126:
extended = 1;
break;
}

if (scanCode == 125) {
input.ki.wVk = 0x5B;
input.ki.dwFlags = !isDown ? 2 : 0;
input.ki.dwFlags = (isDown ? 0 : 2) | extended;
} else {
input.ki.wScan = scanCode;
input.ki.dwFlags = 8U | (!isDown ? 2 : 0);
input.ki.dwFlags = 8U | (isDown ? 0 : 2) | extended;
}

SendInput(1, &input, sizeof(input));
}

JNIEXPORT void JNICALL Java_com_github_animeshz_keyboard_JvmKeyboardHandler_nativeReadEvent(JNIEnv *env, jobject obj, jobject subscriptionCountSupplier) {
MSG msg;

jmethodID supplier_method = env->GetMethodID(env->GetObjectClass(subscriptionCountSupplier), "getAsInt", "()I)");
while (env->CallIntMethod(subscriptionCountSupplier, supplier_method)) {
if (GetMessageW(&msg, nullptr, 0, 0) == 0) break;
TranslateMessage(&msg);
DispatchMessageA(&msg);
}
}

JNIEXPORT jboolean JNICALL Java_com_github_animeshz_keyboard_JvmKeyboardHandler_nativeIsPressed(JNIEnv *env, jobject obj, jint scanCode) {
int vk;
if (scanCode == 125)
Expand All @@ -134,6 +117,38 @@ JNIEXPORT jboolean JNICALL Java_com_github_animeshz_keyboard_JvmKeyboardHandler_
return GetKeyState(vk) < 0;
}

JNIEXPORT jint JNICALL Java_com_github_animeshz_keyboard_JvmKeyboardHandler_nativeStartReadingEvents(JNIEnv *env, jobject obj) {
HHOOK hook = SetWindowsHookExW(WH_KEYBOARD_LL, LowLevelKeyboardProc, GetModuleHandleW(NULL), 0);
if (hook == NULL) return GetLastError();

env->GetJavaVM(&jvm);
threadId = GetCurrentThreadId();
JvmKeyboardHandler = env->NewGlobalRef(obj);
emitEvent = env->GetMethodID(env->GetObjectClass(obj), "emitEvent", "(IZ)V");

MSG msg;
while (GetMessageW(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessageA(&msg);
}

UnhookWindowsHookEx(hook);

return 0;
}

JNIEXPORT jint JNICALL Java_com_github_animeshz_keyboard_JvmKeyboardHandler_nativeStopReadingEvents(JNIEnv *env, jobject obj) {
if (JvmKeyboardHandler != NULL) {
PostThreadMessage(threadId, WM_QUIT, 0, 0L);
emitEvent = NULL;
jvm = NULL;
env->DeleteGlobalRef(JvmKeyboardHandler);
JvmKeyboardHandler = NULL;
}

return 0;
}

#ifdef __cplusplus
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import com.github.animeshz.keyboard.events.KeyEvent
import com.github.animeshz.keyboard.events.KeyState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.newSingleThreadContext
import java.util.function.IntSupplier
import kotlinx.coroutines.runBlocking

@ExperimentalCoroutinesApi
@ExperimentalKeyIO
Expand All @@ -18,16 +19,13 @@ internal object JvmKeyboardHandler : NativeKeyboardHandlerBase() {
init {
NativeUtils.loadLibraryFromJar("KeyboardKt")

val code = nativeInit()
if (code != 0) {
error("Unable to set native hook. Error code: $code")
}

Runtime.getRuntime().addShutdownHook(
Thread {
unconfinedScope.cancel()
ioScope.cancel()
nativeShutdown()
stopReadingEvents()
runBlocking {
ioScope.coroutineContext[Job]?.children?.forEach { it.join() }
}
}
)
}
Expand All @@ -44,19 +42,28 @@ internal object JvmKeyboardHandler : NativeKeyboardHandlerBase() {
return if (nativeIsPressed(key.keyCode)) KeyState.KeyDown else KeyState.KeyUp
}

override fun readEvents() {
ioScope.launch { nativeReadEvent { eventsInternal.subscriptionCount.value } }
}

external override fun isCapsLockOn(): Boolean
external override fun isNumLockOn(): Boolean
external override fun isScrollLockOn(): Boolean

private external fun nativeInit(): Int
private external fun nativeShutdown()
override fun startReadingEvents() {
ioScope.launch {
val code = nativeStartReadingEvents()
if (code != 0) {
// Cannot throw, launch will consume it
IllegalStateException("Unable to set native hook. Error code: $code").printStackTrace()
}
}
}

override fun stopReadingEvents() {
nativeStopReadingEvents()
}

private external fun nativeSendEvent(scanCode: Int, isDown: Boolean)
private external fun nativeReadEvent(a: IntSupplier)
private external fun nativeIsPressed(scanCode: Int): Boolean
private external fun nativeStartReadingEvents(): Int
private external fun nativeStopReadingEvents(): Int

private fun emitEvent(scanCode: Int, pressed: Boolean) {
eventsInternal.tryEmit(KeyEvent(Key.fromKeyCode(scanCode), if (pressed) KeyState.KeyDown else KeyState.KeyUp))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.github.animeshz.keyboard

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.runBlocking

actual fun runBlockingTest(block: suspend CoroutineScope.() -> Unit) =
runBlocking { this.block() }
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ internal class DeviceKeyboardHandler : NativeKeyboardHandlerBase() {
TODO("Not yet implemented")
}

override fun readEvents() {
override fun startReadingEvents() {
TODO("Not yet implemented")
}

override fun stopReadingEvents() {
TODO("Not yet implemented")
}

Expand Down

0 comments on commit 7ce33be

Please sign in to comment.