[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.
This commit is contained in:
parent
2282651fdb
commit
fe53461611
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<String, UsbDevice> devices = manager.getDeviceList();
|
||||
for (Map.Entry<String, UsbDevice> 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<String, UsbDevice> devices = manager.getDeviceList();
|
||||
for (Map.Entry<String, UsbDevice> 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<String, UsbDevice> devices = manager.getDeviceList();
|
||||
for (Map.Entry<String, UsbDevice> 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;
|
||||
}
|
||||
}
|
|
@ -59,6 +59,11 @@
|
|||
android:text="@string/settings"
|
||||
style="@style/InGameMenuOption"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/menu_refresh_wiimotes"
|
||||
android:text="@string/emulation_refresh_wiimotes"
|
||||
style="@style/InGameMenuOption"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/menu_exit"
|
||||
android:text="@string/overlay_exit_emulation"
|
||||
|
|
|
@ -346,6 +346,7 @@
|
|||
<string name="emulation_toggle_input">Toggle Touch Controls</string>
|
||||
<string name="emulation_quicksave">Quick Save</string>
|
||||
<string name="emulation_quickload">Quick Load</string>
|
||||
<string name="emulation_refresh_wiimotes">Refresh Wiimotes</string>
|
||||
|
||||
<!-- GC Adapter Menu-->
|
||||
<string name="gc_adapter_rumble">Enable Vibration</string>
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
#include "Core/Host.h"
|
||||
#include "Core/State.h"
|
||||
#include "Core/HW/Wiimote.h"
|
||||
#include "Core/HW/WiimoteReal/WiimoteReal.h"
|
||||
#include "Core/PowerPC/JitInterface.h"
|
||||
#include "Core/PowerPC/PowerPC.h"
|
||||
#include "Core/PowerPC/Profiler.h"
|
||||
|
@ -632,6 +633,10 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_SurfaceDestr
|
|||
Renderer::s_ChangedSurface.Wait();
|
||||
}
|
||||
}
|
||||
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_RefreshWiimotes(JNIEnv *env, jobject obj)
|
||||
{
|
||||
WiimoteReal::Refresh();
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_Run(JNIEnv *env, jobject obj)
|
||||
{
|
||||
|
@ -646,6 +651,8 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_Run(JNIEnv *
|
|||
UICommon::SetUserDirectory(g_set_userpath);
|
||||
UICommon::Init();
|
||||
|
||||
WiimoteReal::InitAdapterClass();
|
||||
|
||||
// No use running the loop when booting fails
|
||||
if ( BootManager::BootCore( g_filename.c_str() ) )
|
||||
{
|
||||
|
|
|
@ -256,6 +256,8 @@ elseif(UNIX)
|
|||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux" AND BLUEZ_FOUND)
|
||||
set(SRCS ${SRCS} HW/WiimoteReal/IONix.cpp)
|
||||
set(LIBS ${LIBS} bluetooth)
|
||||
elseif(ANDROID)
|
||||
set(SRCS ${SRCS} HW/WiimoteReal/IOAndroid.cpp)
|
||||
else()
|
||||
set(SRCS ${SRCS} HW/WiimoteReal/IODummy.cpp)
|
||||
endif()
|
||||
|
|
|
@ -0,0 +1,165 @@
|
|||
// Copyright 2016 Dolphin Emulator Project
|
||||
// Licensed under GPLv2+
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "Common/Event.h"
|
||||
#include "Common/Flag.h"
|
||||
#include "Common/StringUtil.h"
|
||||
#include "Common/Thread.h"
|
||||
#include "Common/Timer.h"
|
||||
#include "Common/Logging/Log.h"
|
||||
|
||||
#include "Core/HW/WiimoteReal/WiimoteReal.h"
|
||||
|
||||
// Global java_vm class
|
||||
extern JavaVM* g_java_vm;
|
||||
|
||||
namespace WiimoteReal
|
||||
{
|
||||
|
||||
// Java classes
|
||||
static jclass s_adapter_class;
|
||||
|
||||
class WiimoteAndroid final : public Wiimote
|
||||
{
|
||||
public:
|
||||
WiimoteAndroid(int index);
|
||||
~WiimoteAndroid() override;
|
||||
|
||||
protected:
|
||||
bool ConnectInternal() override;
|
||||
void DisconnectInternal() override;
|
||||
bool IsConnected() const override;
|
||||
void IOWakeup() {}
|
||||
int IORead(u8* buf) override;
|
||||
int IOWrite(u8 const* buf, size_t len) override;
|
||||
|
||||
private:
|
||||
int m_mayflash_index;
|
||||
bool is_connected = true;
|
||||
|
||||
JNIEnv* m_env;
|
||||
|
||||
jmethodID m_input_func;
|
||||
jmethodID m_output_func;
|
||||
|
||||
jbyteArray m_java_wiimote_payload;
|
||||
};
|
||||
|
||||
WiimoteScanner::WiimoteScanner()
|
||||
{}
|
||||
|
||||
WiimoteScanner::~WiimoteScanner()
|
||||
{}
|
||||
|
||||
void WiimoteScanner::Update()
|
||||
{}
|
||||
|
||||
void WiimoteScanner::FindWiimotes(std::vector<Wiimote*>& found_wiimotes, Wiimote*& found_board)
|
||||
{
|
||||
found_wiimotes.clear();
|
||||
found_board = nullptr;
|
||||
|
||||
NOTICE_LOG(WIIMOTE, "Finding Wiimotes");
|
||||
|
||||
JNIEnv* env;
|
||||
int get_env_status = g_java_vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6);
|
||||
|
||||
if (get_env_status == JNI_EDETACHED)
|
||||
g_java_vm->AttachCurrentThread(&env, nullptr);
|
||||
|
||||
jmethodID openadapter_func = env->GetStaticMethodID(s_adapter_class, "OpenAdapter", "()Z");
|
||||
jmethodID queryadapter_func = env->GetStaticMethodID(s_adapter_class, "QueryAdapter", "()Z");
|
||||
|
||||
if (env->CallStaticBooleanMethod(s_adapter_class, queryadapter_func) &&
|
||||
env->CallStaticBooleanMethod(s_adapter_class, openadapter_func))
|
||||
{
|
||||
for (int i = 0; i < MAX_WIIMOTES; ++i)
|
||||
found_wiimotes.emplace_back(new WiimoteAndroid(i));
|
||||
}
|
||||
|
||||
if (get_env_status == JNI_EDETACHED)
|
||||
g_java_vm->DetachCurrentThread();
|
||||
}
|
||||
|
||||
bool WiimoteScanner::IsReady() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
WiimoteAndroid::WiimoteAndroid(int index)
|
||||
: Wiimote(), m_mayflash_index(index)
|
||||
{
|
||||
}
|
||||
|
||||
WiimoteAndroid::~WiimoteAndroid()
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
// Connect to a Wiimote with a known address.
|
||||
bool WiimoteAndroid::ConnectInternal()
|
||||
{
|
||||
g_java_vm->AttachCurrentThread(&m_env, nullptr);
|
||||
|
||||
jfieldID payload_field = m_env->GetStaticFieldID(s_adapter_class, "wiimote_payload", "[[B");
|
||||
jobjectArray payload_object = reinterpret_cast<jobjectArray>(m_env->GetStaticObjectField(s_adapter_class, payload_field));
|
||||
m_java_wiimote_payload = (jbyteArray)m_env->GetObjectArrayElement(payload_object, m_mayflash_index);
|
||||
|
||||
// Get function pointers
|
||||
m_input_func = m_env->GetStaticMethodID(s_adapter_class, "Input", "(I)I");
|
||||
m_output_func = m_env->GetStaticMethodID(s_adapter_class, "Output", "(I[BI)I");
|
||||
|
||||
is_connected = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void WiimoteAndroid::DisconnectInternal()
|
||||
{
|
||||
g_java_vm->DetachCurrentThread();
|
||||
}
|
||||
|
||||
bool WiimoteAndroid::IsConnected() const
|
||||
{
|
||||
return is_connected;
|
||||
}
|
||||
|
||||
// positive = read packet
|
||||
// negative = didn't read packet
|
||||
// zero = error
|
||||
int WiimoteAndroid::IORead(u8* buf)
|
||||
{
|
||||
int read_size = m_env->CallStaticIntMethod(s_adapter_class, m_input_func, m_mayflash_index);
|
||||
jbyte* java_data = m_env->GetByteArrayElements(m_java_wiimote_payload, nullptr);
|
||||
memcpy(buf + 1, java_data, std::min(MAX_PAYLOAD - 1, read_size));
|
||||
buf[0] = 0xA1;
|
||||
m_env->ReleaseByteArrayElements(m_java_wiimote_payload, java_data, 0);
|
||||
return read_size <= 0 ? read_size : read_size + 1;
|
||||
}
|
||||
|
||||
|
||||
int WiimoteAndroid::IOWrite(u8 const* buf, size_t len)
|
||||
{
|
||||
jbyteArray output_array = m_env->NewByteArray(len);
|
||||
jbyte* output = m_env->GetByteArrayElements(output_array, nullptr);
|
||||
memcpy(output, buf, len);
|
||||
m_env->ReleaseByteArrayElements(output_array, output, 0);
|
||||
int written = m_env->CallStaticIntMethod(s_adapter_class, m_output_func, m_mayflash_index, output_array, len);
|
||||
m_env->DeleteLocalRef(output_array);
|
||||
return written;
|
||||
}
|
||||
|
||||
void InitAdapterClass()
|
||||
{
|
||||
JNIEnv* env;
|
||||
g_java_vm->AttachCurrentThread(&env, nullptr);
|
||||
|
||||
jclass adapter_class = env->FindClass("org/dolphinemu/dolphinemu/utils/Java_WiimoteAdapter");
|
||||
s_adapter_class = reinterpret_cast<jclass>(env->NewGlobalRef(adapter_class));
|
||||
}
|
||||
|
||||
}
|
|
@ -161,4 +161,8 @@ void ChangeWiimoteSource(unsigned int index, int source);
|
|||
bool IsValidBluetoothName(const std::string& name);
|
||||
bool IsBalanceBoardName(const std::string& name);
|
||||
|
||||
#ifdef ANDROID
|
||||
void InitAdapterClass();
|
||||
#endif
|
||||
|
||||
} // WiimoteReal
|
||||
|
|
Loading…
Reference in New Issue