This application serves as a demonstration and manual testing tool for the virtual gamepad feature in Android. It creates a virtual InputDevice that emulates a physical gamepad and provides a simple on-screen UI with analog sticks, a D-pad, and buttons to control it.
It is intended to be used as a reference for future framework implementation and to validate the behavior of the virtual gamepad service.
This app uses the FLAG_NOT_FOCUSABLE window flag. This is a crucial setting that allows the user to interact with the virtual gamepad buttons and sticks without the controller app stealing input focus from the game or application being controlled.
However, this has a critical side effect: the app will cause an ANR (Application Not Responding) error if no other window has focus.
To use this app correctly:
Install this using:
APP=VirtualGamepadDemo; m $APP && adb install $ANDROID_PRODUCT_OUT/system/app/$APP/$APP.apk
This reference implementation includes several non-obvious optimizations to ensure the best possible performance and lowest latency, which should be carried over to the final framework implementation.
To minimize touch latency, the app requests unbuffered dispatch for the touchscreen input source by calling rootView.requestUnbufferedDispatch(InputDevice.SOURCE_TOUCHSCREEN) once on the root view. This ensures that motion events are delivered to the app as quickly as possible, bypassing the usual VSYNC-based batching.
To avoid making multiple expensive Binder calls when the user touches multiple buttons simultaneously (within the same dispatch cycle), the app overrides the Activity.dispatchTouchEvent method. This allows the app to handle all touch events for a given MotionEvent centrally. The individual OnTouchListeners only update a shared state object. After the default view hierarchy dispatch is complete, the override method sends a single, consolidated MotionEvent to the system via the Binder interface.
To prevent sending redundant events and reduce unnecessary system overhead, the dispatchTouchEvent override checks if the gamepad's state has actually changed before making the Binder call. It does this by copying the state before the super.dispatchTouchEvent call and comparing it to the state after the call. A MotionEvent is only sent if the state is different, ensuring that Binder calls are only made for meaningful input changes.