Compare commits

...
Author SHA1 Message Date
LibretroAdminandf4mrfaux d530eb8f10 android: report a controller the OS has removed as disconnected
Android tells the activity when an input device goes away - a pad
switched off, out of range or unplugged - but nothing passed that on
to the input driver, and no input event marks a removal. A pad
therefore stayed connected for as long as the app ran:

- its port kept its device name, so an overlay hidden while a
  controller is connected never came back;
- whatever it was holding stayed held, since no release arrives from a
  device that is gone;
- no disconnect notification was shown, and pausing on disconnect
  could not happen.

RetroActivityCommon.onInputDeviceRemoved now hands the device id to
the driver. It is left in a small lock-free mailbox and taken out at
the next input poll, which clears the pad's key, axis and hat state
and reports the disconnect through input_autoconfigure_disconnect, as
the other joypad drivers do.

The pad's slot keeps its name and is marked removed instead of being
emptied, so the device gets its old port back when it returns, under
whatever id Android gives it; its return is reported as a connect. A
removed slot is matched before a live one of the same name, so of two
identical controllers the one that went away is not taken for the one
still connected.

A removal is read at the next poll, which can be long after it
happened if the app was in the background. If the id resolves again by
then, the device has come back under it and nothing is reported.

This is the device-removal part of PR #19077, reworked. That version
emptied the slot's name, which the driver uses to find a returning
device's port: every reconnect would have moved the pad to the next
free port.

Compiled with the NDK through the Android unity build. Not run on a
device.

Co-authored-by: f4mrfaux <[email protected]>
2026-10-02 06:05:48 +02:00
2 changed files with 157 additions and 0 deletions

No files matched your search

