How I added Gamepad Support to Android Virtual Device

Muhammad Muiz
5 min readMay 17, 2023

--

For a computer geek what would be better than coding a hobby project during his Eid holidays. I was enjoying my holiday playing stumble guys on Android and was intrigued by the gamepad option. That’s when it hit me. It was time to stumble my way to the top using the fine controls of a gamepad.

I spun up an AVD instance and installed stumble guys on it. I attached my gamepad to my laptop but the emulator did not recognise it as input. After some troubleshooting I found the directional pad settings in the emulator which said “Not supported for this device.” So there was a problem, AVD did not have joystick support. But it was not the time to give up. Not yet.

After exploring online I found out that I can simulate input into the AVD using adb shell command. It wasn’t easy to find out the commands I needed to execute in the adb shell to simulate the directional pad on android and the gamepad buttons. But this is the GitHub gist that helped. To trigger the gamepad d-pad or button input I needed to use:

adb shell input keyevent X

Where X is an integer ID which maps to a specific key.

Next step was to write a python script which captured the gamepad input from my computer and input it to AVD. Even after trying for hours I could not get pygame, pyjoystick and inputs to work on macOS. After hours of debugging the last solution which worked was using the hidapi. So here is how you can use hidapi to capture the input from gamepad:

Using hidapi to capture gamepad input in python

Install hidapi using:

pip install hidapi

Create main.py file and write the following code:

import hid

MY_DEVICE = None


def main():
for device in hid.enumerate():
print(
f"0x{device['vendor_id']:04x}:0x{device['product_id']:04x} {device['product_string']}"
)


if __name__ == "__main__":
main()

The above code should give you an output similar to this:

0x0000:0x0000 
0x0000:0x0000 Headset
0x05ac:0x8600 TouchBarUserDevice
0x0000:0x0000
0x0000:0x0000
0x05ac:0x0341 Apple Internal Keyboard / Trackpad
0x05ac:0x0341 Apple Internal Keyboard / Trackpad
0x05ac:0x0341 Apple Internal Keyboard / Trackpad
0x05ac:0x0341 Apple Internal Keyboard / Trackpad
0x05ac:0x0341 Apple Internal Keyboard / Trackpad
0x05ac:0x0341 Apple Internal Keyboard / Trackpad
0x05ac:0x0341 Apple Internal Keyboard / Trackpad
0x0000:0x0000 BTM
0x0810:0x0001 USB Gamepad
0x05ac:0x0341 Apple Internal Keyboard / Trackpad
0x05ac:0x0341 Keyboard Backlight

Replace the the value to MY_DEVICE with the numbers on the left of your gamepad:

MY_DEVICE = (0x0810, 0x0001)

Run the following:

import hid

MY_DEVICE = (0x0810, 0x0001)


def main():
for device in hid.enumerate():
print(
f"0x{device['vendor_id']:04x}:0x{device['product_id']:04x} {device['product_string']}"
)

gamepad = hid.device()
gamepad.open(*MY_DEVICE)
gamepad.set_nonblocking(True)

while True:
report = gamepad.read(64)
if report:
print(report)


if __name__ == "__main__":
main()

It should produce an output similar to this on the console.

[1, 128, 128, 128, 128, 15, 0, 0]
[1, 128, 128, 128, 128, 15, 0, 0]
[1, 128, 128, 128, 128, 15, 0, 0]
[1, 128, 128, 128, 128, 15, 0, 0]
[1, 128, 128, 128, 128, 15, 0, 0]
[1, 128, 128, 128, 128, 15, 0, 0]
[1, 128, 128, 128, 128, 15, 0, 0]

The values in above list will change as you play around with your gamepad. We need to map all the inputs to adb shell commands like this:

import subprocess

import hid

MY_DEVICE = (0x0810, 0x0001)


def trigger_adb_event(id):
subprocess.run(f"adb shell input keyevent {id}", shell=True)


def main():
for device in hid.enumerate():
print(
f"0x{device['vendor_id']:04x}:0x{device['product_id']:04x} {device['product_string']}"
)

gamepad = hid.device()
gamepad.open(*MY_DEVICE)
gamepad.set_nonblocking(True)

while True:
report = gamepad.read(64)
if report:
if report[5] == 0:
print("Up pressed")
trigger_adb_event(19)

if report[5] == 4:
print("Down pressed")
trigger_adb_event(20)

# more checks here


if __name__ == "__main__":
main()

We need to check for each index instead of the whole array so we can detect multiple inputs at the same time. The subprocess module is needed to trigger adb shell commands.

Time for results

The directional pad controls were not working for stumble guys. It was time for more troubleshooting. I downloaded some gamepad testing apps which allowed me to check if I was mapping the input correctly. I was mapping the directional pad input correctly but maybe stumble guys only responded to joystick input.

Adding joystick support

It was hard understanding how to input joystick events to stumble guys using adb shell. Finally I found this command:

adb shell input joystick roll X Y

Where X is the movement along x-axis and Y is the movement along y-axis. The values ranges from -1 to 1 in floating point numbers, where 0 is neutral.

To add joystick support to my script I added the following snippet:

...

def trigger_adb_roll(x, y):
subprocess.run(f"adb shell input joystick roll {x} {y}", shell=True)


def main():
...

if report:
if report[5] == 0:
print("Up pressed")
trigger_adb_event(19)

if report[5] == 4:
print("Down pressed")
trigger_adb_event(20)

trigger_adb_roll((report[3] - 127) / 127, (report[4] - 127) / 127)

The divide by 127 is to map the hidapi input to the adb shell compatible values. The hidapi had an x and y-axis range from 0–255 while the adb shell needed a range between 1 to -1 in floating point numbers. So above code uses this equation to map hid range to adb shell range:

adb_x = hid_x - 127 / 127

Opened up the simulator and the joystick input was working correctly. Alas it did not work for stumble guys. After some troubleshooting I found an online forum which mentioned the gamepad support bug in stumble guys. So I tried to look for more games which had gamepad support. And I did find some good titles:

Here is gameplay of oceanhorn on AVD using gamepad:

And here is gameplay of crossy roads using the d-pad:

I have not been much of a gamer as I have been an engineer. I delved back into gaming this holiday to look exactly for such a puzzle to solve. And it was there waiting for me, a nice case to crack. Enough to satisfy my spell of creativity. I’m writing my story so anyone else who is struggling to attach gamepad to AVD can find a head start here. You can find the complete code example with all the inputs mapped on my GitHub.

--

--