fix(android): wheel scrolling from the controller does not reach Android peers (#16409)

The controller sends a wheel tick as a MouseEvent whose type bits are
MOUSE_TYPE_WHEEL (src/common.rs) with the scroll delta in y, but
InputService only handles the legacy WHEEL_DOWN/WHEEL_UP masks
(523331/963) that current controllers never send. As a result the
mouse wheel does nothing on every Android controlled device.

Handle MOUSE_TYPE_WHEEL with the same stroke emulation the legacy
masks use, as a purely additive branch: a 90px vertical stroke
(MOUSE_WHEEL_STEP, three quarters of the legacy WHEEL_STEP - the full
120px felt too far per tick to reviewers on real devices) over
WHEEL_DURATION at the current cursor position, with the same top-edge
guard and queue consumption, mapped with the controller's sign
convention (y < 0 = wheel down; y == 0, a horizontal tick, is
ignored). The branch is gated to API 24+ like the rest of the gesture
path - below that, legacy devices take the Shizuku backend or a
no-op, not this class. The legacy WHEEL_DOWN/WHEEL_UP implementations
are untouched.

Community reports matching this gap on modern devices: #9321 (Pixel
8, Android 14) and #1466 (Android tablet).

Signed-off-by: CatzTimes <CatzTimes@users.noreply.github.com>
Co-authored-by: CatzTimes <CatzTimes@users.noreply.github.com>
This commit is contained in:
CatzTimesandCatzTimes authored and GitHub committed 2026-10-22 11:41:56 +08:00
1 parent bd92623972
commit f010d33ca6
1 file changed
+47
@@ -50,6 +50,15 @@ const val WHEEL_BUTTON_UP = 34
const val WHEEL_DOWN = 523331
const val WHEEL_UP = 963
// src/common.rs MOUSE_TYPE_WHEEL: the controller sends a wheel tick as a
// MouseEvent with these type bits and the scroll delta in y (y < 0 = down).
const val MOUSE_TYPE_WHEEL = 3
// Wheel-tick travel for MOUSE_TYPE_WHEEL events. Three quarters of the legacy
// WHEEL_STEP; tuned on real devices during review (the full 120px felt too
// far per tick).
const val MOUSE_WHEEL_STEP = 90
const val TOUCH_SCALE_START = 1
const val TOUCH_SCALE = 2
const val TOUCH_SCALE_END = 3
@@ -102,6 +111,12 @@ class InputService : AccessibilityService() {
private val volumeController: VolumeController by lazy { VolumeController(applicationContext.getSystemService(AUDIO_SERVICE) as AudioManager) }
/**
* Handles a remote mouse event on the gesture path. [mask] is the
* button/phase mask of the mouse protocol ([MOUSE_TYPE_WHEEL] ticks carry
* the scroll delta in [_y] instead of a position), x/y the screen-space
* position. Requires API 24 for the gesture APIs.
*/
@RequiresApi(Build.VERSION_CODES.N)
fun onMouseInput(mask: Int, _x: Int, _y: Int) {
val x = max(0, _x)
@@ -184,6 +199,38 @@ class InputService : AccessibilityService() {
return
}
// The controller sends a wheel tick as a MouseEvent with the type bits
// MOUSE_TYPE_WHEEL (src/common.rs) and the scroll delta in _y
// (y < 0 = wheel down); the WHEEL_DOWN/WHEEL_UP masks below are legacy
// values that current controllers never send. Gated to API 24+ like
// the rest of the gesture path - below that, legacy devices take the
// Shizuku backend or a no-op, not this class.
if (mask == MOUSE_TYPE_WHEEL && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// A horizontal tick (the controller sends y = 0 when the
// horizontal delta dominates) or a delta truncated to zero is not
// a vertical swipe.
if (_y == 0) {
return
}
if (mouseY < MOUSE_WHEEL_STEP) {
return
}
val path = Path()
path.moveTo(mouseX.toFloat(), mouseY.toFloat())
val deltaY = if (_y < 0) -MOUSE_WHEEL_STEP else MOUSE_WHEEL_STEP
path.lineTo(mouseX.toFloat(), (mouseY + deltaY).toFloat())
val stroke = GestureDescription.StrokeDescription(
path,
0,
WHEEL_DURATION
)
val builder = GestureDescription.Builder()
builder.addStroke(stroke)
wheelActionsQueue.offer(builder.build())
consumeWheelActions()
return
}
if (mask == WHEEL_DOWN) {
if (mouseY < WHEEL_STEP) {
return