@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); } }