1 package org.libsdl.app;
3 import android.content.Context;
4 import android.bluetooth.BluetoothDevice;
5 import android.bluetooth.BluetoothGatt;
6 import android.bluetooth.BluetoothGattCallback;
7 import android.bluetooth.BluetoothGattCharacteristic;
8 import android.bluetooth.BluetoothGattDescriptor;
9 import android.bluetooth.BluetoothManager;
10 import android.bluetooth.BluetoothProfile;
11 import android.bluetooth.BluetoothGattService;
12 import android.hardware.usb.UsbDevice;
13 import android.os.Handler;
14 import android.os.Looper;
15 import android.util.Log;
18 //import com.android.internal.util.HexDump;
20 import java.lang.Runnable;
21 import java.util.Arrays;
22 import java.util.LinkedList;
23 import java.util.UUID;
25 class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice {
27 private static final String TAG = "hidapi";
28 private HIDDeviceManager mManager;
29 private BluetoothDevice mDevice;
30 private int mDeviceId;
31 private BluetoothGatt mGatt;
32 private boolean mIsRegistered = false;
33 private boolean mIsConnected = false;
34 private boolean mIsChromebook = false;
35 private boolean mIsReconnecting = false;
36 private boolean mFrozen = false;
37 private LinkedList<GattOperation> mOperations;
38 GattOperation mCurrentOperation = null;
39 private Handler mHandler;
41 private static final int TRANSPORT_AUTO = 0;
42 private static final int TRANSPORT_BREDR = 1;
43 private static final int TRANSPORT_LE = 2;
45 private static final int CHROMEBOOK_CONNECTION_CHECK_INTERVAL = 10000;
47 static public final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3");
48 static public final UUID inputCharacteristic = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3");
49 static public final UUID reportCharacteristic = UUID.fromString("100F6C34-1735-4313-B402-38567131E5F3");
50 static private final byte[] enterValveMode = new byte[] { (byte)0xC0, (byte)0x87, 0x03, 0x08, 0x07, 0x00 };
52 static class GattOperation {
53 private enum Operation {
63 boolean mResult = true;
65 private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) {
71 private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) {
79 // This is executed in main thread
80 BluetoothGattCharacteristic chr;
84 chr = getCharacteristic(mUuid);
85 //Log.v(TAG, "Reading characteristic " + chr.getUuid());
86 if (!mGatt.readCharacteristic(chr)) {
87 Log.e(TAG, "Unable to read characteristic " + mUuid.toString());
94 chr = getCharacteristic(mUuid);
95 //Log.v(TAG, "Writing characteristic " + chr.getUuid() + " value=" + HexDump.toHexString(value));
97 if (!mGatt.writeCharacteristic(chr)) {
98 Log.e(TAG, "Unable to write characteristic " + mUuid.toString());
104 case ENABLE_NOTIFICATION:
105 chr = getCharacteristic(mUuid);
106 //Log.v(TAG, "Writing descriptor of " + chr.getUuid());
108 BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
110 int properties = chr.getProperties();
112 if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) == BluetoothGattCharacteristic.PROPERTY_NOTIFY) {
113 value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
114 } else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) == BluetoothGattCharacteristic.PROPERTY_INDICATE) {
115 value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE;
117 Log.e(TAG, "Unable to start notifications on input characteristic");
122 mGatt.setCharacteristicNotification(chr, true);
123 cccd.setValue(value);
124 if (!mGatt.writeDescriptor(cccd)) {
125 Log.e(TAG, "Unable to write descriptor " + mUuid.toString());
135 public boolean finish() {
139 private BluetoothGattCharacteristic getCharacteristic(UUID uuid) {
140 BluetoothGattService valveService = mGatt.getService(steamControllerService);
141 if (valveService == null)
143 return valveService.getCharacteristic(uuid);
146 static public GattOperation readCharacteristic(BluetoothGatt gatt, UUID uuid) {
147 return new GattOperation(gatt, Operation.CHR_READ, uuid);
150 static public GattOperation writeCharacteristic(BluetoothGatt gatt, UUID uuid, byte[] value) {
151 return new GattOperation(gatt, Operation.CHR_WRITE, uuid, value);
154 static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) {
155 return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid);
159 public HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) {
162 mDeviceId = mManager.getDeviceIDForIdentifier(getIdentifier());
163 mIsRegistered = false;
164 mIsChromebook = mManager.getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management");
165 mOperations = new LinkedList<GattOperation>();
166 mHandler = new Handler(Looper.getMainLooper());
168 mGatt = connectGatt();
169 // final HIDDeviceBLESteamController finalThis = this;
170 // mHandler.postDelayed(new Runnable() {
172 // public void run() {
173 // finalThis.checkConnectionForChromebookIssue();
175 // }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
178 public String getIdentifier() {
179 return String.format("SteamController.%s", mDevice.getAddress());
182 public BluetoothGatt getGatt() {
186 // Because on Chromebooks we show up as a dual-mode device, it will attempt to connect TRANSPORT_AUTO, which will use TRANSPORT_BREDR instead
187 // of TRANSPORT_LE. Let's force ourselves to connect low energy.
188 private BluetoothGatt connectGatt(boolean managed) {
189 if (Build.VERSION.SDK_INT >= 23) {
191 return mDevice.connectGatt(mManager.getContext(), managed, this, TRANSPORT_LE);
192 } catch (Exception e) {
193 return mDevice.connectGatt(mManager.getContext(), managed, this);
196 return mDevice.connectGatt(mManager.getContext(), managed, this);
200 private BluetoothGatt connectGatt() {
201 return connectGatt(false);
204 protected int getConnectionState() {
206 Context context = mManager.getContext();
207 if (context == null) {
208 // We are lacking any context to get our Bluetooth information. We'll just assume disconnected.
209 return BluetoothProfile.STATE_DISCONNECTED;
212 BluetoothManager btManager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE);
213 if (btManager == null) {
214 // This device doesn't support Bluetooth. We should never be here, because how did
215 // we instantiate a device to start with?
216 return BluetoothProfile.STATE_DISCONNECTED;
219 return btManager.getConnectionState(mDevice, BluetoothProfile.GATT);
222 public void reconnect() {
224 if (getConnectionState() != BluetoothProfile.STATE_CONNECTED) {
226 mGatt = connectGatt();
231 protected void checkConnectionForChromebookIssue() {
232 if (!mIsChromebook) {
233 // We only do this on Chromebooks, because otherwise it's really annoying to just attempt
238 int connectionState = getConnectionState();
240 switch (connectionState) {
241 case BluetoothProfile.STATE_CONNECTED:
243 // We are in the Bad Chromebook Place. We can force a disconnect
244 // to try to recover.
245 Log.v(TAG, "Chromebook: We are in a very bad state; the controller shows as connected in the underlying Bluetooth layer, but we never received a callback. Forcing a reconnect.");
246 mIsReconnecting = true;
248 mGatt = connectGatt(false);
251 else if (!isRegistered()) {
252 if (mGatt.getServices().size() > 0) {
253 Log.v(TAG, "Chromebook: We are connected to a controller, but never got our registration. Trying to recover.");
257 Log.v(TAG, "Chromebook: We are connected to a controller, but never discovered services. Trying to recover.");
258 mIsReconnecting = true;
260 mGatt = connectGatt(false);
265 Log.v(TAG, "Chromebook: We are connected, and registered. Everything's good!");
270 case BluetoothProfile.STATE_DISCONNECTED:
271 Log.v(TAG, "Chromebook: We have either been disconnected, or the Chromebook BtGatt.ContextMap bug has bitten us. Attempting a disconnect/reconnect, but we may not be able to recover.");
273 mIsReconnecting = true;
275 mGatt = connectGatt(false);
278 case BluetoothProfile.STATE_CONNECTING:
279 Log.v(TAG, "Chromebook: We're still trying to connect. Waiting a bit longer.");
283 final HIDDeviceBLESteamController finalThis = this;
284 mHandler.postDelayed(new Runnable() {
287 finalThis.checkConnectionForChromebookIssue();
289 }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
292 private boolean isRegistered() {
293 return mIsRegistered;
296 private void setRegistered() {
297 mIsRegistered = true;
300 private boolean probeService(HIDDeviceBLESteamController controller) {
302 if (isRegistered()) {
310 Log.v(TAG, "probeService controller=" + controller);
312 for (BluetoothGattService service : mGatt.getServices()) {
313 if (service.getUuid().equals(steamControllerService)) {
314 Log.v(TAG, "Found Valve steam controller service " + service.getUuid());
316 for (BluetoothGattCharacteristic chr : service.getCharacteristics()) {
317 if (chr.getUuid().equals(inputCharacteristic)) {
318 Log.v(TAG, "Found input characteristic");
319 // Start notifications
320 BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
322 enableNotification(chr.getUuid());
330 if ((mGatt.getServices().size() == 0) && mIsChromebook && !mIsReconnecting) {
331 Log.e(TAG, "Chromebook: Discovered services were empty; this almost certainly means the BtGatt.ContextMap bug has bitten us.");
332 mIsConnected = false;
333 mIsReconnecting = true;
335 mGatt = connectGatt(false);
341 //////////////////////////////////////////////////////////////////////////////////////////////////////
342 //////////////////////////////////////////////////////////////////////////////////////////////////////
343 //////////////////////////////////////////////////////////////////////////////////////////////////////
345 private void finishCurrentGattOperation() {
346 GattOperation op = null;
347 synchronized (mOperations) {
348 if (mCurrentOperation != null) {
349 op = mCurrentOperation;
350 mCurrentOperation = null;
354 boolean result = op.finish(); // TODO: Maybe in main thread as well?
356 // Our operation failed, let's add it back to the beginning of our queue.
358 mOperations.addFirst(op);
361 executeNextGattOperation();
364 private void executeNextGattOperation() {
365 synchronized (mOperations) {
366 if (mCurrentOperation != null)
369 if (mOperations.isEmpty())
372 mCurrentOperation = mOperations.removeFirst();
375 // Run in main thread
376 mHandler.post(new Runnable() {
379 synchronized (mOperations) {
380 if (mCurrentOperation == null) {
381 Log.e(TAG, "Current operation null in executor?");
385 mCurrentOperation.run();
386 // now wait for the GATT callback and when it comes, finish this operation
392 private void queueGattOperation(GattOperation op) {
393 synchronized (mOperations) {
396 executeNextGattOperation();
399 private void enableNotification(UUID chrUuid) {
400 GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid);
401 queueGattOperation(op);
404 public void writeCharacteristic(UUID uuid, byte[] value) {
405 GattOperation op = HIDDeviceBLESteamController.GattOperation.writeCharacteristic(mGatt, uuid, value);
406 queueGattOperation(op);
409 public void readCharacteristic(UUID uuid) {
410 GattOperation op = HIDDeviceBLESteamController.GattOperation.readCharacteristic(mGatt, uuid);
411 queueGattOperation(op);
414 //////////////////////////////////////////////////////////////////////////////////////////////////////
415 ////////////// BluetoothGattCallback overridden methods
416 //////////////////////////////////////////////////////////////////////////////////////////////////////
418 public void onConnectionStateChange(BluetoothGatt g, int status, int newState) {
419 //Log.v(TAG, "onConnectionStateChange status=" + status + " newState=" + newState);
420 mIsReconnecting = false;
423 // Run directly, without GattOperation
424 if (!isRegistered()) {
425 mHandler.post(new Runnable() {
428 mGatt.discoverServices();
433 else if (newState == 0) {
434 mIsConnected = false;
437 // Disconnection is handled in SteamLink using the ACTION_ACL_DISCONNECTED Intent.
440 public void onServicesDiscovered(BluetoothGatt gatt, int status) {
441 //Log.v(TAG, "onServicesDiscovered status=" + status);
443 if (gatt.getServices().size() == 0) {
444 Log.v(TAG, "onServicesDiscovered returned zero services; something has gone horribly wrong down in Android's Bluetooth stack.");
445 mIsReconnecting = true;
446 mIsConnected = false;
448 mGatt = connectGatt(false);
456 public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
457 //Log.v(TAG, "onCharacteristicRead status=" + status + " uuid=" + characteristic.getUuid());
459 if (characteristic.getUuid().equals(reportCharacteristic) && !mFrozen) {
460 mManager.HIDDeviceFeatureReport(getId(), characteristic.getValue());
463 finishCurrentGattOperation();
466 public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
467 //Log.v(TAG, "onCharacteristicWrite status=" + status + " uuid=" + characteristic.getUuid());
469 if (characteristic.getUuid().equals(reportCharacteristic)) {
470 // Only register controller with the native side once it has been fully configured
471 if (!isRegistered()) {
472 Log.v(TAG, "Registering Steam Controller with ID: " + getId());
473 mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0);
478 finishCurrentGattOperation();
481 public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
482 // Enable this for verbose logging of controller input reports
483 //Log.v(TAG, "onCharacteristicChanged uuid=" + characteristic.getUuid() + " data=" + HexDump.dumpHexString(characteristic.getValue()));
485 if (characteristic.getUuid().equals(inputCharacteristic) && !mFrozen) {
486 mManager.HIDDeviceInputReport(getId(), characteristic.getValue());
490 public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
491 //Log.v(TAG, "onDescriptorRead status=" + status);
494 public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
495 BluetoothGattCharacteristic chr = descriptor.getCharacteristic();
496 //Log.v(TAG, "onDescriptorWrite status=" + status + " uuid=" + chr.getUuid() + " descriptor=" + descriptor.getUuid());
498 if (chr.getUuid().equals(inputCharacteristic)) {
499 boolean hasWrittenInputDescriptor = true;
500 BluetoothGattCharacteristic reportChr = chr.getService().getCharacteristic(reportCharacteristic);
501 if (reportChr != null) {
502 Log.v(TAG, "Writing report characteristic to enter valve mode");
503 reportChr.setValue(enterValveMode);
504 gatt.writeCharacteristic(reportChr);
508 finishCurrentGattOperation();
511 public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
512 //Log.v(TAG, "onReliableWriteCompleted status=" + status);
515 public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
516 //Log.v(TAG, "onReadRemoteRssi status=" + status);
519 public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
520 //Log.v(TAG, "onMtuChanged status=" + status);
523 //////////////////////////////////////////////////////////////////////////////////////////////////////
525 //////////////////////////////////////////////////////////////////////////////////////////////////////
533 public int getVendorId() {
535 final int VALVE_USB_VID = 0x28DE;
536 return VALVE_USB_VID;
540 public int getProductId() {
541 // We don't have an easy way to query from the Bluetooth device, but we know what it is
542 final int D0G_BLE2_PID = 0x1106;
547 public String getSerialNumber() {
548 // This will be read later via feature report by Steam
553 public int getVersion() {
558 public String getManufacturerName() {
559 return "Valve Corporation";
563 public String getProductName() {
564 return "Steam Controller";
568 public UsbDevice getDevice() {
573 public boolean open() {
578 public int sendFeatureReport(byte[] report) {
579 if (!isRegistered()) {
580 Log.e(TAG, "Attempted sendFeatureReport before Steam Controller is registered!");
587 // We need to skip the first byte, as that doesn't go over the air
588 byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1);
589 //Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(actual_report));
590 writeCharacteristic(reportCharacteristic, actual_report);
591 return report.length;
595 public int sendOutputReport(byte[] report) {
596 if (!isRegistered()) {
597 Log.e(TAG, "Attempted sendOutputReport before Steam Controller is registered!");
604 //Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(report));
605 writeCharacteristic(reportCharacteristic, report);
606 return report.length;
610 public boolean getFeatureReport(byte[] report) {
611 if (!isRegistered()) {
612 Log.e(TAG, "Attempted getFeatureReport before Steam Controller is registered!");
619 //Log.v(TAG, "getFeatureReport");
620 readCharacteristic(reportCharacteristic);
625 public void close() {
629 public void setFrozen(boolean frozen) {
634 public void shutdown() {
637 BluetoothGatt g = mGatt;
644 mIsRegistered = false;
645 mIsConnected = false;