|
| 1 | +# Samsung S Pen Hover Phantom Click Prevention |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +This document explains the comprehensive S Pen hover→tap prevention system implemented in RetroArch Android to resolve phantom touchscreen clicks caused by Samsung firmware synthesizing touch events during stylus hover transitions. |
| 6 | + |
| 7 | +## Problem Background |
| 8 | + |
| 9 | +### Original Issue |
| 10 | +Samsung S Pen devices generate phantom touchscreen events when the stylus transitions in/out of hover proximity. These synthesized events cause unwanted menu clicks and paint strokes in RetroArch, making stylus hover unusable. |
| 11 | + |
| 12 | +**Symptoms:** |
| 13 | +- Hovering over menu items causes automatic selection |
| 14 | +- S Pen hover transitions trigger paint strokes in emulated games |
| 15 | +- Quick hover gestures produce delayed phantom clicks |
| 16 | +- Menu navigation becomes unusable with stylus proximity |
| 17 | + |
| 18 | +### Root Cause Analysis |
| 19 | +Based on xlabs' previous research and our investigation, the issue stems from Samsung's firmware architecture: |
| 20 | + |
| 21 | +1. **Dual Event Generation:** S Pen hover events generate both stylus events (`0x5002`) AND phantom touchscreen events (`0x1002`) |
| 22 | +2. **Timing Dependency:** Phantom events arrive 50-100ms after hover transitions |
| 23 | +3. **Source Ambiguity:** Some stylus events are reported through `TOUCHSCREEN` source, making source-only filtering insufficient |
| 24 | +4. **Shared Input Pipeline:** Android's input system processes both event types through the same motion event handlers |
| 25 | + |
| 26 | +## Multi-Layer Protection Architecture |
| 27 | + |
| 28 | +The implementation uses a defense-in-depth approach with four complementary protection layers: |
| 29 | + |
| 30 | +### Layer 1: Hover Guard (Temporal/Spatial Filtering) |
| 31 | +**Location:** `input/drivers/android_input.c` - Global state variables |
| 32 | +```c |
| 33 | +static bool g_hover_guard_active = false; |
| 34 | +static int64_t g_hover_guard_until_ms = 0; |
| 35 | +static float g_hover_guard_x = 0.0f, g_hover_guard_y = 0.0f; |
| 36 | +``` |
| 37 | + |
| 38 | +**Function:** Filters phantom touchscreen events immediately following stylus hover transitions. |
| 39 | + |
| 40 | +**Logic:** |
| 41 | +- Armed on any stylus `HOVER_ENTER/MOVE/EXIT` event (100ms window) |
| 42 | +- Drops finger/touch events within 12px radius during guard period |
| 43 | +- Prevents Samsung synthesized touches from promoting to clicks |
| 44 | + |
| 45 | +### Layer 2: Stylus Proximity Tracking |
| 46 | +**Location:** `android_input_t` struct fields |
| 47 | +```c |
| 48 | +bool stylus_proximity_active; |
| 49 | +int64_t stylus_proximity_until_ns; |
| 50 | +``` |
| 51 | + |
| 52 | +**Function:** Tracks stylus hover state with longer temporal window for quick-tap suppression. |
| 53 | + |
| 54 | +**Logic:** |
| 55 | +- Activated on stylus hover events (120ms window) |
| 56 | +- Disables quick-tap mouse emulation while stylus is nearby |
| 57 | +- Uses nanosecond timestamps for precise timing control |
| 58 | + |
| 59 | +### Layer 3: Quick-Tap Defense-in-Depth |
| 60 | +**Location:** `android_check_quick_tap()` function |
| 61 | +```c |
| 62 | +if (g_hover_guard_active) { |
| 63 | + android->quick_tap_time = 0; |
| 64 | + return 0; |
| 65 | +} |
| 66 | +``` |
| 67 | + |
| 68 | +**Function:** Final safety layer preventing phantom touch promotion to mouse clicks. |
| 69 | + |
| 70 | +**Logic:** |
| 71 | +- Direct hover guard check inside quick-tap function |
| 72 | +- Cancels pending quick-tap timers when guard is active |
| 73 | +- Ensures no hover transition can accidentally trigger clicks |
| 74 | + |
| 75 | +### Layer 4: Menu Gesture Isolation |
| 76 | +**Location:** `menu/menu_driver.c` - Gesture detection logic |
| 77 | +```c |
| 78 | +if (menu_input->pointer.type != MENU_POINTER_TOUCHSCREEN) { |
| 79 | + point.gesture = MENU_INPUT_GESTURE_NONE; |
| 80 | +} |
| 81 | +``` |
| 82 | + |
| 83 | +**Function:** Restricts gesture-based menu interactions to touchscreen-only input. |
| 84 | + |
| 85 | +**Logic:** |
| 86 | +- Blocks `TAP/SHORT_PRESS/LONG_PRESS/SWIPE` gestures for mouse/stylus input |
| 87 | +- Forces stylus to use explicit button presses rather than motion timing |
| 88 | +- Prevents hover motion from being interpreted as intentional gestures |
| 89 | + |
| 90 | +## Input Channel Separation |
| 91 | + |
| 92 | +### Design Philosophy |
| 93 | +The implementation maintains strict separation between stylus and finger input channels using the `mouse_activated` flag as a channel selector. |
| 94 | + |
| 95 | +### Channel Logic |
| 96 | +```c |
| 97 | +if (android->mouse_activated) { |
| 98 | + // Stylus/mouse mode: Direct button state access |
| 99 | + return android->mouse_l; |
| 100 | +} else { |
| 101 | + // Touch mode: Quick-tap emulation with proximity guards |
| 102 | + if (!android->stylus_proximity_active && !g_hover_guard_active) |
| 103 | + return android_check_quick_tap(android); |
| 104 | +} |
| 105 | +``` |
| 106 | + |
| 107 | +### State Transitions |
| 108 | +1. **Stylus Contact:** `mouse_activated = true` → Switches to mouse mode |
| 109 | +2. **Stylus Lift:** `mouse_activated = false` → Returns to touch mode |
| 110 | +3. **Finger Touch:** Only processes when `mouse_activated = false` |
| 111 | + |
| 112 | +This design prevents input interference while maintaining responsive behavior. |
| 113 | + |
| 114 | +## ToolType Classification System |
| 115 | + |
| 116 | +### Primary Classification |
| 117 | +Uses Android NDK `AMotionEvent_getToolType()` as the primary discriminator: |
| 118 | +```c |
| 119 | +int32_t tool = AMotionEvent_getToolType(event, 0); |
| 120 | +bool is_stylus = (tool == AMOTION_EVENT_TOOL_TYPE_STYLUS); |
| 121 | +bool is_finger = (tool == AMOTION_EVENT_TOOL_TYPE_FINGER); |
| 122 | +``` |
| 123 | + |
| 124 | +### Fallback Classification |
| 125 | +When toolType is unavailable or unknown, falls back to input source classification: |
| 126 | +```c |
| 127 | +if (!is_stylus && !is_finger) { |
| 128 | + is_stylus = ((source & AINPUT_SOURCE_STYLUS) == AINPUT_SOURCE_STYLUS); |
| 129 | + is_finger = ((source & AINPUT_SOURCE_TOUCHSCREEN) == AINPUT_SOURCE_TOUCHSCREEN); |
| 130 | +} |
| 131 | +``` |
| 132 | + |
| 133 | +**Note:** ToolType classification is more reliable than source-only filtering because xlabs' research showed stylus events sometimes come through `TOUCHSCREEN` source. |
| 134 | + |
| 135 | +## Contact Detection Logic |
| 136 | + |
| 137 | +### Hardware-Based Detection |
| 138 | +Uses actual pressure and distance sensors for precise contact detection: |
| 139 | +```c |
| 140 | +float pressure = AMotionEvent_getPressure(event, motion_ptr); |
| 141 | +float distance = AMotionEvent_getAxisValue(event, AMOTION_EVENT_AXIS_DISTANCE, motion_ptr); |
| 142 | +bool tip_down = (action != AMOTION_EVENT_ACTION_UP) && |
| 143 | + (pressure > 0.01f) && |
| 144 | + (distance <= 0.0f); |
| 145 | +``` |
| 146 | + |
| 147 | +### Settings Integration |
| 148 | +Respects `input_stylus_require_contact_for_click` user preference: |
| 149 | +- **ON:** Only tip pressure contact triggers clicks (hover never clicks) |
| 150 | +- **OFF:** Tip contact OR side button triggers clicks (hover still never clicks) |
| 151 | + |
| 152 | +## Time Unit Consistency |
| 153 | + |
| 154 | +### Timestamp Handling |
| 155 | +The implementation carefully manages different time units across the Android NDK: |
| 156 | + |
| 157 | +- **Event Times:** `AMotionEvent_getEventTime()` returns nanoseconds |
| 158 | +- **System Time:** `cpu_features_get_time_usec()` returns microseconds |
| 159 | +- **Guard Windows:** Stored in milliseconds for human-readable timeouts |
| 160 | + |
| 161 | +### Conversion Patterns |
| 162 | +```c |
| 163 | +// Event time (ns) to milliseconds |
| 164 | +event_time_ms = AMotionEvent_getEventTime(event) / 1000000; |
| 165 | + |
| 166 | +// Proximity timeout (120ms in nanoseconds) |
| 167 | +android->stylus_proximity_until_ns = AMotionEvent_getEventTime(event) + 120000000; |
| 168 | + |
| 169 | +// Time comparison (both converted to milliseconds) |
| 170 | +if (now / 1000 > android->stylus_proximity_until_ns / 1000000) |
| 171 | +``` |
| 172 | + |
| 173 | +## Settings Framework Integration |
| 174 | + |
| 175 | +### Configuration Options |
| 176 | +Two user-configurable settings are provided: |
| 177 | + |
| 178 | +1. **`input_stylus_require_contact_for_click`** |
| 179 | + - Controls stylus click triggering behavior |
| 180 | + - Default: ON (contact required for clicks) |
| 181 | + |
| 182 | +2. **`input_stylus_hover_moves_pointer`** |
| 183 | + - Controls cursor movement during hover |
| 184 | + - Default: OFF (reduces phantom potential) |
| 185 | + |
| 186 | +### Menu Integration |
| 187 | +Full settings menu integration includes: |
| 188 | +- Configuration entries in `menu/menu_setting.c` |
| 189 | +- Internationalization support in `msg_hash_*.h` |
| 190 | +- Context help in `menu_cbs_sublabel.c` |
| 191 | +- Display logic in `menu_displaylist.c` |
| 192 | + |
| 193 | +## Performance Considerations |
| 194 | + |
| 195 | +### Minimal Overhead |
| 196 | +The protection system adds minimal computational overhead: |
| 197 | +- Static guard variables avoid memory allocation |
| 198 | +- Simple boolean/timestamp checks in hot paths |
| 199 | +- Guard expiration only computed when needed |
| 200 | +- No impact on non-stylus input processing |
| 201 | + |
| 202 | +### Guard Window Tuning |
| 203 | +Current timeouts are empirically determined: |
| 204 | +- **Hover Guard:** 100ms (phantom event suppression) |
| 205 | +- **Proximity Tracking:** 120ms (quick-tap suppression) |
| 206 | +- **Spatial Tolerance:** 12px (phantom event detection) |
| 207 | + |
| 208 | +These values can be adjusted per device if needed. |
| 209 | + |
| 210 | +## Future Maintainers |
| 211 | + |
| 212 | +### Key Code Locations |
| 213 | +- **Core Logic:** `input/drivers/android_input.c` |
| 214 | + - Lines ~105-143: Hover guard implementation |
| 215 | + - Lines ~698-722: Quick-tap defense function |
| 216 | + - Lines ~853-983: Stylus event processing |
| 217 | + - Lines ~2040-2055, ~2128-2143: Input state queries |
| 218 | + |
| 219 | +- **Menu Integration:** `menu/menu_driver.c` |
| 220 | + - Lines ~6084-6170: Gesture isolation logic |
| 221 | + |
| 222 | +### Debug Support |
| 223 | +Enable `DEBUG_ANDROID_INPUT` flag for comprehensive logging: |
| 224 | +```c |
| 225 | +#ifdef DEBUG_ANDROID_INPUT |
| 226 | +RARCH_LOG("[RA Input] act=%d src=0x%x tool=%d dev=%d btn=0x%x dropped=%d\n", ...); |
| 227 | +#endif |
| 228 | +``` |
| 229 | + |
| 230 | +### Testing Requirements |
| 231 | +Always test changes on actual Samsung S Pen devices: |
| 232 | +- Galaxy Note series |
| 233 | +- Galaxy Tab S series |
| 234 | +- Galaxy Z Fold series |
| 235 | + |
| 236 | +Virtual devices cannot reproduce the firmware phantom event behavior. |
| 237 | + |
| 238 | +## References |
| 239 | + |
| 240 | +- **xlabs Research:** Previous investigation identified the need for stylus/touch distinction |
| 241 | +- **Samsung S Pen Documentation:** Android NDK motion event handling |
| 242 | +- **RetroArch Input Architecture:** Existing mouse emulation and quick-tap systems |
| 243 | + |
| 244 | +## Commit History |
| 245 | + |
| 246 | +The implementation was developed across multiple commits: |
| 247 | +- `7daf2ae`: Base S Pen implementation with toolType classification |
| 248 | +- `050a396`: Comprehensive hover→tap prevention with proximity tracking |
| 249 | +- `4c47eac`: Defense-in-depth enhancement to quick-tap function |
| 250 | + |
| 251 | +--- |
| 252 | + |
| 253 | +*This documentation should be updated when modifying the S Pen implementation to ensure future maintainers understand the complete protection strategy.* |
0 commit comments