1 package org.libsdl.app;
3 import android.app.Activity;
4 import android.app.AlertDialog;
5 import android.app.PendingIntent;
6 import android.bluetooth.BluetoothAdapter;
7 import android.bluetooth.BluetoothDevice;
8 import android.bluetooth.BluetoothManager;
9 import android.bluetooth.BluetoothProfile;
10 import android.util.Log;
11 import android.content.BroadcastReceiver;
12 import android.content.Context;
13 import android.content.DialogInterface;
14 import android.content.Intent;
15 import android.content.IntentFilter;
16 import android.content.SharedPreferences;
17 import android.content.pm.PackageManager;
18 import android.hardware.usb.*;
19 import android.os.Handler;
20 import android.os.Looper;
22 import java.util.ArrayList;
23 import java.util.HashMap;
24 import java.util.Iterator;
25 import java.util.List;
27 public class HIDDeviceManager {
28 private static final String TAG = "hidapi";
29 private static final String ACTION_USB_PERMISSION = "org.libsdl.app.USB_PERMISSION";
31 private static HIDDeviceManager sManager;
32 private static int sManagerRefCount = 0;
34 public static HIDDeviceManager acquire(Context context) {
35 if (sManagerRefCount == 0) {
36 sManager = new HIDDeviceManager(context);
42 public static void release(HIDDeviceManager manager) {
43 if (manager == sManager) {
45 if (sManagerRefCount == 0) {
52 private Context mContext;
53 private HashMap<Integer, HIDDevice> mDevicesById = new HashMap<Integer, HIDDevice>();
54 private HashMap<BluetoothDevice, HIDDeviceBLESteamController> mBluetoothDevices = new HashMap<BluetoothDevice, HIDDeviceBLESteamController>();
55 private int mNextDeviceId = 0;
56 private SharedPreferences mSharedPreferences = null;
57 private boolean mIsChromebook = false;
58 private UsbManager mUsbManager;
59 private Handler mHandler;
60 private BluetoothManager mBluetoothManager;
61 private List<BluetoothDevice> mLastBluetoothDevices;
63 private final BroadcastReceiver mUsbBroadcast = new BroadcastReceiver() {
65 public void onReceive(Context context, Intent intent) {
66 String action = intent.getAction();
67 if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
68 UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
69 handleUsbDeviceAttached(usbDevice);
70 } else if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) {
71 UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
72 handleUsbDeviceDetached(usbDevice);
73 } else if (action.equals(HIDDeviceManager.ACTION_USB_PERMISSION)) {
74 UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
75 handleUsbDevicePermission(usbDevice, intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false));
80 private final BroadcastReceiver mBluetoothBroadcast = new BroadcastReceiver() {
82 public void onReceive(Context context, Intent intent) {
83 String action = intent.getAction();
84 // Bluetooth device was connected. If it was a Steam Controller, handle it
85 if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) {
86 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
87 Log.d(TAG, "Bluetooth device connected: " + device);
89 if (isSteamController(device)) {
90 connectBluetoothDevice(device);
94 // Bluetooth device was disconnected, remove from controller manager (if any)
95 if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
96 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
97 Log.d(TAG, "Bluetooth device disconnected: " + device);
99 disconnectBluetoothDevice(device);
104 private HIDDeviceManager(final Context context) {
107 // Make sure we have the HIDAPI library loaded with the native functions
109 SDL.loadLibrary("hidapi");
110 } catch (Throwable e) {
111 Log.w(TAG, "Couldn't load hidapi: " + e.toString());
113 AlertDialog.Builder builder = new AlertDialog.Builder(context);
114 builder.setCancelable(false);
115 builder.setTitle("SDL HIDAPI Error");
116 builder.setMessage("Please report the following error to the SDL maintainers: " + e.getMessage());
117 builder.setNegativeButton("Quit", new DialogInterface.OnClickListener() {
119 public void onClick(DialogInterface dialog, int which) {
121 // If our context is an activity, exit rather than crashing when we can't
122 // call our native functions.
123 Activity activity = (Activity)context;
127 catch (ClassCastException cce) {
128 // Context wasn't an activity, there's nothing we can do. Give up and return.
137 HIDDeviceRegisterCallback();
139 mSharedPreferences = mContext.getSharedPreferences("hidapi", Context.MODE_PRIVATE);
140 mIsChromebook = mContext.getPackageManager().hasSystemFeature("org.chromium.arc.device_management");
142 // if (shouldClear) {
143 // SharedPreferences.Editor spedit = mSharedPreferences.edit();
149 mNextDeviceId = mSharedPreferences.getInt("next_device_id", 0);
153 initializeBluetooth();
156 public Context getContext() {
160 public int getDeviceIDForIdentifier(String identifier) {
161 SharedPreferences.Editor spedit = mSharedPreferences.edit();
163 int result = mSharedPreferences.getInt(identifier, 0);
165 result = mNextDeviceId++;
166 spedit.putInt("next_device_id", mNextDeviceId);
169 spedit.putInt(identifier, result);
174 private void initializeUSB() {
175 mUsbManager = (UsbManager)mContext.getSystemService(Context.USB_SERVICE);
179 for (UsbDevice device : mUsbManager.getDeviceList().values()) {
180 Log.i(TAG,"Path: " + device.getDeviceName());
181 Log.i(TAG,"Manufacturer: " + device.getManufacturerName());
182 Log.i(TAG,"Product: " + device.getProductName());
183 Log.i(TAG,"ID: " + device.getDeviceId());
184 Log.i(TAG,"Class: " + device.getDeviceClass());
185 Log.i(TAG,"Protocol: " + device.getDeviceProtocol());
186 Log.i(TAG,"Vendor ID " + device.getVendorId());
187 Log.i(TAG,"Product ID: " + device.getProductId());
188 Log.i(TAG,"Interface count: " + device.getInterfaceCount());
189 Log.i(TAG,"---------------------------------------");
191 // Get interface details
192 for (int index = 0; index < device.getInterfaceCount(); index++) {
193 UsbInterface mUsbInterface = device.getInterface(index);
194 Log.i(TAG," ***** *****");
195 Log.i(TAG," Interface index: " + index);
196 Log.i(TAG," Interface ID: " + mUsbInterface.getId());
197 Log.i(TAG," Interface class: " + mUsbInterface.getInterfaceClass());
198 Log.i(TAG," Interface subclass: " + mUsbInterface.getInterfaceSubclass());
199 Log.i(TAG," Interface protocol: " + mUsbInterface.getInterfaceProtocol());
200 Log.i(TAG," Endpoint count: " + mUsbInterface.getEndpointCount());
202 // Get endpoint details
203 for (int epi = 0; epi < mUsbInterface.getEndpointCount(); epi++)
205 UsbEndpoint mEndpoint = mUsbInterface.getEndpoint(epi);
206 Log.i(TAG," ++++ ++++ ++++");
207 Log.i(TAG," Endpoint index: " + epi);
208 Log.i(TAG," Attributes: " + mEndpoint.getAttributes());
209 Log.i(TAG," Direction: " + mEndpoint.getDirection());
210 Log.i(TAG," Number: " + mEndpoint.getEndpointNumber());
211 Log.i(TAG," Interval: " + mEndpoint.getInterval());
212 Log.i(TAG," Packet size: " + mEndpoint.getMaxPacketSize());
213 Log.i(TAG," Type: " + mEndpoint.getType());
217 Log.i(TAG," No more devices connected.");
220 // Register for USB broadcasts and permission completions
221 IntentFilter filter = new IntentFilter();
222 filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
223 filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
224 filter.addAction(HIDDeviceManager.ACTION_USB_PERMISSION);
225 mContext.registerReceiver(mUsbBroadcast, filter);
227 for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) {
228 handleUsbDeviceAttached(usbDevice);
232 UsbManager getUSBManager() {
236 private void shutdownUSB() {
238 mContext.unregisterReceiver(mUsbBroadcast);
239 } catch (Exception e) {
240 // We may not have registered, that's okay
244 private boolean isHIDDeviceInterface(UsbDevice usbDevice, UsbInterface usbInterface) {
245 if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID) {
248 if (isXbox360Controller(usbDevice, usbInterface) || isXboxOneController(usbDevice, usbInterface)) {
254 private boolean isXbox360Controller(UsbDevice usbDevice, UsbInterface usbInterface) {
255 final int XB360_IFACE_SUBCLASS = 93;
256 final int XB360_IFACE_PROTOCOL = 1; // Wired
257 final int XB360W_IFACE_PROTOCOL = 129; // Wireless
258 final int[] SUPPORTED_VENDORS = {
260 0x044f, // Thrustmaster
269 0x1038, // SteelSeries
274 0x1532, // Razer Sabertooth
277 0x1689, // Razer Onza
282 if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
283 usbInterface.getInterfaceSubclass() == XB360_IFACE_SUBCLASS &&
284 (usbInterface.getInterfaceProtocol() == XB360_IFACE_PROTOCOL ||
285 usbInterface.getInterfaceProtocol() == XB360W_IFACE_PROTOCOL)) {
286 int vendor_id = usbDevice.getVendorId();
287 for (int supportedVid : SUPPORTED_VENDORS) {
288 if (vendor_id == supportedVid) {
296 private boolean isXboxOneController(UsbDevice usbDevice, UsbInterface usbInterface) {
297 final int XB1_IFACE_SUBCLASS = 71;
298 final int XB1_IFACE_PROTOCOL = 208;
299 final int[] SUPPORTED_VENDORS = {
304 0x1532, // Razer Wildcat
309 if (usbInterface.getId() == 0 &&
310 usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
311 usbInterface.getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
312 usbInterface.getInterfaceProtocol() == XB1_IFACE_PROTOCOL) {
313 int vendor_id = usbDevice.getVendorId();
314 for (int supportedVid : SUPPORTED_VENDORS) {
315 if (vendor_id == supportedVid) {
323 private void handleUsbDeviceAttached(UsbDevice usbDevice) {
324 connectHIDDeviceUSB(usbDevice);
327 private void handleUsbDeviceDetached(UsbDevice usbDevice) {
328 List<Integer> devices = new ArrayList<Integer>();
329 for (HIDDevice device : mDevicesById.values()) {
330 if (usbDevice.equals(device.getDevice())) {
331 devices.add(device.getId());
334 for (int id : devices) {
335 HIDDevice device = mDevicesById.get(id);
336 mDevicesById.remove(id);
338 HIDDeviceDisconnected(id);
342 private void handleUsbDevicePermission(UsbDevice usbDevice, boolean permission_granted) {
343 for (HIDDevice device : mDevicesById.values()) {
344 if (usbDevice.equals(device.getDevice())) {
345 boolean opened = false;
346 if (permission_granted) {
347 opened = device.open();
349 HIDDeviceOpenResult(device.getId(), opened);
354 private void connectHIDDeviceUSB(UsbDevice usbDevice) {
355 synchronized (this) {
356 for (int interface_index = 0; interface_index < usbDevice.getInterfaceCount(); interface_index++) {
357 UsbInterface usbInterface = usbDevice.getInterface(interface_index);
358 if (isHIDDeviceInterface(usbDevice, usbInterface)) {
359 HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_index);
360 int id = device.getId();
361 mDevicesById.put(id, device);
362 HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol());
368 private void initializeBluetooth() {
369 Log.d(TAG, "Initializing Bluetooth");
371 if (mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
372 Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH");
376 // Find bonded bluetooth controllers and create SteamControllers for them
377 mBluetoothManager = (BluetoothManager)mContext.getSystemService(Context.BLUETOOTH_SERVICE);
378 if (mBluetoothManager == null) {
379 // This device doesn't support Bluetooth.
383 BluetoothAdapter btAdapter = mBluetoothManager.getAdapter();
384 if (btAdapter == null) {
385 // This device has Bluetooth support in the codebase, but has no available adapters.
389 // Get our bonded devices.
390 for (BluetoothDevice device : btAdapter.getBondedDevices()) {
392 Log.d(TAG, "Bluetooth device available: " + device);
393 if (isSteamController(device)) {
394 connectBluetoothDevice(device);
399 // NOTE: These don't work on Chromebooks, to my undying dismay.
400 IntentFilter filter = new IntentFilter();
401 filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
402 filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
403 mContext.registerReceiver(mBluetoothBroadcast, filter);
406 mHandler = new Handler(Looper.getMainLooper());
407 mLastBluetoothDevices = new ArrayList<BluetoothDevice>();
409 // final HIDDeviceManager finalThis = this;
410 // mHandler.postDelayed(new Runnable() {
412 // public void run() {
413 // finalThis.chromebookConnectionHandler();
419 private void shutdownBluetooth() {
421 mContext.unregisterReceiver(mBluetoothBroadcast);
422 } catch (Exception e) {
423 // We may not have registered, that's okay
427 // Chromebooks do not pass along ACTION_ACL_CONNECTED / ACTION_ACL_DISCONNECTED properly.
428 // This function provides a sort of dummy version of that, watching for changes in the
429 // connected devices and attempting to add controllers as things change.
430 public void chromebookConnectionHandler() {
431 if (!mIsChromebook) {
435 ArrayList<BluetoothDevice> disconnected = new ArrayList<BluetoothDevice>();
436 ArrayList<BluetoothDevice> connected = new ArrayList<BluetoothDevice>();
438 List<BluetoothDevice> currentConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT);
440 for (BluetoothDevice bluetoothDevice : currentConnected) {
441 if (!mLastBluetoothDevices.contains(bluetoothDevice)) {
442 connected.add(bluetoothDevice);
445 for (BluetoothDevice bluetoothDevice : mLastBluetoothDevices) {
446 if (!currentConnected.contains(bluetoothDevice)) {
447 disconnected.add(bluetoothDevice);
451 mLastBluetoothDevices = currentConnected;
453 for (BluetoothDevice bluetoothDevice : disconnected) {
454 disconnectBluetoothDevice(bluetoothDevice);
456 for (BluetoothDevice bluetoothDevice : connected) {
457 connectBluetoothDevice(bluetoothDevice);
460 final HIDDeviceManager finalThis = this;
461 mHandler.postDelayed(new Runnable() {
464 finalThis.chromebookConnectionHandler();
469 public boolean connectBluetoothDevice(BluetoothDevice bluetoothDevice) {
470 Log.v(TAG, "connectBluetoothDevice device=" + bluetoothDevice);
471 synchronized (this) {
472 if (mBluetoothDevices.containsKey(bluetoothDevice)) {
473 Log.v(TAG, "Steam controller with address " + bluetoothDevice + " already exists, attempting reconnect");
475 HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
480 HIDDeviceBLESteamController device = new HIDDeviceBLESteamController(this, bluetoothDevice);
481 int id = device.getId();
482 mBluetoothDevices.put(bluetoothDevice, device);
483 mDevicesById.put(id, device);
485 // The Steam Controller will mark itself connected once initialization is complete
490 public void disconnectBluetoothDevice(BluetoothDevice bluetoothDevice) {
491 synchronized (this) {
492 HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
496 int id = device.getId();
497 mBluetoothDevices.remove(bluetoothDevice);
498 mDevicesById.remove(id);
500 HIDDeviceDisconnected(id);
504 public boolean isSteamController(BluetoothDevice bluetoothDevice) {
505 // Sanity check. If you pass in a null device, by definition it is never a Steam Controller.
506 if (bluetoothDevice == null) {
510 // If the device has no local name, we really don't want to try an equality check against it.
511 if (bluetoothDevice.getName() == null) {
515 return bluetoothDevice.getName().equals("SteamController") && ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) != 0);
518 private void close() {
521 synchronized (this) {
522 for (HIDDevice device : mDevicesById.values()) {
525 mDevicesById.clear();
526 mBluetoothDevices.clear();
527 HIDDeviceReleaseCallback();
531 public void setFrozen(boolean frozen) {
532 synchronized (this) {
533 for (HIDDevice device : mDevicesById.values()) {
534 device.setFrozen(frozen);
539 //////////////////////////////////////////////////////////////////////////////////////////////////////
540 //////////////////////////////////////////////////////////////////////////////////////////////////////
541 //////////////////////////////////////////////////////////////////////////////////////////////////////
543 private HIDDevice getDevice(int id) {
544 synchronized (this) {
545 HIDDevice result = mDevicesById.get(id);
546 if (result == null) {
547 Log.v(TAG, "No device for id: " + id);
548 Log.v(TAG, "Available devices: " + mDevicesById.keySet());
554 //////////////////////////////////////////////////////////////////////////////////////////////////////
555 ////////// JNI interface functions
556 //////////////////////////////////////////////////////////////////////////////////////////////////////
558 public boolean openDevice(int deviceID) {
559 Log.v(TAG, "openDevice deviceID=" + deviceID);
560 HIDDevice device = getDevice(deviceID);
561 if (device == null) {
562 HIDDeviceDisconnected(deviceID);
566 // Look to see if this is a USB device and we have permission to access it
567 UsbDevice usbDevice = device.getDevice();
568 if (usbDevice != null && !mUsbManager.hasPermission(usbDevice)) {
569 HIDDeviceOpenPending(deviceID);
571 mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, new Intent(HIDDeviceManager.ACTION_USB_PERMISSION), 0));
572 } catch (Exception e) {
573 Log.v(TAG, "Couldn't request permission for USB device " + usbDevice);
574 HIDDeviceOpenResult(deviceID, false);
580 return device.open();
581 } catch (Exception e) {
582 Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
587 public int sendOutputReport(int deviceID, byte[] report) {
589 //Log.v(TAG, "sendOutputReport deviceID=" + deviceID + " length=" + report.length);
591 device = getDevice(deviceID);
592 if (device == null) {
593 HIDDeviceDisconnected(deviceID);
597 return device.sendOutputReport(report);
598 } catch (Exception e) {
599 Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
604 public int sendFeatureReport(int deviceID, byte[] report) {
606 //Log.v(TAG, "sendFeatureReport deviceID=" + deviceID + " length=" + report.length);
608 device = getDevice(deviceID);
609 if (device == null) {
610 HIDDeviceDisconnected(deviceID);
614 return device.sendFeatureReport(report);
615 } catch (Exception e) {
616 Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
621 public boolean getFeatureReport(int deviceID, byte[] report) {
623 //Log.v(TAG, "getFeatureReport deviceID=" + deviceID);
625 device = getDevice(deviceID);
626 if (device == null) {
627 HIDDeviceDisconnected(deviceID);
631 return device.getFeatureReport(report);
632 } catch (Exception e) {
633 Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
638 public void closeDevice(int deviceID) {
640 Log.v(TAG, "closeDevice deviceID=" + deviceID);
642 device = getDevice(deviceID);
643 if (device == null) {
644 HIDDeviceDisconnected(deviceID);
649 } catch (Exception e) {
650 Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
655 //////////////////////////////////////////////////////////////////////////////////////////////////////
656 /////////////// Native methods
657 //////////////////////////////////////////////////////////////////////////////////////////////////////
659 private native void HIDDeviceRegisterCallback();
660 private native void HIDDeviceReleaseCallback();
662 native void HIDDeviceConnected(int deviceID, String identifier, int vendorId, int productId, String serial_number, int release_number, String manufacturer_string, String product_string, int interface_number, int interface_class, int interface_subclass, int interface_protocol);
663 native void HIDDeviceOpenPending(int deviceID);
664 native void HIDDeviceOpenResult(int deviceID, boolean opened);
665 native void HIDDeviceDisconnected(int deviceID);
667 native void HIDDeviceInputReport(int deviceID, byte[] report);
668 native void HIDDeviceFeatureReport(int deviceID, byte[] report);