From fe53461611d86384478064be6b4297a7c7e437e1 Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Thu, 4 Feb 2016 18:31:36 -0600 Subject: [PATCH] [Android] Implement support for real Wiimotes with the DolphinBar This is the only way to get Wiimotes working under Android now. This, just like the Wii U Gamecube Controller Adapter, completely goes around Android's limitations and talks with the device directly through USBManager. Couple notes. Continuous scanning must be enabled otherwise the Wiimotes won't be seen. The UI doesn't expose support for this yet. One must change the Wiimote source and continuous scanning settings manually. Testing up to two wiimotes in Taiko No Tatsujin, no reason to believe all four won't work. --- .../dolphinemu/dolphinemu/NativeLibrary.java | 5 + .../activities/EmulationActivity.java | 6 + .../dolphinemu/utils/Java_WiimoteAdapter.java | 156 +++++++++++++++++ .../main/res/layout/fragment_ingame_menu.xml | 5 + .../app/src/main/res/values/strings.xml | 1 + Source/Android/jni/MainAndroid.cpp | 7 + Source/Core/Core/CMakeLists.txt | 2 + Source/Core/Core/HW/WiimoteReal/IOAndroid.cpp | 165 ++++++++++++++++++ Source/Core/Core/HW/WiimoteReal/WiimoteReal.h | 4 + 9 files changed, 351 insertions(+) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/Java_WiimoteAdapter.java create mode 100644 Source/Core/Core/HW/WiimoteReal/IOAndroid.cpp diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java index 38ea670f5e..992b670b1f 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java @@ -379,6 +379,11 @@ public final class NativeLibrary /** Native EGL functions not exposed by Java bindings **/ public static native void eglBindAPI(int api); + /** + * Provides a way to refresh the connections on Wiimotes + */ + public static native void RefreshWiimotes(); + /** * The methods C++ uses to find references to Java classes and methods * are really expensive. Rather than calling them every time we want to diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java index 2f38df3373..f90956dd9a 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java @@ -33,6 +33,7 @@ import org.dolphinemu.dolphinemu.fragments.SaveStateFragment; import org.dolphinemu.dolphinemu.ui.main.MainPresenter; import org.dolphinemu.dolphinemu.utils.Animations; import org.dolphinemu.dolphinemu.utils.Java_GCAdapter; +import org.dolphinemu.dolphinemu.utils.Java_WiimoteAdapter; import org.dolphinemu.dolphinemu.utils.Log; import java.util.List; @@ -120,6 +121,7 @@ public final class EmulationActivity extends AppCompatActivity super.onCreate(savedInstanceState); Java_GCAdapter.manager = (UsbManager) getSystemService(Context.USB_SERVICE); + Java_WiimoteAdapter.manager = (UsbManager) getSystemService(Context.USB_SERVICE); // Picasso will take a while to load these big-ass screenshots. So don't run // the animation until we say so. @@ -392,6 +394,10 @@ public final class EmulationActivity extends AppCompatActivity return; } + case R.id.menu_refresh_wiimotes: + NativeLibrary.RefreshWiimotes(); + return; + // Screenshot capturing case R.id.menu_emulation_screenshot: NativeLibrary.SaveScreenShot(); diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/Java_WiimoteAdapter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/Java_WiimoteAdapter.java new file mode 100644 index 0000000000..049f930cba --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/Java_WiimoteAdapter.java @@ -0,0 +1,156 @@ +package org.dolphinemu.dolphinemu.utils; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.Intent; +import android.hardware.usb.UsbConfiguration; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbEndpoint; +import android.hardware.usb.UsbInterface; +import android.hardware.usb.UsbManager; + +import org.dolphinemu.dolphinemu.NativeLibrary; +import org.dolphinemu.dolphinemu.services.USBPermService; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public class Java_WiimoteAdapter +{ + final static int MAX_PAYLOAD = 23; + final static int MAX_WIIMOTES = 4; + final static int TIMEOUT = 200; + final static short NINTENDO_VENDOR_ID = 0x057e; + final static short NINTENDO_WIIMOTE_PRODUCT_ID = 0x0306; + public static UsbManager manager; + + static UsbDeviceConnection usb_con; + static UsbInterface[] usb_intf = new UsbInterface[MAX_WIIMOTES]; + static UsbEndpoint[] usb_in = new UsbEndpoint[MAX_WIIMOTES]; + + public static byte[][] wiimote_payload = new byte[MAX_WIIMOTES][MAX_PAYLOAD]; + + private static void RequestPermission() + { + HashMap devices = manager.getDeviceList(); + for (Map.Entry pair : devices.entrySet()) + { + UsbDevice dev = (UsbDevice) pair.getValue(); + if (dev.getProductId() == NINTENDO_WIIMOTE_PRODUCT_ID && dev.getVendorId() == NINTENDO_VENDOR_ID) + { + if (!manager.hasPermission(dev)) + { + Log.warning("Requesting permission for Wiimote adapter"); + Intent intent = new Intent(); + PendingIntent pend_intent; + intent.setClass(NativeLibrary.sEmulationActivity, USBPermService.class); + pend_intent = PendingIntent.getService(NativeLibrary.sEmulationActivity, 0, intent, 0); + manager.requestPermission(dev, pend_intent); + } + } + } + } + + public static boolean QueryAdapter() + { + HashMap devices = manager.getDeviceList(); + for (Map.Entry pair : devices.entrySet()) + { + UsbDevice dev = (UsbDevice) pair.getValue(); + if (dev.getProductId() == NINTENDO_WIIMOTE_PRODUCT_ID && dev.getVendorId() == NINTENDO_VENDOR_ID) + { + if (manager.hasPermission(dev)) + return true; + else + RequestPermission(); + } + } + return false; + } + + public static int Input(int index) + { + return usb_con.bulkTransfer(usb_in[index], wiimote_payload[index], MAX_PAYLOAD, TIMEOUT); + } + + public static int Output(int index, byte[] buf, int size) + { + byte report_number = buf[0]; + + // Remove the report number from the buffer + buf = Arrays.copyOfRange(buf, 1, buf.length); + size--; + + final int LIBUSB_REQUEST_TYPE_CLASS = (1 << 5); + final int LIBUSB_RECIPIENT_INTERFACE = 0x1; + final int LIBUSB_ENDPOINT_OUT = 0; + + final int HID_SET_REPORT = 0x9; + final int HID_OUTPUT = (2 << 8); + + int write = usb_con.controlTransfer( + LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE | LIBUSB_ENDPOINT_OUT, + HID_SET_REPORT, + HID_OUTPUT | report_number, + index, + buf, size, + 1000); + + if (write < 0) + return -1; + + return write + 1; + } + + public static boolean OpenAdapter() + { + // If the adapter is already open. Don't attempt to do it again + if (usb_con != null && usb_con.getFileDescriptor() != -1) + return true; + + HashMap devices = manager.getDeviceList(); + for (Map.Entry pair : devices.entrySet()) + { + UsbDevice dev = (UsbDevice) pair.getValue(); + if (dev.getProductId() == NINTENDO_WIIMOTE_PRODUCT_ID && dev.getVendorId() == NINTENDO_VENDOR_ID) + { + if (manager.hasPermission(dev)) + { + usb_con = manager.openDevice(dev); + UsbConfiguration conf = dev.getConfiguration(0); + + Log.info("Number of configurations: " + dev.getConfigurationCount()); + Log.info("Number of Interfaces: " + dev.getInterfaceCount()); + Log.info("Number of Interfaces from conf: " + conf.getInterfaceCount()); + + // Sometimes the interface count is returned as zero. + // Means the device needs to be unplugged and plugged back in again + if (dev.getInterfaceCount() > 0) + { + for (int i = 0; i < MAX_WIIMOTES; ++i) + { + // One interface per Wiimote + usb_intf[i] = dev.getInterface(i); + usb_con.claimInterface(usb_intf[i], true); + + // One endpoint per Wiimote. Input only + // Output reports go through the control channel. + usb_in[i] = usb_intf[i].getEndpoint(0); + Log.info("Interface " + i + " endpoint count:" + usb_intf[i].getEndpointCount()); + } + return true; + } + else + { + // XXX: Message that the device was found, but it needs to be unplugged and plugged back in? + usb_con.close(); + } + } + } + } + return false; + } +} diff --git a/Source/Android/app/src/main/res/layout/fragment_ingame_menu.xml b/Source/Android/app/src/main/res/layout/fragment_ingame_menu.xml index 44093dc324..e218fac5b5 100644 --- a/Source/Android/app/src/main/res/layout/fragment_ingame_menu.xml +++ b/Source/Android/app/src/main/res/layout/fragment_ingame_menu.xml @@ -59,6 +59,11 @@ android:text="@string/settings" style="@style/InGameMenuOption"/> +