+147
View File
@@ -17,6 +17,7 @@
*/
#include <errno.h>
#include <limits.h>
#include <unistd.h>
#include <dlfcn.h>
#include <math.h>
@@ -376,6 +377,58 @@ JNIEXPORT void JNICALL Java_com_retroarch_browser_retroactivity_RetroActivityCom
free(stale);
}
/* --- Input devices the OS has removed ----------------------------------
*
* Android reports a controller going away (switched off, out of range,
* unplugged) to the activity's InputManager listener, on the UI thread.
* Nothing on the native side heard of it: no input event marks a
* removal, so a pad stayed "connected" for as long as the app ran. Its
* port kept its device name (so an overlay hidden while a controller is
* connected never came back), the buttons it was holding stayed held,
* and pausing on disconnect could not happen.
*
* The listener hands the removed device's id to
* inputDeviceRemoved() below, which leaves it in a small mailbox; the
* input thread takes it out at its next poll and reports the
* disconnect. Each slot holds an id plus one, zero meaning empty, and
* is claimed with a compare-and-swap, so it does not matter which
* thread the listener runs on. A flag saves the poll from looking at
* the slots when nothing is waiting: a producer fills a slot and then
* raises the flag, the poll lowers the flag and then empties the slots,
* so an id that lands behind the poll's back leaves the flag raised for
* the next one. If all slots are full the removal is dropped, which
* leaves that pad where every removed pad was before this existed. */
#define ANDROID_REMOVED_SLOTS 8
/* What pad_states[].id holds once its device has been removed. No
* device has this id (-1, the obvious choice, is the virtual
* keyboard's). The slot keeps its name, so the same device finds its
* old port again when it returns. */
#define ANDROID_PAD_ID_REMOVED INT_MIN
static retro_atomic_int_t android_removed_ids[ANDROID_REMOVED_SLOTS];
static retro_atomic_int_t android_removed_pending;
/* Called by Java (RetroActivityCommon.onInputDeviceRemoved). */
JNIEXPORT void JNICALL Java_com_retroarch_browser_retroactivity_RetroActivityCommon_inputDeviceRemoved(
JNIEnv *env, jobject this_obj, jint device_id)
{
unsigned i;
/* Ids below zero are not physical devices. */
if (device_id < 0 || device_id == INT_MAX)
return;
for (i = 0; i < ANDROID_REMOVED_SLOTS; i++)
{
if (retro_atomic_cas_int(&android_removed_ids[i], 0, (int)device_id + 1))
{
retro_atomic_store_release_int(&android_removed_pending, 1);
return;
}
}
}
bool android_keyboard_start(char **buffer_ptr, size_t *size_ptr,
size_t *ptr_ptr, const char *label,
input_keyboard_line_complete_t cb, void *userdata)
@@ -701,6 +754,8 @@ bool android_input_can_be_keyboard(void *data, int port)
state_device_t *device = &android->pad_states[port];
if (!device->id && !*device->name)
return false;
if (device->id == ANDROID_PAD_ID_REMOVED)
return false;
return android_input_can_be_keyboard_jni(device->id);
}
@@ -2199,11 +2254,36 @@ static int android_input_recover_port(android_input_t *android, int id)
int vendorId = 0;
int productId = 0;
int ret = -1;
unsigned i;
settings_t *settings = config_get_ptr();
if (!engine_lookup_name(device_name, &vendorId,
&productId, sizeof(device_name), id))
return -1;
/* A pad the OS reported as removed comes back to the slot it left,
* whatever id it returns under. Its disconnect was reported to the
* frontend, so its return is reported too. A removed slot is looked
* for first: of two identical controllers, the one that went away
* must not be taken for the one still connected. */
for (i = 0; i < android->pads_connected; i++)
{
if ( android->pad_states[i].id == ANDROID_PAD_ID_REMOVED
&& string_is_equal(device_name, android->pad_states[i].name))
{
android->pad_states[i].id = id;
g_android->id[i] = id;
input_autoconfigure_connect(
android->pad_states[i].name,
NULL, NULL,
android_joypad.ident,
i,
vendorId,
productId);
return (int)i;
}
}
ret = android_input_get_id_index_from_name(android, device_name);
if (ret < 0)
return -1;
@@ -3049,6 +3129,69 @@ static void android_input_reinit(void)
/* Handle all events.
*/
/* The OS removed input device 'id': report the pad it was as
* disconnected. */
static void android_input_pad_removed(android_input_t *android,
struct android_app *android_app, int id)
{
unsigned port;
char name[256];
int vendor_id = 0;
int product_id = 0;
/* The poll does not run while the app is in the background, so a
* removal can be read long after it happened. If the id resolves
* again, the device came back under it in the meantime and there is
* nothing to report. */
name[0] = '\0';
if (engine_lookup_name(name, &vendor_id, &product_id, sizeof(name), id))
return;
/* A slot's index is its port (android_input_get_id_port). */
for (port = 0; port < android->pads_connected; port++)
{
if (android->pad_states[port].id != id)
continue;
/* Nothing it held stays held: no release will arrive from a
* device that is gone. */
if (port < DEFAULT_MAX_PADS)
{
memset(android_key_state[port], 0,
sizeof(android_key_state[port]));
memset(android_app->analog_state[port], 0,
sizeof(android_app->analog_state[port]));
memset(android_app->hat_state[port], 0,
sizeof(android_app->hat_state[port]));
}
android->pad_states[port].id = ANDROID_PAD_ID_REMOVED;
android_app->id[port] = 0;
input_autoconfigure_disconnect(port, android->pad_states[port].name);
return;
}
}
/* Take the removals the UI thread left since the last poll. */
static void android_input_poll_removed(android_input_t *android,
struct android_app *android_app)
{
unsigned i;
if (!retro_atomic_load_acquire_int(&android_removed_pending))
return;
retro_atomic_store_release_int(&android_removed_pending, 0);
for (i = 0; i < ANDROID_REMOVED_SLOTS; i++)
{
int v = retro_atomic_exchange_int(&android_removed_ids[i], 0);
if (v)
android_input_pad_removed(android, android_app, v - 1);
}
}
static void android_input_poll(void *data)
{
int ident;
@@ -3060,6 +3203,10 @@ static void android_input_poll(void *data)
/* Apply any text staged by the native (IME) keyboard. */
android_keyboard_poll();
/* Report the pads the OS has removed since the last poll. */
if (android && android_app)
android_input_poll_removed(android, android_app);
/* Backgrounded (APP_CMD_PAUSE/STOP set RUNLOOP_FLAG_IDLE): there is
* nothing to do until the OS delivers the next command, and the
* looper will wake us for it. Block on it with -1 instead of the
@@ -921,6 +921,9 @@ public class RetroActivityCommon extends NativeActivity
closeUsbConnection(deviceId);
mUsbPermissionPending.remove(Integer.valueOf(deviceId));
invalidateDeviceCaches(deviceId);
/* Tell the native input driver: no input event marks a removal, so
* this callback is the only way it learns that a pad is gone. */
inputDeviceRemoved(deviceId);
}
/** Drops every cached per-device lookup for an input device id. */
@@ -1849,6 +1852,13 @@ public class RetroActivityCommon extends NativeActivity
*/
public native void onSystemKeyboardInput(String text, boolean finished);
/**
* Tells the native input driver that Android removed an input device.
*
* @param deviceId The id the device had.
*/
public native void inputDeviceRemoved(int deviceId);
/////////////// Private methods ///////////////