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