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.os.Build;
11 import android.util.Log;
12 import android.content.BroadcastReceiver;
13 import android.content.Context;
14 import android.content.DialogInterface;
15 import android.content.Intent;
16 import android.content.IntentFilter;
17 import android.content.SharedPreferences;
18 import android.content.pm.PackageManager;
19 import android.hardware.usb.*;
20 import android.os.Handler;
21 import android.os.Looper;
23 import java.util.ArrayList;
24 import java.util.HashMap;
25 import java.util.Iterator;
26 import java.util.List;
28 public class HIDDeviceManager {
29 private static final String TAG = "hidapi";
30 private static final String ACTION_USB_PERMISSION = "org.libsdl.app.USB_PERMISSION";
32 private static HIDDeviceManager sManager;
33 private static int sManagerRefCount = 0;
35 public static HIDDeviceManager acquire(Context context) {
36 if (sManagerRefCount == 0) {
37 sManager = new HIDDeviceManager(context);
43 public static void release(HIDDeviceManager manager) {
44 if (manager == sManager) {
46 if (sManagerRefCount == 0) {
53 private Context mContext;
54 private HashMap<Integer, HIDDevice> mDevicesById = new HashMap<Integer, HIDDevice>();
55 private HashMap<BluetoothDevice, HIDDeviceBLESteamController> mBluetoothDevices = new HashMap<BluetoothDevice, HIDDeviceBLESteamController>();
56 private int mNextDeviceId = 0;
57 private SharedPreferences mSharedPreferences = null;
58 private boolean mIsChromebook = false;
59 private UsbManager mUsbManager;
60 private Handler mHandler;
61 private BluetoothManager mBluetoothManager;
62 private List<BluetoothDevice> mLastBluetoothDevices;
64 private final BroadcastReceiver mUsbBroadcast = new BroadcastReceiver() {
66 public void onReceive(Context context, Intent intent) {
67 String action = intent.getAction();
68 if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
69 UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
70 handleUsbDeviceAttached(usbDevice);
71 } else if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) {
72 UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
73 handleUsbDeviceDetached(usbDevice);
74 } else if (action.equals(HIDDeviceManager.ACTION_USB_PERMISSION)) {
75 UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
76 handleUsbDevicePermission(usbDevice, intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false));
81 private final BroadcastReceiver mBluetoothBroadcast = new BroadcastReceiver() {
83 public void onReceive(Context context, Intent intent) {
84 String action = intent.getAction();
85 // Bluetooth device was connected. If it was a Steam Controller, handle it
86 if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) {
87 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
88 Log.d(TAG, "Bluetooth device connected: " + device);
90 if (isSteamController(device)) {
91 connectBluetoothDevice(device);
95 // Bluetooth device was disconnected, remove from controller manager (if any)
96 if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
97 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
98 Log.d(TAG, "Bluetooth device disconnected: " + device);
100 disconnectBluetoothDevice(device);
105 private HIDDeviceManager(final Context context) {
108 HIDDeviceRegisterCallback();
110 mSharedPreferences = mContext.getSharedPreferences("hidapi", Context.MODE_PRIVATE);
111 mIsChromebook = mContext.getPackageManager().hasSystemFeature("org.chromium.arc.device_management");
113 // if (shouldClear) {
114 // SharedPreferences.Editor spedit = mSharedPreferences.edit();
120 mNextDeviceId = mSharedPreferences.getInt("next_device_id", 0);
124 public Context getContext() {
128 public int getDeviceIDForIdentifier(String identifier) {
129 SharedPreferences.Editor spedit = mSharedPreferences.edit();
131 int result = mSharedPreferences.getInt(identifier, 0);
133 result = mNextDeviceId++;
134 spedit.putInt("next_device_id", mNextDeviceId);
137 spedit.putInt(identifier, result);
142 private void initializeUSB() {
143 mUsbManager = (UsbManager)mContext.getSystemService(Context.USB_SERVICE);
144 if (mUsbManager == null) {
150 for (UsbDevice device : mUsbManager.getDeviceList().values()) {
151 Log.i(TAG,"Path: " + device.getDeviceName());
152 Log.i(TAG,"Manufacturer: " + device.getManufacturerName());
153 Log.i(TAG,"Product: " + device.getProductName());
154 Log.i(TAG,"ID: " + device.getDeviceId());
155 Log.i(TAG,"Class: " + device.getDeviceClass());
156 Log.i(TAG,"Protocol: " + device.getDeviceProtocol());
157 Log.i(TAG,"Vendor ID " + device.getVendorId());
158 Log.i(TAG,"Product ID: " + device.getProductId());
159 Log.i(TAG,"Interface count: " + device.getInterfaceCount());
160 Log.i(TAG,"---------------------------------------");
162 // Get interface details
163 for (int index = 0; index < device.getInterfaceCount(); index++) {
164 UsbInterface mUsbInterface = device.getInterface(index);
165 Log.i(TAG," ***** *****");
166 Log.i(TAG," Interface index: " + index);
167 Log.i(TAG," Interface ID: " + mUsbInterface.getId());
168 Log.i(TAG," Interface class: " + mUsbInterface.getInterfaceClass());
169 Log.i(TAG," Interface subclass: " + mUsbInterface.getInterfaceSubclass());
170 Log.i(TAG," Interface protocol: " + mUsbInterface.getInterfaceProtocol());
171 Log.i(TAG," Endpoint count: " + mUsbInterface.getEndpointCount());
173 // Get endpoint details
174 for (int epi = 0; epi < mUsbInterface.getEndpointCount(); epi++)
176 UsbEndpoint mEndpoint = mUsbInterface.getEndpoint(epi);
177 Log.i(TAG," ++++ ++++ ++++");
178 Log.i(TAG," Endpoint index: " + epi);
179 Log.i(TAG," Attributes: " + mEndpoint.getAttributes());
180 Log.i(TAG," Direction: " + mEndpoint.getDirection());
181 Log.i(TAG," Number: " + mEndpoint.getEndpointNumber());
182 Log.i(TAG," Interval: " + mEndpoint.getInterval());
183 Log.i(TAG," Packet size: " + mEndpoint.getMaxPacketSize());
184 Log.i(TAG," Type: " + mEndpoint.getType());
188 Log.i(TAG," No more devices connected.");
191 // Register for USB broadcasts and permission completions
192 IntentFilter filter = new IntentFilter();
193 filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
194 filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
195 filter.addAction(HIDDeviceManager.ACTION_USB_PERMISSION);
196 mContext.registerReceiver(mUsbBroadcast, filter);
198 for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) {
199 handleUsbDeviceAttached(usbDevice);
203 UsbManager getUSBManager() {
207 private void shutdownUSB() {
209 mContext.unregisterReceiver(mUsbBroadcast);
210 } catch (Exception e) {
211 // We may not have registered, that's okay
215 private boolean isHIDDeviceInterface(UsbDevice usbDevice, UsbInterface usbInterface) {
216 if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID) {
219 if (isXbox360Controller(usbDevice, usbInterface) || isXboxOneController(usbDevice, usbInterface)) {
225 private boolean isXbox360Controller(UsbDevice usbDevice, UsbInterface usbInterface) {
226 final int XB360_IFACE_SUBCLASS = 93;
227 final int XB360_IFACE_PROTOCOL = 1; // Wired
228 final int XB360W_IFACE_PROTOCOL = 129; // Wireless
229 final int[] SUPPORTED_VENDORS = {
231 0x044f, // Thrustmaster
240 0x1038, // SteelSeries
245 0x1532, // Razer Sabertooth
248 0x1689, // Razer Onza
249 0x1949, // Lab126, Inc.
254 if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
255 usbInterface.getInterfaceSubclass() == XB360_IFACE_SUBCLASS &&
256 (usbInterface.getInterfaceProtocol() == XB360_IFACE_PROTOCOL ||
257 usbInterface.getInterfaceProtocol() == XB360W_IFACE_PROTOCOL)) {
258 int vendor_id = usbDevice.getVendorId();
259 for (int supportedVid : SUPPORTED_VENDORS) {
260 if (vendor_id == supportedVid) {
268 private boolean isXboxOneController(UsbDevice usbDevice, UsbInterface usbInterface) {
269 final int XB1_IFACE_SUBCLASS = 71;
270 final int XB1_IFACE_PROTOCOL = 208;
271 final int[] SUPPORTED_VENDORS = {
276 0x1532, // Razer Wildcat
281 if (usbInterface.getId() == 0 &&
282 usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
283 usbInterface.getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
284 usbInterface.getInterfaceProtocol() == XB1_IFACE_PROTOCOL) {
285 int vendor_id = usbDevice.getVendorId();
286 for (int supportedVid : SUPPORTED_VENDORS) {
287 if (vendor_id == supportedVid) {
295 private void handleUsbDeviceAttached(UsbDevice usbDevice) {
296 connectHIDDeviceUSB(usbDevice);
299 private void handleUsbDeviceDetached(UsbDevice usbDevice) {
300 List<Integer> devices = new ArrayList<Integer>();
301 for (HIDDevice device : mDevicesById.values()) {
302 if (usbDevice.equals(device.getDevice())) {
303 devices.add(device.getId());
306 for (int id : devices) {
307 HIDDevice device = mDevicesById.get(id);
308 mDevicesById.remove(id);
310 HIDDeviceDisconnected(id);
314 private void handleUsbDevicePermission(UsbDevice usbDevice, boolean permission_granted) {
315 for (HIDDevice device : mDevicesById.values()) {
316 if (usbDevice.equals(device.getDevice())) {
317 boolean opened = false;
318 if (permission_granted) {
319 opened = device.open();
321 HIDDeviceOpenResult(device.getId(), opened);
326 private void connectHIDDeviceUSB(UsbDevice usbDevice) {
327 synchronized (this) {
328 int interface_mask = 0;
329 for (int interface_index = 0; interface_index < usbDevice.getInterfaceCount(); interface_index++) {
330 UsbInterface usbInterface = usbDevice.getInterface(interface_index);
331 if (isHIDDeviceInterface(usbDevice, usbInterface)) {
332 // Check to see if we've already added this interface
333 // This happens with the Xbox Series X controller which has a duplicate interface 0, which is inactive
334 int interface_id = usbInterface.getId();
335 if ((interface_mask & (1 << interface_id)) != 0) {
338 interface_mask |= (1 << interface_id);
340 HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_index);
341 int id = device.getId();
342 mDevicesById.put(id, device);
343 HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol());
349 private void initializeBluetooth() {
350 Log.d(TAG, "Initializing Bluetooth");
352 if (Build.VERSION.SDK_INT <= 30 &&
353 mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
354 Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH");
358 if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) || (Build.VERSION.SDK_INT < 18)) {
359 Log.d(TAG, "Couldn't initialize Bluetooth, this version of Android does not support Bluetooth LE");
363 // Find bonded bluetooth controllers and create SteamControllers for them
364 mBluetoothManager = (BluetoothManager)mContext.getSystemService(Context.BLUETOOTH_SERVICE);
365 if (mBluetoothManager == null) {
366 // This device doesn't support Bluetooth.
370 BluetoothAdapter btAdapter = mBluetoothManager.getAdapter();
371 if (btAdapter == null) {
372 // This device has Bluetooth support in the codebase, but has no available adapters.
376 // Get our bonded devices.
377 for (BluetoothDevice device : btAdapter.getBondedDevices()) {
379 Log.d(TAG, "Bluetooth device available: " + device);
380 if (isSteamController(device)) {
381 connectBluetoothDevice(device);
386 // NOTE: These don't work on Chromebooks, to my undying dismay.
387 IntentFilter filter = new IntentFilter();
388 filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
389 filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
390 mContext.registerReceiver(mBluetoothBroadcast, filter);
393 mHandler = new Handler(Looper.getMainLooper());
394 mLastBluetoothDevices = new ArrayList<BluetoothDevice>();
396 // final HIDDeviceManager finalThis = this;
397 // mHandler.postDelayed(new Runnable() {
399 // public void run() {
400 // finalThis.chromebookConnectionHandler();
406 private void shutdownBluetooth() {
408 mContext.unregisterReceiver(mBluetoothBroadcast);
409 } catch (Exception e) {
410 // We may not have registered, that's okay
414 // Chromebooks do not pass along ACTION_ACL_CONNECTED / ACTION_ACL_DISCONNECTED properly.
415 // This function provides a sort of dummy version of that, watching for changes in the
416 // connected devices and attempting to add controllers as things change.
417 public void chromebookConnectionHandler() {
418 if (!mIsChromebook) {
422 ArrayList<BluetoothDevice> disconnected = new ArrayList<BluetoothDevice>();
423 ArrayList<BluetoothDevice> connected = new ArrayList<BluetoothDevice>();
425 List<BluetoothDevice> currentConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT);
427 for (BluetoothDevice bluetoothDevice : currentConnected) {
428 if (!mLastBluetoothDevices.contains(bluetoothDevice)) {
429 connected.add(bluetoothDevice);
432 for (BluetoothDevice bluetoothDevice : mLastBluetoothDevices) {
433 if (!currentConnected.contains(bluetoothDevice)) {
434 disconnected.add(bluetoothDevice);
438 mLastBluetoothDevices = currentConnected;
440 for (BluetoothDevice bluetoothDevice : disconnected) {
441 disconnectBluetoothDevice(bluetoothDevice);
443 for (BluetoothDevice bluetoothDevice : connected) {
444 connectBluetoothDevice(bluetoothDevice);
447 final HIDDeviceManager finalThis = this;
448 mHandler.postDelayed(new Runnable() {
451 finalThis.chromebookConnectionHandler();
456 public boolean connectBluetoothDevice(BluetoothDevice bluetoothDevice) {
457 Log.v(TAG, "connectBluetoothDevice device=" + bluetoothDevice);
458 synchronized (this) {
459 if (mBluetoothDevices.containsKey(bluetoothDevice)) {
460 Log.v(TAG, "Steam controller with address " + bluetoothDevice + " already exists, attempting reconnect");
462 HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
467 HIDDeviceBLESteamController device = new HIDDeviceBLESteamController(this, bluetoothDevice);
468 int id = device.getId();
469 mBluetoothDevices.put(bluetoothDevice, device);
470 mDevicesById.put(id, device);
472 // The Steam Controller will mark itself connected once initialization is complete
477 public void disconnectBluetoothDevice(BluetoothDevice bluetoothDevice) {
478 synchronized (this) {
479 HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
483 int id = device.getId();
484 mBluetoothDevices.remove(bluetoothDevice);
485 mDevicesById.remove(id);
487 HIDDeviceDisconnected(id);
491 public boolean isSteamController(BluetoothDevice bluetoothDevice) {
492 // Sanity check. If you pass in a null device, by definition it is never a Steam Controller.
493 if (bluetoothDevice == null) {
497 // If the device has no local name, we really don't want to try an equality check against it.
498 if (bluetoothDevice.getName() == null) {
502 return bluetoothDevice.getName().equals("SteamController") && ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) != 0);
505 private void close() {
508 synchronized (this) {
509 for (HIDDevice device : mDevicesById.values()) {
512 mDevicesById.clear();
513 mBluetoothDevices.clear();
514 HIDDeviceReleaseCallback();
518 public void setFrozen(boolean frozen) {
519 synchronized (this) {
520 for (HIDDevice device : mDevicesById.values()) {
521 device.setFrozen(frozen);
526 //////////////////////////////////////////////////////////////////////////////////////////////////////
527 //////////////////////////////////////////////////////////////////////////////////////////////////////
528 //////////////////////////////////////////////////////////////////////////////////////////////////////
530 private HIDDevice getDevice(int id) {
531 synchronized (this) {
532 HIDDevice result = mDevicesById.get(id);
533 if (result == null) {
534 Log.v(TAG, "No device for id: " + id);
535 Log.v(TAG, "Available devices: " + mDevicesById.keySet());
541 //////////////////////////////////////////////////////////////////////////////////////////////////////
542 ////////// JNI interface functions
543 //////////////////////////////////////////////////////////////////////////////////////////////////////
545 public boolean initialize(boolean usb, boolean bluetooth) {
546 Log.v(TAG, "initialize(" + usb + ", " + bluetooth + ")");
552 initializeBluetooth();
557 public boolean openDevice(int deviceID) {
558 Log.v(TAG, "openDevice deviceID=" + deviceID);
559 HIDDevice device = getDevice(deviceID);
560 if (device == null) {
561 HIDDeviceDisconnected(deviceID);
565 // Look to see if this is a USB device and we have permission to access it
566 UsbDevice usbDevice = device.getDevice();
567 if (usbDevice != null && !mUsbManager.hasPermission(usbDevice)) {
568 HIDDeviceOpenPending(deviceID);
570 final int FLAG_MUTABLE = 0x02000000; // PendingIntent.FLAG_MUTABLE, but don't require SDK 31
572 if (Build.VERSION.SDK_INT >= 31) {
573 flags = FLAG_MUTABLE;
577 mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, new Intent(HIDDeviceManager.ACTION_USB_PERMISSION), flags));
578 } catch (Exception e) {
579 Log.v(TAG, "Couldn't request permission for USB device " + usbDevice);
580 HIDDeviceOpenResult(deviceID, false);
586 return device.open();
587 } catch (Exception e) {
588 Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
593 public int sendOutputReport(int deviceID, byte[] report) {
595 //Log.v(TAG, "sendOutputReport deviceID=" + deviceID + " length=" + report.length);
597 device = getDevice(deviceID);
598 if (device == null) {
599 HIDDeviceDisconnected(deviceID);
603 return device.sendOutputReport(report);
604 } catch (Exception e) {
605 Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
610 public int sendFeatureReport(int deviceID, byte[] report) {
612 //Log.v(TAG, "sendFeatureReport deviceID=" + deviceID + " length=" + report.length);
614 device = getDevice(deviceID);
615 if (device == null) {
616 HIDDeviceDisconnected(deviceID);
620 return device.sendFeatureReport(report);
621 } catch (Exception e) {
622 Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
627 public boolean getFeatureReport(int deviceID, byte[] report) {
629 //Log.v(TAG, "getFeatureReport deviceID=" + deviceID);
631 device = getDevice(deviceID);
632 if (device == null) {
633 HIDDeviceDisconnected(deviceID);
637 return device.getFeatureReport(report);
638 } catch (Exception e) {
639 Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
644 public void closeDevice(int deviceID) {
646 Log.v(TAG, "closeDevice deviceID=" + deviceID);
648 device = getDevice(deviceID);
649 if (device == null) {
650 HIDDeviceDisconnected(deviceID);
655 } catch (Exception e) {
656 Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
661 //////////////////////////////////////////////////////////////////////////////////////////////////////
662 /////////////// Native methods
663 //////////////////////////////////////////////////////////////////////////////////////////////////////
665 private native void HIDDeviceRegisterCallback();
666 private native void HIDDeviceReleaseCallback();
668 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);
669 native void HIDDeviceOpenPending(int deviceID);
670 native void HIDDeviceOpenResult(int deviceID, boolean opened);
671 native void HIDDeviceDisconnected(int deviceID);
673 native void HIDDeviceInputReport(int deviceID, byte[] report);
674 native void HIDDeviceFeatureReport(int deviceID, byte[] report);