From a97d8b3848be7acb1e10442531a8f1db0aec0622 Mon Sep 17 00:00:00 2001
From: Rachel Blackman <[EMAIL REDACTED]>
Date: Tue, 26 May 2026 17:40:52 -0700
Subject: [PATCH] Ensure we release exclusive USB access to controllers when
backgrounded. #15694
(cherry picked from commit ac177763aadd4c84bf5b96ea93bb697b4fc85aa0)
---
.../java/org/libsdl/app/HIDDeviceUSB.java | 38 ++++++++++++++++++-
.../main/java/org/libsdl/app/SDLActivity.java | 14 ++++++-
2 files changed, 48 insertions(+), 4 deletions(-)
diff --git a/android-project/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java b/android-project/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java
index 8ec1cd1bc825c..895463973341a 100644
--- a/android-project/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java
+++ b/android-project/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java
@@ -21,6 +21,7 @@ class HIDDeviceUSB implements HIDDevice {
protected InputThread mInputThread;
protected boolean mRunning;
protected boolean mFrozen;
+ protected boolean mClaimed;
public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface_index) {
mManager = manager;
@@ -29,6 +30,7 @@ public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface
mInterface = mDevice.getInterface(mInterfaceIndex).getId();
mDeviceId = manager.getDeviceIDForIdentifier(getIdentifier());
mRunning = false;
+ mClaimed = false;
}
String getIdentifier() {
@@ -114,6 +116,7 @@ public boolean open() {
close();
return false;
}
+ mClaimed = true;
// Find the endpoints
for (int j = 0; j < iface.getEndpointCount(); j++) {
@@ -137,6 +140,7 @@ public boolean open() {
// back to the Android system gamepad functionality (and lose our paddles et al).
if (mInputEndpoint == null) {
Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName());
+ mConnection.releaseInterface(iface);
close();
return false;
}
@@ -156,6 +160,11 @@ public int writeReport(byte[] report, boolean feature) {
return -1;
}
+ if (!mClaimed) {
+ Log.w(TAG, "writeReport() called but some other process currently owns the USB device");
+ return -1;
+ }
+
if (feature) {
int res = -1;
int offset = 0;
@@ -212,6 +221,12 @@ public boolean readReport(byte[] report, boolean feature) {
Log.w(TAG, "readReport() called with no device connection");
return false;
}
+ if (!mClaimed) {
+ if (feature) {
+ return false;
+ }
+ return true;
+ }
if (report_number == 0x0) {
/* Offset the return buffer by 1, so that the report ID
@@ -265,10 +280,13 @@ public void close() {
mInputThread = null;
}
if (mConnection != null) {
- UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
- mConnection.releaseInterface(iface);
+ if (mClaimed) {
+ UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
+ mConnection.releaseInterface(iface);
+ }
mConnection.close();
mConnection = null;
+ mClaimed = false;
}
}
@@ -281,6 +299,22 @@ public void shutdown() {
@Override
public void setFrozen(boolean frozen) {
mFrozen = frozen;
+
+ /* If we have a valid device connection and the claim state doesn't match what we want, try to correct that. */
+ if (mConnection != null && mClaimed == mFrozen) {
+ UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
+ if (frozen) {
+ mClaimed = !mConnection.releaseInterface(iface);
+ if (mClaimed) {
+ Log.e(TAG, "Tried to release claim on USB device, but failed!");
+ }
+ } else {
+ mClaimed = mConnection.claimInterface(iface, true);
+ if (!mClaimed) {
+ Log.e(TAG, "Tried to regain claim on USB device, but failed!");
+ }
+ }
+ }
}
protected class InputThread extends Thread {
diff --git a/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java b/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java
index be775dba3a6ad..202cf20948c6a 100644
--- a/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java
+++ b/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java
@@ -530,7 +530,8 @@ protected void onPause() {
if (mHIDDeviceManager != null) {
mHIDDeviceManager.setFrozen(true);
- }
+ }
+
if (!mHasMultiWindow) {
pauseNativeThread();
}
@@ -543,7 +544,8 @@ protected void onResume() {
if (mHIDDeviceManager != null) {
mHIDDeviceManager.setFrozen(false);
- }
+ }
+
if (!mHasMultiWindow) {
resumeNativeThread();
}
@@ -616,6 +618,14 @@ public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
Log.v(TAG, "onWindowFocusChanged(): " + hasFocus);
+ // If we are gaining focus, we can always try to restore our USB devices. If we are losing focus,
+ // only try to relinquish them if we don't have background events allowed (for multi-window Android setups).
+ if (hasFocus || !SDLActivity.nativeGetHintBoolean("SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS", false)) {
+ if (mHIDDeviceManager != null) {
+ mHIDDeviceManager.setFrozen(!hasFocus);
+ }
+ }
+
if (SDLActivity.mBrokenLibraries) {
return;
}