[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:
Ryan Houdek 2016-02-04 18:31:36 -06:00
parent 2282651fdb
commit fe53461611
9 changed files with 351 additions and 0 deletions

View file

@ -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

View file

@ -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();

View file

@ -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;
}
}

View file

@ -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"

View file

@ -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>

View file

@ -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() ) )
{

View file

@ -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()

View file

@ -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));
}
}

View file

@ -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