Skip to content

Commit 34022b4

Browse files
f4mrfauxclaude
andcommitted
android: propagate InputManager device-removed to input config (FR #18178)
When a wireless controller is powered off or otherwise vanishes, Android's InputManager fires onInputDeviceRemoved on the activity, but RetroArch's input config layer kept the cached device-name binding, so input_config_get_device_name(port) continued to report the controller as present until the next autoconf event. For consumers that drive behaviour off that check - notably the Conditional Overlay Profile resolver added in this PR, which switches the on-screen overlay between full and minimal presets based on hotplug state - the controller appeared bonded indefinitely and the overlay never returned to its full preset after disconnect. Wire RetroActivityCommon.onInputDeviceRemoved through to android_input.c via a JNI bridge: Java onInputDeviceRemoved(id) -> RetroActivityCommon.inputDeviceRemoved(id) [native] -> push id into g_android_removed_ids[] [mutex queue] android_input_poll() -> android_input_drain_pending_removed(android) -> find pad_states[port].id == removed_id -> clear pad_states[port].id and .name[0] -> input_config_clear_device_name(port) 8-slot queue guarded by android_app->mutex (the lock already used by APP_CMD_* handlers in this file) so multi-device disconnect bursts (two BT controllers powered off together) are not dropped, and the read + drain is atomic. pad_states[port].id is cleared with -1 rather than 0 since Android InputDevice.getDeviceId() can legitimately return 0 for internal/virtual devices. The Conditional Overlay resolver fires the FULL transition naturally on the next poll once the port slot is clear, with the existing debounce (input_overlay_switch_delay_ms) in effect. C89-compliant (variables declared at top of block, /* */ comments, Allman braces, 3-space indent); additive only - if Android never delivers onInputDeviceRemoved the existing behaviour is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent cde459b commit 34022b4

2 files changed

Lines changed: 88 additions & 0 deletions

File tree

input/drivers/android_input.c

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,79 @@ bool (*engine_lookup_name)(char *buf,
166166
int *vendorId, int *productId, size_t len, int id);
167167
void (*engine_handle_dpad)(struct android_app *, AInputEvent*, int, int);
168168

169+
/* Pending-removed-device queue. Populated from the Java UI thread by the
170+
* JNI export below (RetroActivityCommon.onInputDeviceRemoved) and drained
171+
* from the native input poll thread on every poll. Guarded by
172+
* android_app->mutex (the same lock used by APP_CMD_* handlers in this
173+
* file), which is a known-good cross-thread primitive on Android. A small
174+
* fixed queue size handles multi-device disconnect bursts (e.g. two BT
175+
* controllers powered off together); overflow drops the newest event
176+
* rather than clobbering pending entries. */
177+
#define ANDROID_REMOVED_QUEUE_SIZE 8
178+
static int g_android_removed_ids[ANDROID_REMOVED_QUEUE_SIZE];
179+
static int g_android_removed_count = 0;
180+
181+
JNIEXPORT void JNICALL
182+
Java_com_retroarch_browser_retroactivity_RetroActivityCommon_inputDeviceRemoved
183+
(JNIEnv *env, jobject this_obj, jint device_id)
184+
{
185+
struct android_app *android_app = (struct android_app*)g_android;
186+
(void)env;
187+
(void)this_obj;
188+
/* Native side not up yet (activity early lifecycle): drop the event.
189+
* RetroArch will re-autoconfig on next input event from the device if
190+
* it reconnects, so this is safe to ignore. */
191+
if (!android_app)
192+
return;
193+
slock_lock(android_app->mutex);
194+
if (g_android_removed_count < ANDROID_REMOVED_QUEUE_SIZE)
195+
g_android_removed_ids[g_android_removed_count++] = (int)device_id;
196+
slock_unlock(android_app->mutex);
197+
}
198+
199+
/* Called from android_input_poll. Drains queued device-removed events
200+
* delivered by Android's InputManager and clears the matching pad_states
201+
* slot + RetroArch input config name, so input_config_get_device_name(port)
202+
* reflects the OS state. This is what lets the Conditional Overlay Profile
203+
* resolver (FR #18178) see the controller as gone after a Bluetooth
204+
* controller powers off or otherwise unbinds. */
205+
static void android_input_drain_pending_removed(android_input_t *android)
206+
{
207+
struct android_app *android_app = (struct android_app*)g_android;
208+
int local_ids[ANDROID_REMOVED_QUEUE_SIZE];
209+
int local_count = 0;
210+
int i;
211+
unsigned port;
212+
if (!android_app)
213+
return;
214+
slock_lock(android_app->mutex);
215+
if (g_android_removed_count > 0)
216+
{
217+
local_count = g_android_removed_count;
218+
memcpy(local_ids, g_android_removed_ids,
219+
sizeof(int) * (size_t)local_count);
220+
g_android_removed_count = 0;
221+
}
222+
slock_unlock(android_app->mutex);
223+
for (i = 0; i < local_count; i++)
224+
{
225+
int removed_id = local_ids[i];
226+
for (port = 0; port < MAX_USERS; port++)
227+
{
228+
if (android->pad_states[port].id == removed_id)
229+
{
230+
/* -1 sentinel rather than 0: Android InputDevice.getDeviceId()
231+
* can legitimately return 0 for some internal/virtual devices,
232+
* so a 0-cleared slot could spuriously match a future query. */
233+
android->pad_states[port].id = -1;
234+
android->pad_states[port].name[0] = '\0';
235+
input_config_clear_device_name(port);
236+
break;
237+
}
238+
}
239+
}
240+
}
241+
169242
static void android_input_poll_input_gingerbread(android_input_t *android);
170243
static void android_input_poll_input_default(android_input_t *android);
171244
static void (*android_input_poll_input)(android_input_t *android);
@@ -1832,6 +1905,8 @@ static void android_input_poll(void *data)
18321905
android_input_t *android = (android_input_t*)data;
18331906
settings_t *settings = config_get_ptr();
18341907

1908+
android_input_drain_pending_removed(android);
1909+
18351910
while ((ident =
18361911
ALooper_pollAll(settings->uints.input_block_timeout,
18371912
NULL, NULL, NULL)) >= 0)

pkg/android/phoenix-common/src/com/retroarch/browser/retroactivity/RetroActivityCommon.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,10 @@ public void onInputDeviceRemoved(int deviceId)
554554
/* Close any cached USB connection so we don't hold a stale handle. */
555555
closeUsbConnection(deviceId);
556556
mUsbPermissionPending.remove(Integer.valueOf(deviceId));
557+
/* Tell the native input driver so the conditional overlay resolver
558+
* and other consumers of input_config_get_device_name() see the
559+
* device as gone. */
560+
inputDeviceRemoved(deviceId);
557561
}
558562

559563
/**
@@ -990,6 +994,15 @@ public void deleteCore(String coreName) {
990994
*/
991995
public native void safTreeAdded(String tree);
992996

997+
/**
998+
* Notify the native input driver that Android removed an input device.
999+
* Called from {@link #onInputDeviceRemoved(int)} so the conditional overlay
1000+
* resolver (FR #18178) sees a controller as gone after a Bluetooth
1001+
* controller is powered off / unbinds, and any other native consumer of
1002+
* input_config_get_device_name() reflects the OS state.
1003+
*/
1004+
public native void inputDeviceRemoved(int deviceId);
1005+
9931006

9941007

9951008
/////////////// Private methods ///////////////

0 commit comments

Comments
 (0)