Skip to content

Commit 7ce33be

Browse files
authored
Merge pull request #9 from Animeshz/0.1.x
Fix frozen Windows JVM handler, and a few minor bugs.
2 parents 633a396 + 841378d commit 7ce33be

14 files changed

Lines changed: 199 additions & 102 deletions

File tree

keyboard/src/commonMain/kotlin/com/github/animeshz/keyboard/NativeKeyboardHandler.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
99
import kotlinx.coroutines.flow.SharedFlow
1010
import kotlinx.coroutines.flow.asSharedFlow
1111
import kotlinx.coroutines.flow.distinctUntilChanged
12-
import kotlinx.coroutines.flow.filter
12+
import kotlinx.coroutines.flow.drop
1313
import kotlinx.coroutines.flow.launchIn
1414
import kotlinx.coroutines.flow.map
1515
import kotlinx.coroutines.flow.onEach
@@ -72,10 +72,11 @@ internal abstract class NativeKeyboardHandlerBase : NativeKeyboardHandler {
7272
eventsInternal.subscriptionCount
7373
.map { it > 0 }
7474
.distinctUntilChanged()
75-
.filter { it }
76-
.onEach { readEvents() }
75+
.drop(1) // Drop first false event
76+
.onEach { if (it) startReadingEvents() else stopReadingEvents() }
7777
.launchIn(unconfinedScope)
7878
}
7979

80-
protected abstract fun readEvents()
80+
protected abstract fun startReadingEvents()
81+
protected abstract fun stopReadingEvents()
8182
}

keyboard/src/commonTest/kotlin/com/github/animeshz/keyboard/NativeKeyboardHandlerTest.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import com.github.animeshz.keyboard.entity.Key
44
import com.github.animeshz.keyboard.events.KeyEvent
55
import com.github.animeshz.keyboard.events.KeyState
66
import io.kotest.matchers.comparables.shouldNotBeEqualComparingTo
7+
import io.kotest.matchers.should
8+
import io.kotest.matchers.shouldBe
9+
import kotlinx.coroutines.flow.take
10+
import kotlinx.coroutines.flow.toList
11+
import kotlinx.coroutines.launch
712
import kotlin.test.Test
813

914
@ExperimentalKeyIO
@@ -25,4 +30,25 @@ class NativeKeyboardHandlerTest {
2530

2631
finalState shouldNotBeEqualComparingTo initialState
2732
}
33+
34+
@Test
35+
fun `Test send and receive event`() = runBlockingTest {
36+
val handler = nativeKbHandlerForPlatform()
37+
38+
launch {
39+
handler.sendEvent(KeyEvent(Key.LeftCtrl, KeyState.KeyDown))
40+
handler.sendEvent(KeyEvent(Key.LeftCtrl, KeyState.KeyUp))
41+
}
42+
43+
val events = handler.events.take(2).toList()
44+
45+
events[0] should {
46+
it.key shouldBe Key.LeftCtrl
47+
it.state shouldBe KeyState.KeyDown
48+
}
49+
events[1] should {
50+
it.key shouldBe Key.LeftCtrl
51+
it.state shouldBe KeyState.KeyUp
52+
}
53+
}
2854
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.github.animeshz.keyboard
2+
3+
import kotlinx.coroutines.CoroutineScope
4+
5+
expect fun runBlockingTest(block: suspend CoroutineScope.() -> Unit)

keyboard/src/commonTest/kotlin/com/github/animeshz/keyboard/KeyboardTestTmp.kt renamed to keyboard/src/commonTest/kotlin/examples/KeyboardTestTmp.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.github.animeshz.keyboard
1+
package examples
22

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

keyboard/src/commonTest/kotlin/com/github/animeshz/keyboard/NativeKeyboardHandlerTestTmp.kt renamed to keyboard/src/commonTest/kotlin/examples/NativeKeyboardHandlerTestTmp.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.github.animeshz.keyboard
1+
package examples
22

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

keyboard/src/jvmMain/generated/jni/com_github_animeshz_keyboard_JvmKeyboardHandler.h

Lines changed: 16 additions & 24 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

keyboard/src/jvmMain/jni/windows-x64/JvmKeyboardHandler.cpp

Lines changed: 53 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#include <stdio.h>
12
#include <windows.h>
23
#include <winuser.h>
34

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

1213
#define FAKE_ALT LLKHF_INJECTED | 0x20
1314

14-
HHOOK hook;
15-
16-
JavaVM *jvm;
17-
jobject JvmKeyboardHandler;
18-
jmethodID emitEvent;
15+
DWORD threadId = 0;
16+
JavaVM *jvm = NULL;
17+
jobject JvmKeyboardHandler = NULL;
18+
jmethodID emitEvent = NULL;
1919

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

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

