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