From 812ed34dc3c4e54dbb7abf3c80c135e782c7f9cd Mon Sep 17 00:00:00 2001 From: JosJuice Date: Thu, 2 Jan 2025 17:22:29 +0100 Subject: [PATCH] Android: Detect GCAdapter connection using BroadcastReceiver We can register a BroadcastReceiver to have Android tell us when a GC adapter gets connected instead of having a loop where we continuously call SleepCurrentThread(1000) and poll the current status. When waiting for a GC adapter to connect, this both reduces power usage and improves responsiveness. Note that I made openAdapter get the UsbDevice that's been stored by the hotplug code instead of having openAdapter find the UsbDevice on its own like before. This is only because I want to ensure that the UsbDevice being tracked for disconnection is the same as the UsbDevice actually being used, in case the user has multiple adapters connected. --- .../dolphinemu/utils/GCAdapter.java | 180 ++++++++++++++---- Source/Core/InputCommon/GCAdapter.cpp | 39 +++- 2 files changed, 172 insertions(+), 47 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GCAdapter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GCAdapter.java index 924496e488..12ed20fec7 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GCAdapter.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GCAdapter.java @@ -3,8 +3,10 @@ package org.dolphinemu.dolphinemu.utils; import android.app.PendingIntent; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.hardware.usb.UsbConfiguration; import android.hardware.usb.UsbConstants; import android.hardware.usb.UsbDevice; @@ -16,10 +18,12 @@ import android.os.Build; import android.widget.Toast; import androidx.annotation.Keep; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import org.dolphinemu.dolphinemu.BuildConfig; import org.dolphinemu.dolphinemu.DolphinApplication; import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.services.USBPermService; import java.util.HashMap; import java.util.Map; @@ -36,6 +40,21 @@ public class GCAdapter static UsbEndpoint usbIn; static UsbEndpoint usbOut; + private static final String ACTION_GC_ADAPTER_PERMISSION_GRANTED = + BuildConfig.APPLICATION_ID + ".GC_ADAPTER_PERMISSION_GRANTED"; + + private static final Object hotplugCallbackLock = new Object(); + private static boolean hotplugCallbackEnabled = false; + private static UsbDevice adapterDevice = null; + private static BroadcastReceiver hotplugBroadcastReceiver = new BroadcastReceiver() + { + @Override + public void onReceive(Context context, Intent intent) + { + onUsbDevicesChanged(); + } + }; + private static void requestPermission() { HashMap devices = manager.getDeviceList(); @@ -47,11 +66,11 @@ public class GCAdapter if (!manager.hasPermission(dev)) { Context context = DolphinApplication.getAppContext(); - Intent intent = new Intent(context, USBPermService.class); int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE : 0; - PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, flags); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, + new Intent(ACTION_GC_ADAPTER_PERMISSION_GRANTED), flags); manager.requestPermission(dev, pendingIntent); } @@ -71,7 +90,16 @@ public class GCAdapter } @Keep - public static boolean queryAdapter() + public static boolean isUsbDeviceAvailable() + { + synchronized (hotplugCallbackLock) + { + return adapterDevice != null; + } + } + + @Nullable + private static UsbDevice queryAdapter() { HashMap devices = manager.getDeviceList(); for (Map.Entry pair : devices.entrySet()) @@ -80,12 +108,12 @@ public class GCAdapter if (dev.getProductId() == 0x0337 && dev.getVendorId() == 0x057e) { if (manager.hasPermission(dev)) - return true; + return dev; else requestPermission(); } } - return false; + return null; } public static void initAdapter() @@ -109,50 +137,118 @@ public class GCAdapter @Keep public static boolean openAdapter() { - HashMap devices = manager.getDeviceList(); - for (Map.Entry pair : devices.entrySet()) + UsbDevice dev; + synchronized (hotplugCallbackLock) { - UsbDevice dev = pair.getValue(); - if (dev.getProductId() == 0x0337 && dev.getVendorId() == 0x057e) + dev = adapterDevice; + } + + if (dev != null) + { + usbConnection = manager.openDevice(dev); + + Log.info("GCAdapter: Number of configurations: " + dev.getConfigurationCount()); + Log.info("GCAdapter: Number of interfaces: " + dev.getInterfaceCount()); + + if (dev.getConfigurationCount() > 0 && dev.getInterfaceCount() > 0) { - if (manager.hasPermission(dev)) + UsbConfiguration conf = dev.getConfiguration(0); + usbInterface = conf.getInterface(0); + usbConnection.claimInterface(usbInterface, true); + + Log.info("GCAdapter: Number of endpoints: " + usbInterface.getEndpointCount()); + + if (usbInterface.getEndpointCount() == 2) { - usbConnection = manager.openDevice(dev); - - Log.info("GCAdapter: Number of configurations: " + dev.getConfigurationCount()); - Log.info("GCAdapter: Number of interfaces: " + dev.getInterfaceCount()); - - if (dev.getConfigurationCount() > 0 && dev.getInterfaceCount() > 0) - { - UsbConfiguration conf = dev.getConfiguration(0); - usbInterface = conf.getInterface(0); - usbConnection.claimInterface(usbInterface, true); - - Log.info("GCAdapter: Number of endpoints: " + usbInterface.getEndpointCount()); - - if (usbInterface.getEndpointCount() == 2) - { - for (int i = 0; i < usbInterface.getEndpointCount(); ++i) - if (usbInterface.getEndpoint(i).getDirection() == UsbConstants.USB_DIR_IN) - usbIn = usbInterface.getEndpoint(i); - else - usbOut = usbInterface.getEndpoint(i); - - initAdapter(); - return true; - } + for (int i = 0; i < usbInterface.getEndpointCount(); ++i) + if (usbInterface.getEndpoint(i).getDirection() == UsbConstants.USB_DIR_IN) + usbIn = usbInterface.getEndpoint(i); else - { - usbConnection.releaseInterface(usbInterface); - } - } + usbOut = usbInterface.getEndpoint(i); - Toast.makeText(DolphinApplication.getAppContext(), R.string.replug_gc_adapter, - Toast.LENGTH_LONG).show(); - usbConnection.close(); + initAdapter(); + return true; + } + else + { + usbConnection.releaseInterface(usbInterface); } } + + Toast.makeText(DolphinApplication.getAppContext(), R.string.replug_gc_adapter, + Toast.LENGTH_LONG).show(); + usbConnection.close(); } return false; } + + @Keep + public static void enableHotplugCallback() + { + synchronized (hotplugCallbackLock) + { + if (hotplugCallbackEnabled) + { + throw new IllegalStateException("enableHotplugCallback was called when already enabled"); + } + + IntentFilter filter = new IntentFilter(); + filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); + filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); + filter.addAction(ACTION_GC_ADAPTER_PERMISSION_GRANTED); + + ContextCompat.registerReceiver(DolphinApplication.getAppContext(), hotplugBroadcastReceiver, + filter, ContextCompat.RECEIVER_EXPORTED); + + hotplugCallbackEnabled = true; + + onUsbDevicesChanged(); + } + } + + @Keep + public static void disableHotplugCallback() + { + synchronized (hotplugCallbackLock) + { + if (hotplugCallbackEnabled) + { + DolphinApplication.getAppContext().unregisterReceiver(hotplugBroadcastReceiver); + hotplugCallbackEnabled = false; + adapterDevice = null; + } + } + } + + public static void onUsbDevicesChanged() + { + synchronized (hotplugCallbackLock) + { + if (adapterDevice != null) + { + boolean adapterStillConnected = manager.getDeviceList().entrySet().stream() + .anyMatch(pair -> pair.getValue().getDeviceId() == adapterDevice.getDeviceId()); + + if (!adapterStillConnected) + { + adapterDevice = null; + onAdapterDisconnected(); + } + } + + if (adapterDevice == null) + { + UsbDevice newAdapter = queryAdapter(); + if (newAdapter != null) + { + adapterDevice = newAdapter; + onAdapterConnected(); + } + } + } + } + + private static native void onAdapterConnected(); + + private static native void onAdapterDisconnected(); } diff --git a/Source/Core/InputCommon/GCAdapter.cpp b/Source/Core/InputCommon/GCAdapter.cpp index 5764aa660b..b02306806d 100644 --- a/Source/Core/InputCommon/GCAdapter.cpp +++ b/Source/Core/InputCommon/GCAdapter.cpp @@ -144,9 +144,9 @@ static std::mutex s_write_mutex; static std::thread s_adapter_detect_thread; static Common::Flag s_adapter_detect_thread_running; -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION static Common::Event s_hotplug_event; +#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION static std::function s_detect_callback; #if defined(__FreeBSD__) && __FreeBSD__ >= 11 @@ -344,6 +344,23 @@ static int HotplugCallback(libusb_context* ctx, libusb_device* dev, libusb_hotpl return 0; } #endif +#elif GCADAPTER_USE_ANDROID_IMPLEMENTATION +extern "C" { + +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_utils_GCAdapter_onAdapterConnected(JNIEnv* env, jclass) +{ + INFO_LOG_FMT(CONTROLLERINTERFACE, "GC adapter connected"); + if (!s_detected) + s_hotplug_event.Set(); +} + +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_utils_GCAdapter_onAdapterDisconnected(JNIEnv* env, jclass) +{ + INFO_LOG_FMT(CONTROLLERINTERFACE, "GC adapter disconnected"); +} +} #endif static void ScanThreadFunc() @@ -393,15 +410,22 @@ static void ScanThreadFunc() #elif GCADAPTER_USE_ANDROID_IMPLEMENTATION JNIEnv* const env = IDCache::GetEnvForThread(); - const jmethodID queryadapter_func = - env->GetStaticMethodID(s_adapter_class, "queryAdapter", "()Z"); + const jmethodID enable_hotplug_callback_func = + env->GetStaticMethodID(s_adapter_class, "enableHotplugCallback", "()V"); + env->CallStaticVoidMethod(s_adapter_class, enable_hotplug_callback_func); + + const jmethodID is_usb_device_available_func = + env->GetStaticMethodID(s_adapter_class, "isUsbDeviceAvailable", "()Z"); while (s_adapter_detect_thread_running.IsSet()) { if (!s_detected && UseAdapter() && - env->CallStaticBooleanMethod(s_adapter_class, queryadapter_func)) + env->CallStaticBooleanMethod(s_adapter_class, is_usb_device_available_func)) + { Setup(); - Common::SleepCurrentThread(1000); + } + + s_hotplug_event.Wait(); } #endif @@ -691,6 +715,11 @@ void Shutdown() if (s_libusb_context->IsValid() && s_libusb_hotplug_enabled) libusb_hotplug_deregister_callback(*s_libusb_context, s_hotplug_handle); #endif +#elif GCADAPTER_USE_ANDROID_IMPLEMENTATION + JNIEnv* const env = IDCache::GetEnvForThread(); + const jmethodID disable_hotplug_callback_func = + env->GetStaticMethodID(s_adapter_class, "disableHotplugCallback", "()V"); + env->CallStaticVoidMethod(s_adapter_class, disable_hotplug_callback_func); #endif Reset(CalledFromReadThread::No);