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