2828
JNIEnv *env;
29-
if (jvm->AttachCurrentThread((void **)&env, nullptr) >= JNI_OK) {
30-
int scanCode = keyInfo->scanCode;
29+
if (jvm->AttachCurrentThread((void **)&env, NULL) >= JNI_OK) {
30+
jint scanCode = keyInfo->scanCode;
3131
switch (vk) {
3232
case 0x21:
3333
scanCode = 104;
@@ -70,23 +70,7 @@ LRESULT CALLBACK LowLevelKeyboardProc(_In_ int nCode, _In_ WPARAM wParam, _In_ L
7070
}
7171
}
7272

73-
return CallNextHookEx(nullptr, nCode, wParam, lParam);
74-
}
75-
76-
JNIEXPORT jint JNICALL Java_com_github_animeshz_keyboard_JvmKeyboardHandler_nativeInit(JNIEnv *env, jobject obj) {
77-
env->GetJavaVM(&jvm);
78-
JvmKeyboardHandler = env->NewGlobalRef(obj);
79-
emitEvent = env->GetMethodID(env->GetObjectClass(obj), "emitEvent", "(IZ)V");
80-
81-
hook = SetWindowsHookExW(WH_KEYBOARD_LL, LowLevelKeyboardProc, GetModuleHandleW(nullptr), 0);
82-
83-
if (hook == nullptr) return GetLastError();
84-
return 0;
85-
}
86-
87-
JNIEXPORT void JNICALL Java_com_github_animeshz_keyboard_JvmKeyboardHandler_nativeShutdown(JNIEnv *env, jobject obj) {
88-
UnhookWindowsHookEx(hook);
89-
env->DeleteGlobalRef(JvmKeyboardHandler);
73+
return CallNextHookEx(NULL, nCode, wParam, lParam);
9074
}
9175

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

10488
// Send Windows/Super key with virtual code, because there's no particular scan code for that.
89+
jint extended = 0;
90+
switch(scanCode) {
91+
case 54:
92+
case 97:
93+
case 100:
94+
case 126:
95+
extended = 1;
96+
break;
97+
}
98+
10599
if (scanCode == 125) {
106100
input.ki.wVk = 0x5B;
107-
input.ki.dwFlags = !isDown ? 2 : 0;
101+
input.ki.dwFlags = (isDown ? 0 : 2) | extended;
108102
} else {
109103
input.ki.wScan = scanCode;
110-
input.ki.dwFlags = 8U | (!isDown ? 2 : 0);
104+
input.ki.dwFlags = 8U | (isDown ? 0 : 2) | extended;
111105
}
112106

113107
SendInput(1, &input, sizeof(input));
114108
}
115109

116-
JNIEXPORT void JNICALL Java_com_github_animeshz_keyboard_JvmKeyboardHandler_nativeReadEvent(JNIEnv *env, jobject obj, jobject subscriptionCountSupplier) {
117-
MSG msg;
118-
119-
jmethodID supplier_method = env->GetMethodID(env->GetObjectClass(subscriptionCountSupplier), "getAsInt", "()I)");
120-
while (env->CallIntMethod(subscriptionCountSupplier, supplier_method)) {
121-
if (GetMessageW(&msg, nullptr, 0, 0) == 0) break;
122-
TranslateMessage(&msg);
123-
DispatchMessageA(&msg);
124-
}
125-
}
126-
127110
JNIEXPORT jboolean JNICALL Java_com_github_animeshz_keyboard_JvmKeyboardHandler_nativeIsPressed(JNIEnv *env, jobject obj, jint scanCode) {
128111
int vk;
129112
if (scanCode == 125)
@@ -134,6 +117,38 @@ JNIEXPORT jboolean JNICALL Java_com_github_animeshz_keyboard_JvmKeyboardHandler_
134117
return GetKeyState(vk) < 0;
135118
}
136119

120+
JNIEXPORT jint JNICALL Java_com_github_animeshz_keyboard_JvmKeyboardHandler_nativeStartReadingEvents(JNIEnv *env, jobject obj) {
121+
HHOOK hook = SetWindowsHookExW(WH_KEYBOARD_LL, LowLevelKeyboardProc, GetModuleHandleW(NULL), 0);
122+
if (hook == NULL) return GetLastError();
123+
124+
env->GetJavaVM(&jvm);
125+
threadId = GetCurrentThreadId();
126+
JvmKeyboardHandler = env->NewGlobalRef(obj);
127+
emitEvent = env->GetMethodID(env->GetObjectClass(obj), "emitEvent", "(IZ)V");
128+
129+
MSG msg;
130+
while (GetMessageW(&msg, NULL, 0, 0)) {
131+
TranslateMessage(&msg);
132+
DispatchMessageA(&msg);
133+
}
134+
135+
UnhookWindowsHookEx(hook);
136+
137+
return 0;
138+
}
139+
140+
JNIEXPORT jint JNICALL Java_com_github_animeshz_keyboard_JvmKeyboardHandler_nativeStopReadingEvents(JNIEnv *env, jobject obj) {
141+
if (JvmKeyboardHandler != NULL) {
142+
PostThreadMessage(threadId, WM_QUIT, 0, 0L);
143+
emitEvent = NULL;
144+
jvm = NULL;
145+
env->DeleteGlobalRef(JvmKeyboardHandler);
146+
JvmKeyboardHandler = NULL;
147+
}
148+
149+
return 0;
150+
}
151+
137152
#ifdef __cplusplus
138153
}
139154
#endif

