From f010d33ca622fbb7a1d8c722acb726b494f6212e Mon Sep 17 00:00:00 2001 From: CatzTimes <62692767+CatzTimes@users.noreply.github.com> Date: Thu, 22 Oct 2026 11:41:56 +0800 Subject: [PATCH] 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 Co-authored-by: CatzTimes --- .../com/carriez/flutter_hbb/InputService.kt | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt index 6a4a21bab..5681d56df 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt @@ -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