SDL  2.0
HIDDeviceBLESteamController.java
Go to the documentation of this file.
1 package org.libsdl.app;
2 
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.os.Handler;
13 import android.os.Looper;
14 import android.util.Log;
15 
16 //import com.android.internal.util.HexDump;
17 
18 import java.lang.Runnable;
19 import java.lang.reflect.InvocationTargetException;
20 import java.lang.reflect.Method;
21 import java.util.Arrays;
22 import java.util.LinkedList;
23 import java.util.UUID;
24 
25 class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice {
26 
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;
40 
41  private static final int TRANSPORT_AUTO = 0;
42  private static final int TRANSPORT_BREDR = 1;
43  private static final int TRANSPORT_LE = 2;
44 
45  private static final int CHROMEBOOK_CONNECTION_CHECK_INTERVAL = 10000;
46 
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 };
51 
52  static class GattOperation {
53  private enum Operation {
56  ENABLE_NOTIFICATION
57  }
58 
59  Operation mOp;
60  UUID mUuid;
61  byte[] mValue;
62  BluetoothGatt mGatt;
63  boolean mResult = true;
64 
65  private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) {
66  mGatt = gatt;
67  mOp = operation;
68  mUuid = uuid;
69  }
70 
71  private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) {
72  mGatt = gatt;
73  mOp = operation;
74  mUuid = uuid;
75  mValue = value;
76  }
77 
78  public void run() {
79  // This is executed in main thread
80  BluetoothGattCharacteristic chr;
81 
82  switch (mOp) {
83  case CHR_READ:
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());
88  mResult = false;
89  break;
90  }
91  mResult = true;
92  break;
93  case CHR_WRITE:
94  chr = getCharacteristic(mUuid);
95  //Log.v(TAG, "Writing characteristic " + chr.getUuid() + " value=" + HexDump.toHexString(value));
96  chr.setValue(mValue);
97  if (!mGatt.writeCharacteristic(chr)) {
98  Log.e(TAG, "Unable to write characteristic " + mUuid.toString());
99  mResult = false;
100  break;
101  }
102  mResult = true;
103  break;
104  case ENABLE_NOTIFICATION:
105  chr = getCharacteristic(mUuid);
106  //Log.v(TAG, "Writing descriptor of " + chr.getUuid());
107  if (chr != null) {
108  BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
109  if (cccd != null) {
110  int properties = chr.getProperties();
111  byte[] value;
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;
116  } else {
117  Log.e(TAG, "Unable to start notifications on input characteristic");
118  mResult = false;
119  return;
120  }
121 
122  mGatt.setCharacteristicNotification(chr, true);
123  cccd.setValue(value);
124  if (!mGatt.writeDescriptor(cccd)) {
125  Log.e(TAG, "Unable to write descriptor " + mUuid.toString());
126  mResult = false;
127  return;
128  }
129  mResult = true;
130  }
131  }
132  }
133  }
134 
135  public boolean finish() {
136  return mResult;
137  }
138 
139  private BluetoothGattCharacteristic getCharacteristic(UUID uuid) {
140  BluetoothGattService valveService = mGatt.getService(steamControllerService);
141  if (valveService == null)
142  return null;
143  return valveService.getCharacteristic(uuid);
144  }
145 
146  static public GattOperation readCharacteristic(BluetoothGatt gatt, UUID uuid) {
147  return new GattOperation(gatt, Operation.CHR_READ, uuid);
148  }
149 
150  static public GattOperation writeCharacteristic(BluetoothGatt gatt, UUID uuid, byte[] value) {
151  return new GattOperation(gatt, Operation.CHR_WRITE, uuid, value);
152  }
153 
154  static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) {
155  return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid);
156  }
157  }
158 
159  public HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) {
160  mManager = manager;
161  mDevice = 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());
167 
168  mGatt = connectGatt();
169  final HIDDeviceBLESteamController finalThis = this;
170  mHandler.postDelayed(new Runnable() {
171  @Override
172  public void run() {
173  finalThis.checkConnectionForChromebookIssue();
174  }
175  }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
176  }
177 
178  public String getIdentifier() {
179  return String.format("SteamController.%s", mDevice.getAddress());
180  }
181 
182  public BluetoothGatt getGatt() {
183  return mGatt;
184  }
185 
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  try {
190  Method m = mDevice.getClass().getDeclaredMethod("connectGatt", Context.class, boolean.class, BluetoothGattCallback.class, int.class);
191  return (BluetoothGatt) m.invoke(mDevice, mManager.getContext(), managed, this, TRANSPORT_LE);
192  } catch (Exception e) {
193  return mDevice.connectGatt(mManager.getContext(), managed, this);
194  }
195  }
196 
197  private BluetoothGatt connectGatt() {
198  return connectGatt(false);
199  }
200 
201  protected int getConnectionState() {
202 
203  Context context = mManager.getContext();
204  if (context == null) {
205  // We are lacking any context to get our Bluetooth information. We'll just assume disconnected.
206  return BluetoothProfile.STATE_DISCONNECTED;
207  }
208 
209  BluetoothManager btManager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE);
210  if (btManager == null) {
211  // This device doesn't support Bluetooth. We should never be here, because how did
212  // we instantiate a device to start with?
213  return BluetoothProfile.STATE_DISCONNECTED;
214  }
215 
216  return btManager.getConnectionState(mDevice, BluetoothProfile.GATT);
217  }
218 
219  public void reconnect() {
220 
221  if (getConnectionState() != BluetoothProfile.STATE_CONNECTED) {
222  mGatt.disconnect();
223  mGatt = connectGatt();
224  }
225 
226  }
227 
228  protected void checkConnectionForChromebookIssue() {
229  if (!mIsChromebook) {
230  // We only do this on Chromebooks, because otherwise it's really annoying to just attempt
231  // over and over.
232  return;
233  }
234 
235  int connectionState = getConnectionState();
236 
237  switch (connectionState) {
238  case BluetoothProfile.STATE_CONNECTED:
239  if (!mIsConnected) {
240  // We are in the Bad Chromebook Place. We can force a disconnect
241  // to try to recover.
242  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.");
243  mIsReconnecting = true;
244  mGatt.disconnect();
245  mGatt = connectGatt(false);
246  break;
247  }
248  else if (!isRegistered()) {
249  if (mGatt.getServices().size() > 0) {
250  Log.v(TAG, "Chromebook: We are connected to a controller, but never got our registration. Trying to recover.");
251  probeService(this);
252  }
253  else {
254  Log.v(TAG, "Chromebook: We are connected to a controller, but never discovered services. Trying to recover.");
255  mIsReconnecting = true;
256  mGatt.disconnect();
257  mGatt = connectGatt(false);
258  break;
259  }
260  }
261  else {
262  Log.v(TAG, "Chromebook: We are connected, and registered. Everything's good!");
263  return;
264  }
265  break;
266 
267  case BluetoothProfile.STATE_DISCONNECTED:
268  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.");
269 
270  mIsReconnecting = true;
271  mGatt.disconnect();
272  mGatt = connectGatt(false);
273  break;
274 
275  case BluetoothProfile.STATE_CONNECTING:
276  Log.v(TAG, "Chromebook: We're still trying to connect. Waiting a bit longer.");
277  break;
278  }
279 
280  final HIDDeviceBLESteamController finalThis = this;
281  mHandler.postDelayed(new Runnable() {
282  @Override
283  public void run() {
284  finalThis.checkConnectionForChromebookIssue();
285  }
286  }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
287  }
288 
289  private boolean isRegistered() {
290  return mIsRegistered;
291  }
292 
293  private void setRegistered() {
294  mIsRegistered = true;
295  }
296 
297  private boolean probeService(HIDDeviceBLESteamController controller) {
298 
299  if (isRegistered()) {
300  return true;
301  }
302 
303  if (!mIsConnected) {
304  return false;
305  }
306 
307  Log.v(TAG, "probeService controller=" + controller);
308 
309  for (BluetoothGattService service : mGatt.getServices()) {
310  if (service.getUuid().equals(steamControllerService)) {
311  Log.v(TAG, "Found Valve steam controller service " + service.getUuid());
312 
313  for (BluetoothGattCharacteristic chr : service.getCharacteristics()) {
314  if (chr.getUuid().equals(inputCharacteristic)) {
315  Log.v(TAG, "Found input characteristic");
316  // Start notifications
317  BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
318  if (cccd != null) {
319  enableNotification(chr.getUuid());
320  }
321  }
322  }
323  return true;
324  }
325  }
326 
327  if ((mGatt.getServices().size() == 0) && mIsChromebook && !mIsReconnecting) {
328  Log.e(TAG, "Chromebook: Discovered services were empty; this almost certainly means the BtGatt.ContextMap bug has bitten us.");
329  mIsConnected = false;
330  mIsReconnecting = true;
331  mGatt.disconnect();
332  mGatt = connectGatt(false);
333  }
334 
335  return false;
336  }
337 
338  //////////////////////////////////////////////////////////////////////////////////////////////////////
339  //////////////////////////////////////////////////////////////////////////////////////////////////////
340  //////////////////////////////////////////////////////////////////////////////////////////////////////
341 
342  private void finishCurrentGattOperation() {
343  GattOperation op = null;
344  synchronized (mOperations) {
345  if (mCurrentOperation != null) {
346  op = mCurrentOperation;
347  mCurrentOperation = null;
348  }
349  }
350  if (op != null) {
351  boolean result = op.finish(); // TODO: Maybe in main thread as well?
352 
353  // Our operation failed, let's add it back to the beginning of our queue.
354  if (!result) {
355  mOperations.addFirst(op);
356  }
357  }
358  executeNextGattOperation();
359  }
360 
361  private void executeNextGattOperation() {
362  synchronized (mOperations) {
363  if (mCurrentOperation != null)
364  return;
365 
366  if (mOperations.isEmpty())
367  return;
368 
369  mCurrentOperation = mOperations.removeFirst();
370  }
371 
372  // Run in main thread
373  mHandler.post(new Runnable() {
374  @Override
375  public void run() {
376  synchronized (mOperations) {
377  if (mCurrentOperation == null) {
378  Log.e(TAG, "Current operation null in executor?");
379  return;
380  }
381 
382  mCurrentOperation.run();
383  // now wait for the GATT callback and when it comes, finish this operation
384  }
385  }
386  });
387  }
388 
389  private void queueGattOperation(GattOperation op) {
390  synchronized (mOperations) {
391  mOperations.add(op);
392  }
393  executeNextGattOperation();
394  }
395 
396  private void enableNotification(UUID chrUuid) {
397  GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid);
398  queueGattOperation(op);
399  }
400 
401  public void writeCharacteristic(UUID uuid, byte[] value) {
402  GattOperation op = HIDDeviceBLESteamController.GattOperation.writeCharacteristic(mGatt, uuid, value);
403  queueGattOperation(op);
404  }
405 
406  public void readCharacteristic(UUID uuid) {
407  GattOperation op = HIDDeviceBLESteamController.GattOperation.readCharacteristic(mGatt, uuid);
408  queueGattOperation(op);
409  }
410 
411  //////////////////////////////////////////////////////////////////////////////////////////////////////
412  ////////////// BluetoothGattCallback overridden methods
413  //////////////////////////////////////////////////////////////////////////////////////////////////////
414 
415  public void onConnectionStateChange(BluetoothGatt g, int status, int newState) {
416  //Log.v(TAG, "onConnectionStateChange status=" + status + " newState=" + newState);
417  mIsReconnecting = false;
418  if (newState == 2) {
419  mIsConnected = true;
420  // Run directly, without GattOperation
421  if (!isRegistered()) {
422  mHandler.post(new Runnable() {
423  @Override
424  public void run() {
425  mGatt.discoverServices();
426  }
427  });
428  }
429  }
430  else if (newState == 0) {
431  mIsConnected = false;
432  }
433 
434  // Disconnection is handled in SteamLink using the ACTION_ACL_DISCONNECTED Intent.
435  }
436 
437  public void onServicesDiscovered(BluetoothGatt gatt, int status) {
438  //Log.v(TAG, "onServicesDiscovered status=" + status);
439  if (status == 0) {
440  if (gatt.getServices().size() == 0) {
441  Log.v(TAG, "onServicesDiscovered returned zero services; something has gone horribly wrong down in Android's Bluetooth stack.");
442  mIsReconnecting = true;
443  mIsConnected = false;
444  gatt.disconnect();
445  mGatt = connectGatt(false);
446  }
447  else {
448  probeService(this);
449  }
450  }
451  }
452 
453  public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
454  //Log.v(TAG, "onCharacteristicRead status=" + status + " uuid=" + characteristic.getUuid());
455 
456  if (characteristic.getUuid().equals(reportCharacteristic) && !mFrozen) {
457  mManager.HIDDeviceFeatureReport(getId(), characteristic.getValue());
458  }
459 
460  finishCurrentGattOperation();
461  }
462 
463  public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
464  //Log.v(TAG, "onCharacteristicWrite status=" + status + " uuid=" + characteristic.getUuid());
465 
466  if (characteristic.getUuid().equals(reportCharacteristic)) {
467  // Only register controller with the native side once it has been fully configured
468  if (!isRegistered()) {
469  Log.v(TAG, "Registering Steam Controller with ID: " + getId());
470  mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0);
471  setRegistered();
472  }
473  }
474 
475  finishCurrentGattOperation();
476  }
477 
478  public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
479  // Enable this for verbose logging of controller input reports
480  //Log.v(TAG, "onCharacteristicChanged uuid=" + characteristic.getUuid() + " data=" + HexDump.dumpHexString(characteristic.getValue()));
481 
482  if (characteristic.getUuid().equals(inputCharacteristic) && !mFrozen) {
483  mManager.HIDDeviceInputReport(getId(), characteristic.getValue());
484  }
485  }
486 
487  public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
488  //Log.v(TAG, "onDescriptorRead status=" + status);
489  }
490 
491  public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
492  BluetoothGattCharacteristic chr = descriptor.getCharacteristic();
493  //Log.v(TAG, "onDescriptorWrite status=" + status + " uuid=" + chr.getUuid() + " descriptor=" + descriptor.getUuid());
494 
495  if (chr.getUuid().equals(inputCharacteristic)) {
496  boolean hasWrittenInputDescriptor = true;
497  BluetoothGattCharacteristic reportChr = chr.getService().getCharacteristic(reportCharacteristic);
498  if (reportChr != null) {
499  Log.v(TAG, "Writing report characteristic to enter valve mode");
500  reportChr.setValue(enterValveMode);
501  gatt.writeCharacteristic(reportChr);
502  }
503  }
504 
505  finishCurrentGattOperation();
506  }
507 
508  public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
509  //Log.v(TAG, "onReliableWriteCompleted status=" + status);
510  }
511 
512  public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
513  //Log.v(TAG, "onReadRemoteRssi status=" + status);
514  }
515 
516  public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
517  //Log.v(TAG, "onMtuChanged status=" + status);
518  }
519 
520  //////////////////////////////////////////////////////////////////////////////////////////////////////
521  //////// Public API
522  //////////////////////////////////////////////////////////////////////////////////////////////////////
523 
524  @Override
525  public int getId() {
526  return mDeviceId;
527  }
528 
529  @Override
530  public int getVendorId() {
531  // Valve Corporation
532  final int VALVE_USB_VID = 0x28DE;
533  return VALVE_USB_VID;
534  }
535 
536  @Override
537  public int getProductId() {
538  // We don't have an easy way to query from the Bluetooth device, but we know what it is
539  final int D0G_BLE2_PID = 0x1106;
540  return D0G_BLE2_PID;
541  }
542 
543  @Override
544  public String getSerialNumber() {
545  // This will be read later via feature report by Steam
546  return "12345";
547  }
548 
549  @Override
550  public int getVersion() {
551  return 0;
552  }
553 
554  @Override
555  public String getManufacturerName() {
556  return "Valve Corporation";
557  }
558 
559  @Override
560  public String getProductName() {
561  return "Steam Controller";
562  }
563 
564  @Override
565  public boolean open() {
566  return true;
567  }
568 
569  @Override
570  public int sendFeatureReport(byte[] report) {
571  if (!isRegistered()) {
572  Log.e(TAG, "Attempted sendFeatureReport before Steam Controller is registered!");
573  if (mIsConnected) {
574  probeService(this);
575  }
576  return -1;
577  }
578 
579  // We need to skip the first byte, as that doesn't go over the air
580  byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1);
581  //Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(actual_report));
582  writeCharacteristic(reportCharacteristic, actual_report);
583  return report.length;
584  }
585 
586  @Override
587  public int sendOutputReport(byte[] report) {
588  if (!isRegistered()) {
589  Log.e(TAG, "Attempted sendOutputReport before Steam Controller is registered!");
590  if (mIsConnected) {
591  probeService(this);
592  }
593  return -1;
594  }
595 
596  //Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(report));
597  writeCharacteristic(reportCharacteristic, report);
598  return report.length;
599  }
600 
601  @Override
602  public boolean getFeatureReport(byte[] report) {
603  if (!isRegistered()) {
604  Log.e(TAG, "Attempted getFeatureReport before Steam Controller is registered!");
605  if (mIsConnected) {
606  probeService(this);
607  }
608  return false;
609  }
610 
611  //Log.v(TAG, "getFeatureReport");
612  readCharacteristic(reportCharacteristic);
613  return true;
614  }
615 
616  @Override
617  public void close() {
618  }
619 
620  @Override
621  public void setFrozen(boolean frozen) {
622  mFrozen = frozen;
623  }
624 
625  @Override
626  public void shutdown() {
627  close();
628 
629  BluetoothGatt g = mGatt;
630  if (g != null) {
631  g.disconnect();
632  g.close();
633  mGatt = null;
634  }
635  mManager = null;
636  mIsRegistered = false;
637  mIsConnected = false;
638  mOperations.clear();
639  }
640 
641 }
642 
GLuint64EXT * result
SDL_PRINTF_FORMAT_STRING const char int SDL_PRINTF_FORMAT_STRING const char int SDL_PRINTF_FORMAT_STRING const char int SDL_PRINTF_FORMAT_STRING const char const char SDL_SCANF_FORMAT_STRING const char return SDL_ThreadFunction const char void return Uint32 return Uint32 SDL_AssertionHandler void SDL_SpinLock SDL_atomic_t int int return SDL_atomic_t return void void void return void return int return SDL_AudioSpec SDL_AudioSpec return int int return return int SDL_RWops int SDL_AudioSpec Uint8 Uint32 * e
const GLfloat * m
static screen_context_t context
Definition: video.c:25
static SDL_AudioDeviceID device
Definition: loopwave.c:37
GLsizei const GLfloat * value
#define TAG
Definition: hid.cpp:16
GLboolean GLboolean g