keyboard/src/jvmMain/kotlin/com/github/animeshz/keyboard/JvmKeyboardHandler.kt

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import com.github.animeshz.keyboard.events.KeyEvent
55
import com.github.animeshz.keyboard.events.KeyState
66
import kotlinx.coroutines.CoroutineScope
77
import kotlinx.coroutines.ExperimentalCoroutinesApi
8+
import kotlinx.coroutines.Job
89
import kotlinx.coroutines.cancel
910
import kotlinx.coroutines.launch
1011
import kotlinx.coroutines.newSingleThreadContext
11-
import java.util.function.IntSupplier
12+
import kotlinx.coroutines.runBlocking
1213

1314
@ExperimentalCoroutinesApi
1415
@ExperimentalKeyIO
@@ -18,16 +19,13 @@ internal object JvmKeyboardHandler : NativeKeyboardHandlerBase() {
1819
init {
1920
NativeUtils.loadLibraryFromJar("KeyboardKt")
2021

21-
val code = nativeInit()
22-
if (code != 0) {
23-
error("Unable to set native hook. Error code: $code")
24-
}
25-
2622
Runtime.getRuntime().addShutdownHook(
2723
Thread {
2824
unconfinedScope.cancel()
29-
ioScope.cancel()
30-
nativeShutdown()
25+
stopReadingEvents()
26+
runBlocking {
27+
ioScope.coroutineContext[Job]?.children?.forEach { it.join() }
28+
}
3129
}
3230
)
3331
}
@@ -44,19 +42,28 @@ internal object JvmKeyboardHandler : NativeKeyboardHandlerBase() {
4442
return if (nativeIsPressed(key.keyCode)) KeyState.KeyDown else KeyState.KeyUp
4543
}
4644

47-
override fun readEvents() {
48-
ioScope.launch { nativeReadEvent { eventsInternal.subscriptionCount.value } }
49-
}
50-
5145
external override fun isCapsLockOn(): Boolean
5246
external override fun isNumLockOn(): Boolean
5347
external override fun isScrollLockOn(): Boolean
5448

55-
private external fun nativeInit(): Int
56-
private external fun nativeShutdown()
49+
override fun startReadingEvents() {
50+
ioScope.launch {
51+
val code = nativeStartReadingEvents()
52+
if (code != 0) {
53+
// Cannot throw, launch will consume it
54+
IllegalStateException("Unable to set native hook. Error code: $code").printStackTrace()
55+
}
56+
}
57+
}
58+
59+
override fun stopReadingEvents() {
60+
nativeStopReadingEvents()
61+
}
62+
5763
private external fun nativeSendEvent(scanCode: Int, isDown: Boolean)
58-
private external fun nativeReadEvent(a: IntSupplier)
5964
private external fun nativeIsPressed(scanCode: Int): Boolean
65+
private external fun nativeStartReadingEvents(): Int
66+
private external fun nativeStopReadingEvents(): Int
6067

6168
private fun emitEvent(scanCode: Int, pressed: Boolean) {
6269
eventsInternal.tryEmit(KeyEvent(Key.fromKeyCode(scanCode), if (pressed) KeyState.KeyDown else KeyState.KeyUp))
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.github.animeshz.keyboard
2+
3+
import kotlinx.coroutines.CoroutineScope
4+
import kotlinx.coroutines.runBlocking
5+
6+
actual fun runBlockingTest(block: suspend CoroutineScope.() -> Unit) =
7+
runBlocking { this.block() }

keyboard/src/linuxX64Main/kotlin/com/github/animeshz/keyboard/DeviceKeyboardHandler.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ internal class DeviceKeyboardHandler : NativeKeyboardHandlerBase() {
2828
TODO("Not yet implemented")
2929
}
3030

31-
override fun readEvents() {
31+
override fun startReadingEvents() {
32+
TODO("Not yet implemented")
33+
}
34+
35+
override fun stopReadingEvents() {
3236
TODO("Not yet implemented")
3337
}
3438

0 commit comments

Comments
 (0)