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.os.Handler;
13 import android.os.Looper;
14 import android.util.Log;
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;
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 {
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) {
80 BluetoothGattCharacteristic chr;
84 chr = getCharacteristic(mUuid);
86 if (!mGatt.readCharacteristic(chr)) {
87 Log.e(TAG,
"Unable to read characteristic " + mUuid.toString());
94 chr = getCharacteristic(mUuid);
97 if (!mGatt.writeCharacteristic(chr)) {
98 Log.e(TAG,
"Unable to write characteristic " + mUuid.toString());
104 case ENABLE_NOTIFICATION:
105 chr = getCharacteristic(mUuid);
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) {
150 static public GattOperation writeCharacteristic(BluetoothGatt gatt, UUID uuid, byte[] value) {
154 static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) {
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() {
173 finalThis.checkConnectionForChromebookIssue();
175 }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
178 public String getIdentifier() {
179 return String.format(
"SteamController.%s", mDevice.getAddress());
182 public BluetoothGatt getGatt() {
188 private BluetoothGatt connectGatt(
boolean managed) {
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);
197 private BluetoothGatt connectGatt() {
198 return connectGatt(
false);
201 protected int getConnectionState() {
203 Context
context = mManager.getContext();
204 if (context == null) {
206 return BluetoothProfile.STATE_DISCONNECTED;
209 BluetoothManager btManager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE);
210 if (btManager == null) {
213 return BluetoothProfile.STATE_DISCONNECTED;
216 return btManager.getConnectionState(mDevice, BluetoothProfile.GATT);
219 public void reconnect() {
221 if (getConnectionState() != BluetoothProfile.STATE_CONNECTED) {
223 mGatt = connectGatt();
228 protected void checkConnectionForChromebookIssue() {
229 if (!mIsChromebook) {
235 int connectionState = getConnectionState();
237 switch (connectionState) {
238 case BluetoothProfile.STATE_CONNECTED:
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;
245 mGatt = connectGatt(
false);
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.");
254 Log.v(TAG,
"Chromebook: We are connected to a controller, but never discovered services. Trying to recover.");
255 mIsReconnecting =
true;
257 mGatt = connectGatt(
false);
262 Log.v(TAG,
"Chromebook: We are connected, and registered. Everything's good!");
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.");
270 mIsReconnecting =
true;
272 mGatt = connectGatt(
false);
275 case BluetoothProfile.STATE_CONNECTING:
276 Log.v(TAG,
"Chromebook: We're still trying to connect. Waiting a bit longer.");
280 final HIDDeviceBLESteamController finalThis =
this;
281 mHandler.postDelayed(
new Runnable() {
284 finalThis.checkConnectionForChromebookIssue();
286 }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
289 private boolean isRegistered() {
290 return mIsRegistered;
293 private void setRegistered() {
294 mIsRegistered =
true;
297 private boolean probeService(HIDDeviceBLESteamController controller) {
299 if (isRegistered()) {
307 Log.v(TAG,
"probeService controller=" + controller);
309 for (BluetoothGattService service : mGatt.getServices()) {
310 if (service.getUuid().equals(steamControllerService)) {
311 Log.v(TAG,
"Found Valve steam controller service " + service.getUuid());
313 for (BluetoothGattCharacteristic chr : service.getCharacteristics()) {
314 if (chr.getUuid().equals(inputCharacteristic)) {
315 Log.v(TAG,
"Found input characteristic");
317 BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString(
"00002902-0000-1000-8000-00805f9b34fb"));
319 enableNotification(chr.getUuid());
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;
332 mGatt = connectGatt(
false);
342 private void finishCurrentGattOperation() {
343 GattOperation op = null;
344 synchronized (mOperations) {
345 if (mCurrentOperation != null) {
346 op = mCurrentOperation;
347 mCurrentOperation = null;
351 boolean result = op.finish();
355 mOperations.addFirst(op);
358 executeNextGattOperation();
361 private void executeNextGattOperation() {
362 synchronized (mOperations) {
363 if (mCurrentOperation != null)
366 if (mOperations.isEmpty())
369 mCurrentOperation = mOperations.removeFirst();
373 mHandler.post(
new Runnable() {
376 synchronized (mOperations) {
377 if (mCurrentOperation == null) {
378 Log.e(TAG,
"Current operation null in executor?");
382 mCurrentOperation.run();
389 private void queueGattOperation(GattOperation op) {
390 synchronized (mOperations) {
393 executeNextGattOperation();
396 private void enableNotification(UUID chrUuid) {
397 GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid);
398 queueGattOperation(op);
401 public void writeCharacteristic(UUID uuid, byte[]
value) {
402 GattOperation op = HIDDeviceBLESteamController.GattOperation.writeCharacteristic(mGatt, uuid, value);
403 queueGattOperation(op);
406 public void readCharacteristic(UUID uuid) {
407 GattOperation op = HIDDeviceBLESteamController.GattOperation.readCharacteristic(mGatt, uuid);
408 queueGattOperation(op);
415 public void onConnectionStateChange(BluetoothGatt
g,
int status,
int newState) {
417 mIsReconnecting =
false;
421 if (!isRegistered()) {
422 mHandler.post(
new Runnable() {
425 mGatt.discoverServices();
430 else if (newState == 0) {
431 mIsConnected =
false;
437 public void onServicesDiscovered(BluetoothGatt gatt,
int status) {
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;
445 mGatt = connectGatt(
false);
453 public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic,
int status) {
456 if (characteristic.getUuid().equals(reportCharacteristic) && !mFrozen) {
457 mManager.HIDDeviceFeatureReport(getId(), characteristic.getValue());
460 finishCurrentGattOperation();
463 public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic,
int status) {
466 if (characteristic.getUuid().equals(reportCharacteristic)) {
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);
475 finishCurrentGattOperation();
478 public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
482 if (characteristic.getUuid().equals(inputCharacteristic) && !mFrozen) {
483 mManager.HIDDeviceInputReport(getId(), characteristic.getValue());
487 public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
int status) {
491 public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
int status) {
492 BluetoothGattCharacteristic chr = descriptor.getCharacteristic();
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);
505 finishCurrentGattOperation();
508 public void onReliableWriteCompleted(BluetoothGatt gatt,
int status) {
512 public void onReadRemoteRssi(BluetoothGatt gatt,
int rssi,
int status) {
516 public void onMtuChanged(BluetoothGatt gatt,
int mtu,
int status) {
530 public int getVendorId() {
532 final int VALVE_USB_VID = 0x28DE;
533 return VALVE_USB_VID;
537 public int getProductId() {
539 final int D0G_BLE2_PID = 0x1106;
544 public String getSerialNumber() {
550 public int getVersion() {
555 public String getManufacturerName() {
556 return "Valve Corporation";
560 public String getProductName() {
561 return "Steam Controller";
565 public boolean open() {
570 public int sendFeatureReport(byte[] report) {
571 if (!isRegistered()) {
572 Log.e(TAG,
"Attempted sendFeatureReport before Steam Controller is registered!");
580 byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1);
582 writeCharacteristic(reportCharacteristic, actual_report);
583 return report.length;
587 public int sendOutputReport(byte[] report) {
588 if (!isRegistered()) {
589 Log.e(TAG,
"Attempted sendOutputReport before Steam Controller is registered!");
597 writeCharacteristic(reportCharacteristic, report);
598 return report.length;
602 public boolean getFeatureReport(byte[] report) {
603 if (!isRegistered()) {
604 Log.e(TAG,
"Attempted getFeatureReport before Steam Controller is registered!");
612 readCharacteristic(reportCharacteristic);
617 public void close() {
621 public void setFrozen(
boolean frozen) {
626 public void shutdown() {
629 BluetoothGatt g = mGatt;
636 mIsRegistered =
false;
637 mIsConnected =
false;
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
static screen_context_t context
static SDL_AudioDeviceID device
GLsizei const GLfloat * value