changed build system for Android from Ant to Gradle
[rocksndiamonds.git] / build-projects / android / app / src / main / java / org / libsdl / app / HIDDeviceBLESteamController.java
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.hardware.usb.UsbDevice;
13 import android.os.Handler;
14 import android.os.Looper;
15 import android.util.Log;
16 import android.os.*;
17
18 //import com.android.internal.util.HexDump;
19
20 import java.lang.Runnable;
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 {
54             CHR_READ,
55             CHR_WRITE,
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         if (Build.VERSION.SDK_INT >= 23) {
190             try {
191                 return mDevice.connectGatt(mManager.getContext(), managed, this, TRANSPORT_LE);
192             } catch (Exception e) {
193                 return mDevice.connectGatt(mManager.getContext(), managed, this);
194             }
195         } else {
196             return mDevice.connectGatt(mManager.getContext(), managed, this);
197         }
198     }
199
200     private BluetoothGatt connectGatt() {
201         return connectGatt(false);
202     }
203
204     protected int getConnectionState() {
205
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;
210         }
211
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;
217         }
218
219         return btManager.getConnectionState(mDevice, BluetoothProfile.GATT);
220     }
221
222     public void reconnect() {
223
224         if (getConnectionState() != BluetoothProfile.STATE_CONNECTED) {
225             mGatt.disconnect();
226             mGatt = connectGatt();
227         }
228
229     }
230
231     protected void checkConnectionForChromebookIssue() {
232         if (!mIsChromebook) {
233             // We only do this on Chromebooks, because otherwise it's really annoying to just attempt
234             // over and over.
235             return;
236         }
237
238         int connectionState = getConnectionState();
239
240         switch (connectionState) {
241             case BluetoothProfile.STATE_CONNECTED:
242                 if (!mIsConnected) {
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;
247                     mGatt.disconnect();
248                     mGatt = connectGatt(false);
249                     break;
250                 }
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.");
254                         probeService(this);
255                     }
256                     else {
257                         Log.v(TAG, "Chromebook: We are connected to a controller, but never discovered services.  Trying to recover.");
258                         mIsReconnecting = true;
259                         mGatt.disconnect();
260                         mGatt = connectGatt(false);
261                         break;
262                     }
263                 }
264                 else {
265                     Log.v(TAG, "Chromebook: We are connected, and registered.  Everything's good!");
266                     return;
267                 }
268                 break;
269
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.");
272
273                 mIsReconnecting = true;
274                 mGatt.disconnect();
275                 mGatt = connectGatt(false);
276                 break;
277
278             case BluetoothProfile.STATE_CONNECTING:
279                 Log.v(TAG, "Chromebook: We're still trying to connect.  Waiting a bit longer.");
280                 break;
281         }
282
283         final HIDDeviceBLESteamController finalThis = this;
284         mHandler.postDelayed(new Runnable() {
285             @Override
286             public void run() {
287                 finalThis.checkConnectionForChromebookIssue();
288             }
289         }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
290     }
291
292     private boolean isRegistered() {
293         return mIsRegistered;
294     }
295
296     private void setRegistered() {
297         mIsRegistered = true;
298     }
299
300     private boolean probeService(HIDDeviceBLESteamController controller) {
301
302         if (isRegistered()) {
303             return true;
304         }
305
306         if (!mIsConnected) {
307             return false;
308         }
309
310         Log.v(TAG, "probeService controller=" + controller);
311
312         for (BluetoothGattService service : mGatt.getServices()) {
313             if (service.getUuid().equals(steamControllerService)) {
314                 Log.v(TAG, "Found Valve steam controller service " + service.getUuid());
315
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"));
321                         if (cccd != null) {
322                             enableNotification(chr.getUuid());
323                         }
324                     }
325                 }
326                 return true;
327             }
328         }
329
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;
334             mGatt.disconnect();
335             mGatt = connectGatt(false);
336         }
337
338         return false;
339     }
340
341     //////////////////////////////////////////////////////////////////////////////////////////////////////
342     //////////////////////////////////////////////////////////////////////////////////////////////////////
343     //////////////////////////////////////////////////////////////////////////////////////////////////////
344
345     private void finishCurrentGattOperation() {
346         GattOperation op = null;
347         synchronized (mOperations) {
348             if (mCurrentOperation != null) {
349                 op = mCurrentOperation;
350                 mCurrentOperation = null;
351             }
352         }
353         if (op != null) {
354             boolean result = op.finish(); // TODO: Maybe in main thread as well?
355
356             // Our operation failed, let's add it back to the beginning of our queue.
357             if (!result) {
358                 mOperations.addFirst(op);
359             }
360         }
361         executeNextGattOperation();
362     }
363
364     private void executeNextGattOperation() {
365         synchronized (mOperations) {
366             if (mCurrentOperation != null)
367                 return;
368
369             if (mOperations.isEmpty())
370                 return;
371
372             mCurrentOperation = mOperations.removeFirst();
373         }
374
375         // Run in main thread
376         mHandler.post(new Runnable() {
377             @Override
378             public void run() {
379                 synchronized (mOperations) {
380                     if (mCurrentOperation == null) {
381                         Log.e(TAG, "Current operation null in executor?");
382                         return;
383                     }
384
385                     mCurrentOperation.run();
386                     // now wait for the GATT callback and when it comes, finish this operation
387                 }
388             }
389         });
390     }
391
392     private void queueGattOperation(GattOperation op) {
393         synchronized (mOperations) {
394             mOperations.add(op);
395         }
396         executeNextGattOperation();
397     }
398
399     private void enableNotification(UUID chrUuid) {
400         GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid);
401         queueGattOperation(op);
402     }
403
404     public void writeCharacteristic(UUID uuid, byte[] value) {
405         GattOperation op = HIDDeviceBLESteamController.GattOperation.writeCharacteristic(mGatt, uuid, value);
406         queueGattOperation(op);
407     }
408
409     public void readCharacteristic(UUID uuid) {
410         GattOperation op = HIDDeviceBLESteamController.GattOperation.readCharacteristic(mGatt, uuid);
411         queueGattOperation(op);
412     }
413
414     //////////////////////////////////////////////////////////////////////////////////////////////////////
415     //////////////  BluetoothGattCallback overridden methods
416     //////////////////////////////////////////////////////////////////////////////////////////////////////
417
418     public void onConnectionStateChange(BluetoothGatt g, int status, int newState) {
419         //Log.v(TAG, "onConnectionStateChange status=" + status + " newState=" + newState);
420         mIsReconnecting = false;
421         if (newState == 2) {
422             mIsConnected = true;
423             // Run directly, without GattOperation
424             if (!isRegistered()) {
425                 mHandler.post(new Runnable() {
426                     @Override
427                     public void run() {
428                         mGatt.discoverServices();
429                     }
430                 });
431             }
432         } 
433         else if (newState == 0) {
434             mIsConnected = false;
435         }
436
437         // Disconnection is handled in SteamLink using the ACTION_ACL_DISCONNECTED Intent.
438     }
439
440     public void onServicesDiscovered(BluetoothGatt gatt, int status) {
441         //Log.v(TAG, "onServicesDiscovered status=" + status);
442         if (status == 0) {
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;
447                 gatt.disconnect();
448                 mGatt = connectGatt(false);
449             }
450             else {
451                 probeService(this);
452             }
453         }
454     }
455
456     public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
457         //Log.v(TAG, "onCharacteristicRead status=" + status + " uuid=" + characteristic.getUuid());
458
459         if (characteristic.getUuid().equals(reportCharacteristic) && !mFrozen) {
460             mManager.HIDDeviceFeatureReport(getId(), characteristic.getValue());
461         }
462
463         finishCurrentGattOperation();
464     }
465
466     public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
467         //Log.v(TAG, "onCharacteristicWrite status=" + status + " uuid=" + characteristic.getUuid());
468
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);
474                 setRegistered();
475             }
476         }
477
478         finishCurrentGattOperation();
479     }
480
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()));
484
485         if (characteristic.getUuid().equals(inputCharacteristic) && !mFrozen) {
486             mManager.HIDDeviceInputReport(getId(), characteristic.getValue());
487         }
488     }
489
490     public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
491         //Log.v(TAG, "onDescriptorRead status=" + status);
492     }
493
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());
497
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);
505             }
506         }
507
508         finishCurrentGattOperation();
509     }
510
511     public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
512         //Log.v(TAG, "onReliableWriteCompleted status=" + status);
513     }
514
515     public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
516         //Log.v(TAG, "onReadRemoteRssi status=" + status);
517     }
518
519     public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
520         //Log.v(TAG, "onMtuChanged status=" + status);
521     }
522
523     //////////////////////////////////////////////////////////////////////////////////////////////////////
524     //////// Public API
525     //////////////////////////////////////////////////////////////////////////////////////////////////////
526
527     @Override
528     public int getId() {
529         return mDeviceId;
530     }
531
532     @Override
533     public int getVendorId() {
534         // Valve Corporation
535         final int VALVE_USB_VID = 0x28DE;
536         return VALVE_USB_VID;
537     }
538
539     @Override
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;
543         return D0G_BLE2_PID;
544     }
545
546     @Override
547     public String getSerialNumber() {
548         // This will be read later via feature report by Steam
549         return "12345";
550     }
551
552     @Override
553     public int getVersion() {
554         return 0;
555     }
556
557     @Override
558     public String getManufacturerName() {
559         return "Valve Corporation";
560     }
561
562     @Override
563     public String getProductName() {
564         return "Steam Controller";
565     }
566
567         @Override
568     public UsbDevice getDevice() {
569                 return null;
570         }
571
572     @Override
573     public boolean open() {
574         return true;
575     }
576
577     @Override
578     public int sendFeatureReport(byte[] report) {
579         if (!isRegistered()) {
580             Log.e(TAG, "Attempted sendFeatureReport before Steam Controller is registered!");
581             if (mIsConnected) {
582                 probeService(this);
583             }
584             return -1;
585         }
586
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;
592     }
593
594     @Override
595     public int sendOutputReport(byte[] report) {
596         if (!isRegistered()) {
597             Log.e(TAG, "Attempted sendOutputReport before Steam Controller is registered!");
598             if (mIsConnected) {
599                 probeService(this);
600             }
601             return -1;
602         }
603
604         //Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(report));
605         writeCharacteristic(reportCharacteristic, report);
606         return report.length;
607     }
608
609     @Override
610     public boolean getFeatureReport(byte[] report) {
611         if (!isRegistered()) {
612             Log.e(TAG, "Attempted getFeatureReport before Steam Controller is registered!");
613             if (mIsConnected) {
614                 probeService(this);
615             }
616             return false;
617         }
618
619         //Log.v(TAG, "getFeatureReport");
620         readCharacteristic(reportCharacteristic);
621         return true;
622     }
623
624     @Override
625     public void close() {
626     }
627
628     @Override
629     public void setFrozen(boolean frozen) {
630         mFrozen = frozen;
631     }
632
633     @Override
634     public void shutdown() {
635         close();
636
637         BluetoothGatt g = mGatt;
638         if (g != null) {
639             g.disconnect();
640             g.close();
641             mGatt = null;
642         }
643         mManager = null;
644         mIsRegistered = false;
645         mIsConnected = false;
646         mOperations.clear();
647     }
648
649 }
650