android-project/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java
author Sam Lantinga <slouken@libsdl.org>
Fri, 02 Nov 2018 17:25:00 -0700
changeset 12389 5817dbd75619
parent 12307 461ef7221483
permissions -rw-r--r--
Fixed bug 4320 - Android remove reflection for HIDDeviceBLESteamController

Sylvain

Uneeded use of reflection to access connectGatt method in HIDDeviceBLESteamController.java

The method is API 23

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