Skip to content

Instantly share code, notes, and snippets.

@liamkernighan
Created February 8, 2020 17:31
Show Gist options
  • Save liamkernighan/aa43a076c1283f03977e32722026a565 to your computer and use it in GitHub Desktop.
Save liamkernighan/aa43a076c1283f03977e32722026a565 to your computer and use it in GitHub Desktop.
Java ble service
package com.nasladdin.partner.services;
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import com.nasladdin.partner.application.GlobalApplication;
import com.nasladdin.partner.dependencyinjection.DependencyContainer;
import com.nasladdin.partner.helpers.sharedpreferences.BluetoothDevicePreferencesManager;
import java.util.List;
import java.util.UUID;
public class BluetoothLeService extends Service {
public class LocalBinder extends Binder {
public BluetoothLeService getService() {
return BluetoothLeService.this;
}
}
//region constants and enums
/** Данная характеристика поступает при нажатии кнопки сканера и поднесении RFID-метки.
* Подписываемся на эту неё при подключении сканера RFID-меток
*/
private final static UUID DATA_ARRIVED_SCANNER_CHARACTERISTIC_UUID = UUID.fromString("0000ffe1-0000-1000-8000-00805f9b34fb");
/**
* Набор байт, который ожидает устройство для того, чтобы начать сканирование. "Программное нажатие кнопки".
*/
private final static byte[] SEND_START_SCANNING_WITHOUT_BUTTON_COMMAND = new byte[] { 6, 0, 1, 4, 0, -84, 54, 0, 0, 0 }; // Взято из кода китайцев.
// todo удалить
public final static String RFID_ARRIVED_INTENT_EXTRA = "RFID_ARRIVED_INTENT_EXTRA";
public final static String DEVICE_DISCONNECTED_ADAPTER_STATE = "DEVICE_DISCONNECTED_ADAPTER_STATE";
public final static String CONNECTED_DEVICE_NAME_INTENT_KEY = "CONNECTED_DEVICE_NAME_INTENT_KEY";
public final static String CONNECTED_DEVICE_VIEW_ENTITY_INTENT_KEY = "CONNECTED_DEVICE_VIEW_ENTITY_INTENT_KEY";
public enum ConnectionState {
CONNECTED,
CONNECTING,
DISCONNECTED,
}
public enum IntentAction {
CONNECTED ("CONNECTED"),
DISCONNECTED ("DISCONNECTED"),
SINGLE_BUTTON_RFID_ARRIVED("SINGLE_BUTTON_RFID_ARRIVED"),
BATCH_WITHOUT_BUTTON_RFID_ARRIVED("BATCH_WITHOUT_BUTTON_RFID_ARRIVED");
private String _title;
IntentAction(String title) {
_title = title;
}
public String getTitle() {
return this._title;
}
}
private enum ScanMode {
SINGLE_WITH_BUTTON, BATCH_WITHOUT_BUTTON,
}
//endregion
//region props
private BluetoothManager _bluetoothManager;
private BluetoothAdapter _bluetoothAdapter;
private String _bluetoothDeviceAddress;
private String _bluetoothDeviceName;
private BluetoothGatt _bluetoothGatt;
private ConnectionState _connectionState = ConnectionState.DISCONNECTED;
public ConnectionState getConnectionState() {
return _connectionState;
}
private final IBinder _binder = new LocalBinder();
private BluetoothDevicePreferencesManager _bluetoothDevicePreferencesManager;
private BluetoothGattCharacteristic _notificationDataCharacteristic; // Храним в классе для отправки данных на устройство.
private StringBuilder _batchScanStringBuilder = new StringBuilder();
private ScanMode _scanMode = ScanMode.SINGLE_WITH_BUTTON;
@Override
public void onCreate() {
super.onCreate();
DependencyContainer container = GlobalApplication.Companion.getDependencyContainer();
_bluetoothDevicePreferencesManager = container.getBluetoothDevicePreferencesManager();
}
private final BluetoothGattCallback _gattCallback = new BluetoothGattCallback() {
@Override
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
super.onDescriptorRead(gatt, descriptor, status);
}
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (_connectionState == ConnectionState.CONNECTED && newState == BluetoothProfile.STATE_CONNECTED) {
return;
}
if (newState == BluetoothProfile.STATE_CONNECTED) {
setStateConnected();
_bluetoothGatt.discoverServices();
broadcastConnectedEvent();
}
else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
broadcastDisconnectedEvent();
}
}
private void setStateConnected() {
_connectionState = ConnectionState.CONNECTED;
_bluetoothDevicePreferencesManager.setDevice(_bluetoothDeviceAddress, _bluetoothDeviceName);
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
_bluetoothGatt = gatt;
subscribeToRfidCharacteristicNotification(gatt.getServices());
}
}
private void subscribeToRfidCharacteristicNotification(List<BluetoothGattService> services) {
for (BluetoothGattService service : services) {
BluetoothGattCharacteristic characteristic = service.getCharacteristic(DATA_ARRIVED_SCANNER_CHARACTERISTIC_UUID);
if (characteristic != null) {
_bluetoothGatt.setCharacteristicNotification(characteristic, true);
_notificationDataCharacteristic = characteristic;
break;
}
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
}
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
if (!characteristic.getUuid().equals(DATA_ARRIVED_SCANNER_CHARACTERISTIC_UUID)) {
return;
}
broadcastScannedRfid(characteristic);
}
};
//endregion
public String getCurrentlyConnectedDeviceName() {
if (_connectionState == ConnectionState.CONNECTED) {
return _bluetoothDeviceName;
}
return null;
}
@Override
public IBinder onBind(Intent intent) {
return _binder;
}
@Override
public boolean onUnbind(Intent intent) {
close();
return super.onUnbind(intent);
}
private void close() {
if (_bluetoothGatt == null) {
return;
}
_bluetoothGatt.close();
_bluetoothGatt = null;
}
public boolean initBluetoothAdapter() {
if (_bluetoothManager == null) {
_bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
if (_bluetoothManager == null) {
return false;
}
}
_bluetoothAdapter = _bluetoothManager.getAdapter();
return _bluetoothAdapter != null;
}
public void beginConnectingDevice(String address) {
// Обязательно высвобождаем ресурсы и отписываемся ото всех событий текущего подключения.
disconnectCurrentDevice();
if (_bluetoothAdapter == null || address == null) {
return;
}
// Previously connected device. Try to reconnect.
if (address.equals(_bluetoothDeviceAddress) && _connectionState != ConnectionState.CONNECTED) {
if (_bluetoothGatt != null && _bluetoothGatt.connect()) {
_connectionState = ConnectionState.CONNECTING;
}
return;
}
if (address.equals(_bluetoothDeviceAddress)) {
return;
}
final BluetoothDevice device = _bluetoothAdapter.getRemoteDevice(address);
if (device == null) {
return;
}
_bluetoothGatt = device.connectGatt(this, true, _gattCallback);
_bluetoothDeviceAddress = address;
_bluetoothDeviceName = device.getName();
_connectionState = ConnectionState.CONNECTING;
}
public void disconnectCurrentDevice() {
_bluetoothDeviceAddress = null;
_bluetoothDeviceName = null;
_connectionState = ConnectionState.DISCONNECTED;
if (_bluetoothGatt != null) {
_bluetoothGatt.disconnect();
_bluetoothGatt.close(); // close обязателен. В противном случае будем ловить события с предыдущих подключений и неизвестные ошибки. Второй вариант - _adapter.cancelDiscovery(), но этот тоже хорошо работает.
_bluetoothGatt = null;
}
_bluetoothDevicePreferencesManager.deleteDevice();
}
private void broadcastScannedRfid(BluetoothGattCharacteristic characteristic) {
final byte[] data = characteristic.getValue();
if (data == null || data.length == 0) return;
String rfid = bytesToHexString(data, 0 , data.length);
String intentAction = _scanMode == ScanMode.SINGLE_WITH_BUTTON
? IntentAction.SINGLE_BUTTON_RFID_ARRIVED.getTitle()
: IntentAction.BATCH_WITHOUT_BUTTON_RFID_ARRIVED.getTitle();
if (_scanMode == ScanMode.BATCH_WITHOUT_BUTTON) {
_batchScanStringBuilder.append(rfid);
}
Intent intent = new Intent(intentAction);
intent.putExtra(RFID_ARRIVED_INTENT_EXTRA, rfid);
sendBroadcast(intent);
}
public void resetAccumulatedBatchRfids() {
_batchScanStringBuilder.setLength(0);
}
public String getAccumulatedBatchScanRfids() {
return _batchScanStringBuilder.toString();
}
private void broadcastDisconnectedEvent() {
final Intent intent = new Intent(IntentAction.DISCONNECTED.getTitle());
intent.putExtra(DEVICE_DISCONNECTED_ADAPTER_STATE, _bluetoothAdapter.isEnabled());
sendBroadcast(intent);
}
private void broadcastConnectedEvent() {
final Intent intent = new Intent(IntentAction.CONNECTED.getTitle());
intent.putExtra(CONNECTED_DEVICE_NAME_INTENT_KEY, getCurrentlyConnectedDeviceName());
sendBroadcast(intent);
}
public void startBatchScanningRfidsWithoutButton() {
// todo если нажать на кнопку при пакетном сканировании, то мы валимся с ошибкой.
if (_bluetoothAdapter == null || _bluetoothGatt == null) {
throw new IllegalStateException("Bluetooth is not initialized.");
}
_notificationDataCharacteristic.setValue(SEND_START_SCANNING_WITHOUT_BUTTON_COMMAND);
_scanMode = ScanMode.BATCH_WITHOUT_BUTTON;
_bluetoothGatt.writeCharacteristic(_notificationDataCharacteristic);
}
public void resetModeToSingleButton() {
_scanMode = ScanMode.SINGLE_WITH_BUTTON;
}
public static String bytesToHexString(byte[] src, int offset, int length) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
String asHex = Integer.toHexString(src[i + offset] & 0xFF);
sb.append((asHex.length() == 1) ? "0" + asHex : asHex);
}
return sb.toString().toUpperCase();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment