@Override
    public final void onCharacteristicChanged(
        final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
      final String data = ParserUtils.parse(characteristic);

      if (isBatteryLevelCharacteristic(characteristic)) {
        Logger.i(
            mLogSession,
            "Notification received from " + characteristic.getUuid() + ", value: " + data);
        final int batteryValue =
            characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0);
        Logger.a(mLogSession, "Battery level received: " + batteryValue + "%");
        mCallbacks.onBatteryValueReceived(batteryValue);
      } else {
        final BluetoothGattDescriptor cccd =
            characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID);
        final boolean notifications =
            cccd == null
                || cccd.getValue() == null
                || cccd.getValue().length != 2
                || cccd.getValue()[0] == 0x01;

        if (notifications) {
          Logger.i(
              mLogSession,
              "Notification received from " + characteristic.getUuid() + ", value: " + data);
          onCharacteristicNotified(gatt, characteristic);
        } else { // indications
          Logger.i(
              mLogSession,
              "Indication received from " + characteristic.getUuid() + ", value: " + data);
          onCharacteristicIndicated(gatt, characteristic);
        }
      }
    }
 @Override
 public void onCharacteristicWrite(
     final BluetoothGatt gatt,
     final BluetoothGattCharacteristic characteristic,
     final int status) {
   if (status == BluetoothGatt.GATT_SUCCESS) {
     Logger.i(
         mLogSession,
         "Data written to "
             + characteristic.getUuid()
             + ", value: "
             + ParserUtils.parse(characteristic.getValue()));
     // The value has been written. Notify the manager and proceed with the initialization queue.
     onCharacteristicWrite(gatt, characteristic);
     nextRequest();
   } else if (status == BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION) {
     if (gatt.getDevice().getBondState() != BluetoothDevice.BOND_NONE) {
       DebugLogger.w(TAG, ERROR_AUTH_ERROR_WHILE_BONDED);
       mCallbacks.onError(ERROR_AUTH_ERROR_WHILE_BONDED, status);
     }
   } else {
     DebugLogger.e(TAG, "onCharacteristicRead error " + status);
     onError(ERROR_READ_CHARACTERISTIC, status);
   }
 }
    @Override
    public final void onDescriptorWrite(
        final BluetoothGatt gatt, final BluetoothGattDescriptor descriptor, final int status) {
      if (status == BluetoothGatt.GATT_SUCCESS) {
        Logger.i(
            mLogSession,
            "Data written to descr. "
                + descriptor.getUuid()
                + ", value: "
                + ParserUtils.parse(descriptor));

        if (isServiceChangedCCCD(descriptor)) {
          Logger.a(mLogSession, "Service Changed notifications enabled");
          if (!readBatteryLevel()) nextRequest();
        } else if (isBatteryLevelCCCD(descriptor)) {
          final byte[] value = descriptor.getValue();
          if (value != null && value.length > 0 && value[0] == 0x01) {
            Logger.a(mLogSession, "Battery Level notifications enabled");
            nextRequest();
          } else Logger.a(mLogSession, "Battery Level notifications disabled");
        } else {
          nextRequest();
        }
      } else if (status == BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION) {
        if (gatt.getDevice().getBondState() != BluetoothDevice.BOND_NONE) {
          DebugLogger.w(TAG, ERROR_AUTH_ERROR_WHILE_BONDED);
          mCallbacks.onError(ERROR_AUTH_ERROR_WHILE_BONDED, status);
        }
      } else {
        DebugLogger.e(TAG, "onDescriptorWrite error " + status);
        onError(ERROR_WRITE_DESCRIPTOR, status);
      }
    }
    @Override
    public final void onServicesDiscovered(final BluetoothGatt gatt, final int status) {
      if (status == BluetoothGatt.GATT_SUCCESS) {
        Logger.i(mLogSession, "Services Discovered");
        if (isRequiredServiceSupported(gatt)) {
          Logger.v(mLogSession, "Primary service found");
          final boolean optionalServicesFound = isOptionalServiceSupported(gatt);
          if (optionalServicesFound) Logger.v(mLogSession, "Secondary service found");

          // Notify the parent activity
          mCallbacks.onServicesDiscovered(optionalServicesFound);

          // Obtain the queue of initialization requests
          mInitInProgress = true;
          mInitQueue = initGatt(gatt);

          // When the device is bonded and has Service Changed characteristic, the indications must
          // be enabled first.
          // In case this method returns true we have to continue in the onDescriptorWrite callback
          if (ensureServiceChangedEnabled(gatt)) return;

          // We have discovered services, let's start by reading the battery level value. If the
          // characteristic is not readable, try to enable notifications.
          // If there is no Battery service, proceed with the initialization queue.
          if (!readBatteryLevel()) nextRequest();
        } else {
          Logger.w(mLogSession, "Device is not supported");
          mCallbacks.onDeviceNotSupported();
          disconnect();
        }
      } else {
        DebugLogger.e(TAG, "onServicesDiscovered error " + status);
        onError(ERROR_DISCOVERY_SERVICE, status);
      }
    }
  /**
   * When the device is bonded and has the Generic Attribute service and the Service Changed
   * characteristic this method enables indications on this characteristic. In case one of the
   * requirements is not fulfilled this method returns <code>false</code>.
   *
   * @param gatt the gatt device with services discovered
   * @return <code>true</code> when the request has been sent, <code>false</code> when the device is
   *     not bonded, does not have the Generic Attribute service, the GA service does not have the
   *     Service Changed characteristic or this characteristic does not have the CCCD.
   */
  private boolean ensureServiceChangedEnabled(final BluetoothGatt gatt) {
    if (gatt == null) return false;

    // The Service Changed indications have sense only on bonded devices
    final BluetoothDevice device = gatt.getDevice();
    if (device.getBondState() != BluetoothDevice.BOND_BONDED) return false;

    final BluetoothGattService gaService = gatt.getService(GENERIC_ATTRIBUTE_SERVICE);
    if (gaService == null) return false;

    final BluetoothGattCharacteristic scCharacteristic =
        gaService.getCharacteristic(SERVICE_CHANGED_CHARACTERISTIC);
    if (scCharacteristic == null) return false;

    Logger.i(mLogSession, "Service Changed characteristic found on a bonded device");
    return enableIndications(scCharacteristic);
  }
        @Override
        public void onReceive(final Context context, final Intent intent) {
          final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
          final int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1);
          final int previousBondState =
              intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, -1);

          // Skip other devices
          if (mBluetoothGatt == null
              || !device.getAddress().equals(mBluetoothGatt.getDevice().getAddress())) return;

          Logger.d(
              mLogSession,
              "[Broadcast] Action received: "
                  + BluetoothDevice.ACTION_BOND_STATE_CHANGED
                  + ", bond state changed to: "
                  + bondStateToString(bondState)
                  + " ("
                  + bondState
                  + ")");
          DebugLogger.i(
              TAG,
              "Bond state changed for: "
                  + device.getName()
                  + " new state: "
                  + bondState
                  + " previous: "
                  + previousBondState);

          switch (bondState) {
            case BluetoothDevice.BOND_BONDING:
              mCallbacks.onBondingRequired();
              break;
            case BluetoothDevice.BOND_BONDED:
              Logger.i(mLogSession, "Device bonded");
              mCallbacks.onBonded();

              // Start initializing again.
              // In fact, bonding forces additional, internal service discovery (at least on Nexus
              // devices), so this method may safely be used to start this process again.
              Logger.v(mLogSession, "Discovering Services...");
              Logger.d(mLogSession, "gatt.discoverServices()");
              mBluetoothGatt.discoverServices();
              break;
          }
        }
    @Override
    public final void onCharacteristicRead(
        final BluetoothGatt gatt,
        final BluetoothGattCharacteristic characteristic,
        final int status) {
      if (status == BluetoothGatt.GATT_SUCCESS) {
        Logger.i(
            mLogSession,
            "Read Response received from "
                + characteristic.getUuid()
                + ", value: "
                + ParserUtils.parse(characteristic));

        if (isBatteryLevelCharacteristic(characteristic)) {
          final int batteryValue =
              characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0);
          Logger.a(mLogSession, "Battery level received: " + batteryValue + "%");
          mCallbacks.onBatteryValueReceived(batteryValue);

          // The Battery Level value has been read. Let's try to enable Battery Level notifications.
          // If the Battery Level characteristic does not have the NOTIFY property, proceed with the
          // initialization queue.
          if (!setBatteryNotifications(true)) nextRequest();
        } else {
          // The value has been read. Notify the manager and proceed with the initialization queue.
          onCharacteristicRead(gatt, characteristic);
          nextRequest();
        }
      } else if (status == BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION) {
        if (gatt.getDevice().getBondState() != BluetoothDevice.BOND_NONE) {
          DebugLogger.w(TAG, ERROR_AUTH_ERROR_WHILE_BONDED);
          mCallbacks.onError(ERROR_AUTH_ERROR_WHILE_BONDED, status);
        }
      } else {
        DebugLogger.e(TAG, "onCharacteristicRead error " + status);
        onError(ERROR_READ_CHARACTERISTIC, status);
      }
    }
    @Override
    public final void onConnectionStateChange(
        final BluetoothGatt gatt, final int status, final int newState) {
      Logger.d(
          mLogSession,
          "[Callback] Connection state changed with status: "
              + status
              + " and new state: "
              + newState
              + " ("
              + stateToString(newState)
              + ")");

      if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) {
        // Notify the parent activity/service
        Logger.i(mLogSession, "Connected to " + gatt.getDevice().getAddress());
        mConnected = true;
        mCallbacks.onDeviceConnected();

        /*
         * The onConnectionStateChange event is triggered just after the Android connects to a device.
         * In case of bonded devices, the encryption is reestablished AFTER this callback is called.
         * Moreover, when the device has Service Changed indication enabled, and the list of services has changed (e.g. using the DFU),
         * the indication is received few milliseconds later, depending on the connection interval.
         * When received, Android will start performing a service discovery operation itself, internally.
         *
         * If the mBluetoothGatt.discoverServices() method would be invoked here, if would returned cached services,
         * as the SC indication wouldn't be received yet.
         * Therefore we have to postpone the service discovery operation until we are (almost, as there is no such callback) sure, that it had to be handled.
         * Our tests has shown that 600 ms is enough. It is important to call it AFTER receiving the SC indication, but not necessarily
         * after Android finishes the internal service discovery.
         *
         * NOTE: This applies only for bonded devices with Service Changed characteristic, but to be sure we will postpone
         * service discovery for all devices.
         */
        mHandler.postDelayed(
            new Runnable() {
              @Override
              public void run() {
                // Some proximity tags (e.g. nRF PROXIMITY) initialize bonding automatically when
                // connected.
                if (gatt.getDevice().getBondState() != BluetoothDevice.BOND_BONDING) {
                  Logger.v(mLogSession, "Discovering Services...");
                  Logger.d(mLogSession, "gatt.discoverServices()");
                  gatt.discoverServices();
                }
              }
            },
            600);
      } else {
        if (newState == BluetoothProfile.STATE_DISCONNECTED) {
          if (status != BluetoothGatt.GATT_SUCCESS)
            Logger.w(
                mLogSession,
                "Error: (0x"
                    + Integer.toHexString(status)
                    + "): "
                    + GattError.parseConnectionError(status));

          onDeviceDisconnected();
          mConnected = false;
          if (mUserDisconnected) {
            Logger.i(mLogSession, "Disconnected");
            mCallbacks.onDeviceDisconnected();
            close();
          } else {
            Logger.w(mLogSession, "Connection lost");
            mCallbacks.onLinklossOccur();
            // We are not closing the connection here as the device should try to reconnect
            // automatically.
            // This may be only called when the shouldAutoConnect() method returned true.
          }
          return;
        }

        // TODO Should the disconnect method be called or the connection is still valid? Does this
        // ever happen?
        Logger.e(
            mLogSession,
            "Error (0x"
                + Integer.toHexString(status)
                + "): "
                + GattError.parseConnectionError(status));
        mCallbacks.onError(ERROR_CONNECTION_STATE_CHANGE, status);
      }
    }