Inside Waydroid, which runs Android on Linux, the numpad stopped typing digits. With Gboard set to Japanese, it worked. With “English (US)”, it did not. The digit row above the letters worked in both. On the host (Arch Linux, sway), the numpad typed digits normally.

Adding input * xkb_numlock enabled to the sway config did not help. I looked into why, using the source code and measurements.

Summary

  • The host’s NumLock state is not passed to the Android inside Waydroid. The sway setting has no effect on Waydroid.
  • Android’s own NumLock state is probably being cleared right after it is set. The source and the measurements agree, but I did not observe the value that does the clearing.
  • Adding a keyboard layout through an Android app, one that maps the numpad to digits, made the numpad type digits in English mode.

What the source says

I read Waydroid’s HWC (hwcomposer/wayland-hwc.cpp in android_hardware_waydroid), the input code of Android 13 (android13-release), and the patches Waydroid applies (android_vendor_waydroid).

1. Host keys are passed as key codes. Modifier state is not passed. The HWC writes key events from the Waydroid window to a pipe on the Android side (/dev/input/wl_keyboard_events) as key codes. The function that receives Wayland’s modifier state, keyboard_handle_modifiers, has an empty body. Even if NumLock is on in the host, Android does not know.

2. Numpad digits appear only when Android’s NumLock is on. In Android 13’s Generic.kcm, NUMPAD_1 is defined as base: fallback MOVE_END, numlock: '1'. With NumLock off, it acts as the End key.

3. NumLock toggles on key release. toggleLockedMetaState in Keyboard.cpp does nothing on key press and flips the state on key release.

4. The input device Waydroid creates registers only two keys. Waydroid’s EventHub patch (0004-EventHub-Add-wayland-inputs-support.patch for Android 13) sets only BTN_MISC and KEY_OK in the keyBitmask of wayland_keyboard. Android’s EventHub::markSupportedKeyCodes treats any key missing from that registration as “not on this device”, even if the key layout defines it. KeyboardInputMapper::updateLedState clears the NumLock state for a device that has no NumLock key. NumLock (code 69) is not in the registration.

Measurements

I took the input device state with dumpsys input. This extracts only the part that does not contain key history:

        sudo waydroid shell dumpsys input | sed -n '/Input Reader State/,/Input Dispatcher State/p' > out-1.txt
    

I looked at MetaState (modifier and lock state) and DownTime (time of the last key press) for wayland_keyboard.

ActionDownTimeMetaState
Initial776391554710000x0
Press a781768744360000x0
Press and release NumLock781987405550000x0

Pressing NumLock changes DownTime, so the key event reaches wayland_keyboard. But MetaState stays 0x0. If NumLock had turned on, the 0x200000 bit would be set. The event arrives and the state does not stay. That fits the reading of point 4.

The device list (Event Hub State) showed that wayland_keyboard uses Generic.kl and Generic.kcm. The key layout does define NUM_LOCK.

What I could not confirm

  • Whether point 4 (NUM_LOCK missing from the registration, so the NumLock state is cleared) is the direct cause. It is an inference from the code and from MetaState staying 0x0. I did not see the value of the check itself.
  • Why the numpad works in Japanese mode. I suspect Gboard’s Japanese input handles numpad key codes itself, but Gboard is closed source and I cannot check.
  • Whether the patches in my Android image match the ones I read. I only confirmed that the Android version is 13.

The same symptom is reported in Waydroid issue #1944 (June 2025, still open).

The fix

Fixing the Waydroid patch would mean rebuilding the Android image. Instead, I added a keyboard layout through an Android app. android-numpad-kl provides a layout that maps numpad keys (scan codes 71–73, 75–77, 79–83) to digit keys regardless of NumLock. I found it in a comment on another issue.

Before installing, I read all of the source. It is four Java files, about 500 lines, and the only permission is RECEIVE_BOOT_COMPLETED. I found no network access and no command execution.

I built it from source instead of using the distributed APK. The original build setup needs Gradle and JDK 17, but this machine has only the Android SDK (platform 36) and JDK 27. I put together a short script that calls aapt2, javac, d8, zipalign and apksigner in order. The executables in /opt/android-sdk/build-tools were executable only by root, so I used copies.

Problems while installing

  • waydroid app install returns no result. Reading the source, it only tells Android to start the installation and has no code that receives the result. A failed install prints nothing. I found the reason with sudo waydroid shell -- logcat -d (the -- is required).
  • waydroid does not run if python3 is the pyenv one. It cannot import dbus. I ran it as /usr/bin/python3 /bin/waydroid ... to use the system Python.
  • Play Protect blocked it. With the original targetSdk of 31, it showed “Blocked an unsafe app (targets an older version of Android)” with only an “OK” button. With 35, it changed to “We recommend scanning this app”.
  • The dialog vanished within a second. It disappears when the mouse focus moves into the Waydroid window. Reading keyboard_handle_enter in the HWC, it switches the Android screen each time focus enters the window. I think that is the cause, but I did not confirm it. I put the focus on Waydroid first, then ran sleep 8; waydroid app install ... and answered the dialog.

Finally, in Android’s settings under “Physical keyboard”, I selected “Full keyboard with numpad, US QWERTY” for wayland_keyboard. The numpad then typed digits in English mode.

What I kept

The build script, the resulting APK and the steps are in ~/repos/waydroid-numpad-kl. I learned that the host’s xkb_numlock enabled has no effect on Waydroid, but it may still work for other apps, so I did not remove it.