package it.innove; import android.app.Activity; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.os.Build; import androidx.annotation.Nullable; import android.util.Log; import com.facebook.react.bridge.ActivityEventListener; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; import com.facebook.react.modules.core.RCTNativeAppEventEmitter; import java.lang.reflect.Method; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import static android.app.Activity.RESULT_OK; import static android.bluetooth.BluetoothProfile.GATT; import static android.os.Build.VERSION_CODES.LOLLIPOP; class BleManager extends ReactContextBaseJavaModule implements ActivityEventListener { public static final String LOG_TAG = "ReactNativeBleManager"; private static final int ENABLE_REQUEST = 539; private class BondRequest { private String uuid; private Callback callback; BondRequest(String _uuid, Callback _callback) { uuid = _uuid; callback = _callback; } } private BluetoothAdapter bluetoothAdapter; private BluetoothManager bluetoothManager; private Context context; private ReactApplicationContext reactContext; private Callback enableBluetoothCallback; private ScanManager scanManager; private BondRequest bondRequest; private BondRequest removeBondRequest; private boolean forceLegacy; public ReactApplicationContext getReactContext() { return reactContext; } // key is the MAC Address private final Map<String, Peripheral> peripherals = new LinkedHashMap<>(); // scan session id public BleManager(ReactApplicationContext reactContext) { super(reactContext); context = reactContext; this.reactContext = reactContext; reactContext.addActivityEventListener(this); Log.d(LOG_TAG, "BleManager created"); } @Override public String getName() { return "BleManager"; } private BluetoothAdapter getBluetoothAdapter() { if (bluetoothAdapter == null) { BluetoothManager manager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE); bluetoothAdapter = manager.getAdapter(); } return bluetoothAdapter; } private BluetoothManager getBluetoothManager() { if (bluetoothManager == null) { bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE); } return bluetoothManager; } public void sendEvent(String eventName, @Nullable WritableMap params) { getReactApplicationContext().getJSModule(RCTNativeAppEventEmitter.class).emit(eventName, params); } @ReactMethod public void start(ReadableMap options, Callback callback) { Log.d(LOG_TAG, "start"); if (getBluetoothAdapter() == null) { Log.d(LOG_TAG, "No bluetooth support"); callback.invoke("No bluetooth support"); return; } forceLegacy = false; if (options.hasKey("forceLegacy")) { forceLegacy = options.getBoolean("forceLegacy"); } if (Build.VERSION.SDK_INT >= LOLLIPOP && !forceLegacy) { scanManager = new LollipopScanManager(reactContext, this); } else { scanManager = new LegacyScanManager(reactContext, this); } IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); context.registerReceiver(mReceiver, filter); callback.invoke(); Log.d(LOG_TAG, "BleManager initialized"); } @ReactMethod public void enableBluetooth(Callback callback) { if (getBluetoothAdapter() == null) { Log.d(LOG_TAG, "No bluetooth support"); callback.invoke("No bluetooth support"); return; } if (!getBluetoothAdapter().isEnabled()) { enableBluetoothCallback = callback; Intent intentEnable = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); if (getCurrentActivity() == null) callback.invoke("Current activity not available"); else getCurrentActivity().startActivityForResult(intentEnable, ENABLE_REQUEST); } else callback.invoke(); } @ReactMethod public void scan(ReadableArray serviceUUIDs, final int scanSeconds, boolean allowDuplicates, ReadableMap options, Callback callback) { Log.d(LOG_TAG, "scan"); if (getBluetoothAdapter() == null) { Log.d(LOG_TAG, "No bluetooth support"); callback.invoke("No bluetooth support"); return; } if (!getBluetoothAdapter().isEnabled()) { return; } synchronized (peripherals) { for (Iterator<Map.Entry<String, Peripheral>> iterator = peripherals.entrySet().iterator(); iterator .hasNext();) { Map.Entry<String, Peripheral> entry = iterator.next(); if (!entry.getValue().isConnected()) { iterator.remove(); } } } if (scanManager != null) scanManager.scan(serviceUUIDs, scanSeconds, options, callback); } @ReactMethod public void stopScan(Callback callback) { Log.d(LOG_TAG, "Stop scan"); if (getBluetoothAdapter() == null) { Log.d(LOG_TAG, "No bluetooth support"); callback.invoke("No bluetooth support"); return; } if (!getBluetoothAdapter().isEnabled()) { callback.invoke(); return; } if (scanManager != null) { scanManager.stopScan(callback); WritableMap map = Arguments.createMap(); sendEvent("BleManagerStopScan", map); } } @ReactMethod public void createBond(String peripheralUUID, Callback callback) { Log.d(LOG_TAG, "Request bond to: " + peripheralUUID); Set<BluetoothDevice> deviceSet = getBluetoothAdapter().getBondedDevices(); for (BluetoothDevice device : deviceSet) { if (peripheralUUID.equalsIgnoreCase(device.getAddress())) { callback.invoke(); return; } } Peripheral peripheral = retrieveOrCreatePeripheral(peripheralUUID); if (peripheral == null) { callback.invoke("Invalid peripheral uuid"); return; } else if (bondRequest != null) { callback.invoke("Only allow one bond request at a time"); return; } else if (peripheral.getDevice().createBond()) { Log.d(LOG_TAG, "Request bond successful for: " + peripheralUUID); bondRequest = new BondRequest(peripheralUUID, callback); // request bond success, waiting for boradcast return; } callback.invoke("Create bond request fail"); } @ReactMethod private void removeBond(String peripheralUUID, Callback callback) { Log.d(LOG_TAG, "Remove bond to: " + peripheralUUID); Peripheral peripheral = retrieveOrCreatePeripheral(peripheralUUID); if (peripheral == null) { callback.invoke("Invalid peripheral uuid"); return; } else { try { Method m = peripheral.getDevice().getClass().getMethod("removeBond", (Class[]) null); m.invoke(peripheral.getDevice(), (Object[]) null); removeBondRequest = new BondRequest(peripheralUUID, callback); return; } catch (Exception e) { Log.d(LOG_TAG, "Error in remove bond: " + peripheralUUID, e); callback.invoke("Remove bond request fail"); } } } @ReactMethod public void connect(String peripheralUUID, Callback callback) { Log.d(LOG_TAG, "Connect to: " + peripheralUUID); Peripheral peripheral = retrieveOrCreatePeripheral(peripheralUUID); if (peripheral == null) { callback.invoke("Invalid peripheral uuid"); return; } peripheral.connect(callback, getCurrentActivity()); } @ReactMethod public void disconnect(String peripheralUUID, boolean force, Callback callback) { Log.d(LOG_TAG, "Disconnect from: " + peripheralUUID); Peripheral peripheral = peripherals.get(peripheralUUID); if (peripheral != null) { peripheral.disconnect(force); callback.invoke(); } else callback.invoke("Peripheral not found"); } @ReactMethod public void startNotificationUseBuffer(String deviceUUID, String serviceUUID, String characteristicUUID, Integer buffer, Callback callback) { Log.d(LOG_TAG, "startNotification"); if (serviceUUID == null || characteristicUUID == null) { callback.invoke("ServiceUUID and characteristicUUID required."); return; } Peripheral peripheral = peripherals.get(deviceUUID); if (peripheral != null) { peripheral.registerNotify(UUIDHelper.uuidFromString(serviceUUID), UUIDHelper.uuidFromString(characteristicUUID), buffer, callback); } else callback.invoke("Peripheral not found"); } @ReactMethod public void startNotification(String deviceUUID, String serviceUUID, String characteristicUUID, Callback callback) { Log.d(LOG_TAG, "startNotification"); if (serviceUUID == null || characteristicUUID == null) { callback.invoke("ServiceUUID and characteristicUUID required."); return; } Peripheral peripheral = peripherals.get(deviceUUID); if (peripheral != null) { peripheral.registerNotify(UUIDHelper.uuidFromString(serviceUUID), UUIDHelper.uuidFromString(characteristicUUID), 1, callback); } else callback.invoke("Peripheral not found"); } @ReactMethod public void stopNotification(String deviceUUID, String serviceUUID, String characteristicUUID, Callback callback) { Log.d(LOG_TAG, "stopNotification"); if (serviceUUID == null || characteristicUUID == null) { callback.invoke("ServiceUUID and characteristicUUID required."); return; } Peripheral peripheral = peripherals.get(deviceUUID); if (peripheral != null) { peripheral.removeNotify(UUIDHelper.uuidFromString(serviceUUID), UUIDHelper.uuidFromString(characteristicUUID), callback); } else callback.invoke("Peripheral not found"); } @ReactMethod public void write(String deviceUUID, String serviceUUID, String characteristicUUID, ReadableArray message, Integer maxByteSize, Callback callback) { Log.d(LOG_TAG, "Write to: " + deviceUUID); if (serviceUUID == null || characteristicUUID == null) { callback.invoke("ServiceUUID and characteristicUUID required."); return; } Peripheral peripheral = peripherals.get(deviceUUID); if (peripheral != null) { byte[] decoded = new byte[message.size()]; for (int i = 0; i < message.size(); i++) { decoded[i] = new Integer(message.getInt(i)).byteValue(); } Log.d(LOG_TAG, "Message(" + decoded.length + "): " + bytesToHex(decoded)); peripheral.write(UUIDHelper.uuidFromString(serviceUUID), UUIDHelper.uuidFromString(characteristicUUID), decoded, maxByteSize, null, callback, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT); } else callback.invoke("Peripheral not found"); } @ReactMethod public void writeWithoutResponse(String deviceUUID, String serviceUUID, String characteristicUUID, ReadableArray message, Integer maxByteSize, Integer queueSleepTime, Callback callback) { Log.d(LOG_TAG, "Write without response to: " + deviceUUID); if (serviceUUID == null || characteristicUUID == null) { callback.invoke("ServiceUUID and characteristicUUID required."); return; } Peripheral peripheral = peripherals.get(deviceUUID); if (peripheral != null) { byte[] decoded = new byte[message.size()]; for (int i = 0; i < message.size(); i++) { decoded[i] = new Integer(message.getInt(i)).byteValue(); } Log.d(LOG_TAG, "Message(" + decoded.length + "): " + bytesToHex(decoded)); peripheral.write(UUIDHelper.uuidFromString(serviceUUID), UUIDHelper.uuidFromString(characteristicUUID), decoded, maxByteSize, queueSleepTime, callback, BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE); } else callback.invoke("Peripheral not found"); } @ReactMethod public void read(String deviceUUID, String serviceUUID, String characteristicUUID, Callback callback) { Log.d(LOG_TAG, "Read from: " + deviceUUID); if (serviceUUID == null || characteristicUUID == null) { callback.invoke("ServiceUUID and characteristicUUID required."); return; } Peripheral peripheral = peripherals.get(deviceUUID); if (peripheral != null) { peripheral.read(UUIDHelper.uuidFromString(serviceUUID), UUIDHelper.uuidFromString(characteristicUUID), callback); } else callback.invoke("Peripheral not found", null); } @ReactMethod public void retrieveServices(String deviceUUID, ReadableArray services, Callback callback) { Log.d(LOG_TAG, "Retrieve services from: " + deviceUUID); Peripheral peripheral = peripherals.get(deviceUUID); if (peripheral != null) { peripheral.retrieveServices(callback); } else callback.invoke("Peripheral not found", null); } @ReactMethod public void refreshCache(String deviceUUID, Callback callback) { Log.d(LOG_TAG, "Refershing cache for: " + deviceUUID); Peripheral peripheral = peripherals.get(deviceUUID); if (peripheral != null) { peripheral.refreshCache(callback); } else callback.invoke("Peripheral not found"); } @ReactMethod public void readRSSI(String deviceUUID, Callback callback) { Log.d(LOG_TAG, "Read RSSI from: " + deviceUUID); Peripheral peripheral = peripherals.get(deviceUUID); if (peripheral != null) { peripheral.readRSSI(callback); } else callback.invoke("Peripheral not found", null); } private Peripheral savePeripheral(BluetoothDevice device) { String address = device.getAddress(); synchronized (peripherals) { if (!peripherals.containsKey(address)) { Peripheral peripheral; if (Build.VERSION.SDK_INT >= LOLLIPOP && !forceLegacy) { peripheral = new LollipopPeripheral(device, reactContext); } else { peripheral = new Peripheral(device, reactContext); } peripherals.put(device.getAddress(), peripheral); } } return peripherals.get(address); } public Peripheral getPeripheral(BluetoothDevice device) { String address = device.getAddress(); return peripherals.get(address); } public Peripheral savePeripheral(Peripheral peripheral) { synchronized (peripherals) { peripherals.put(peripheral.getDevice().getAddress(), peripheral); } return peripheral; } @ReactMethod public void checkState() { Log.d(LOG_TAG, "checkState"); BluetoothAdapter adapter = getBluetoothAdapter(); String state = "off"; if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { state = "unsupported"; } else if (adapter != null) { switch (adapter.getState()) { case BluetoothAdapter.STATE_ON: state = "on"; break; case BluetoothAdapter.STATE_OFF: state = "off"; } } WritableMap map = Arguments.createMap(); map.putString("state", state); Log.d(LOG_TAG, "state:" + state); sendEvent("BleManagerDidUpdateState", map); } private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Log.d(LOG_TAG, "onReceive"); final String action = intent.getAction(); if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); String stringState = ""; switch (state) { case BluetoothAdapter.STATE_OFF: stringState = "off"; clearPeripherals(); break; case BluetoothAdapter.STATE_TURNING_OFF: stringState = "turning_off"; disconnectPeripherals(); break; case BluetoothAdapter.STATE_ON: stringState = "on"; break; case BluetoothAdapter.STATE_TURNING_ON: stringState = "turning_on"; break; } WritableMap map = Arguments.createMap(); map.putString("state", stringState); Log.d(LOG_TAG, "state: " + stringState); sendEvent("BleManagerDidUpdateState", map); } else if (action.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) { final int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR); final int prevState = intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, BluetoothDevice.ERROR); BluetoothDevice device = (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); String bondStateStr = "UNKNOWN"; switch (bondState) { case BluetoothDevice.BOND_BONDED: bondStateStr = "BOND_BONDED"; break; case BluetoothDevice.BOND_BONDING: bondStateStr = "BOND_BONDING"; break; case BluetoothDevice.BOND_NONE: bondStateStr = "BOND_NONE"; break; } Log.d(LOG_TAG, "bond state: " + bondStateStr); if (bondRequest != null && bondRequest.uuid.equals(device.getAddress())) { if (bondState == BluetoothDevice.BOND_BONDED) { bondRequest.callback.invoke(); bondRequest = null; } else if (bondState == BluetoothDevice.BOND_NONE || bondState == BluetoothDevice.ERROR) { bondRequest.callback.invoke("Bond request has been denied"); bondRequest = null; } } if (bondState == BluetoothDevice.BOND_BONDED) { Peripheral peripheral; if (Build.VERSION.SDK_INT >= LOLLIPOP && !forceLegacy) { peripheral = new LollipopPeripheral(device, reactContext); } else { peripheral = new Peripheral(device, reactContext); } WritableMap map = peripheral.asWritableMap(); sendEvent("BleManagerPeripheralDidBond", map); } if (removeBondRequest != null && removeBondRequest.uuid.equals(device.getAddress()) && bondState == BluetoothDevice.BOND_NONE && prevState == BluetoothDevice.BOND_BONDED) { removeBondRequest.callback.invoke(); removeBondRequest = null; } } } }; private void clearPeripherals() { if (!peripherals.isEmpty()) { synchronized (peripherals) { peripherals.clear(); } } } private void disconnectPeripherals() { if (!peripherals.isEmpty()) { synchronized (peripherals) { for (Peripheral peripheral : peripherals.values()) { if (peripheral.isConnected()) { peripheral.disconnect(true); } } } } } @ReactMethod public void getDiscoveredPeripherals(Callback callback) { Log.d(LOG_TAG, "Get discovered peripherals"); WritableArray map = Arguments.createArray(); synchronized (peripherals) { for (Map.Entry<String, Peripheral> entry : peripherals.entrySet()) { Peripheral peripheral = entry.getValue(); WritableMap jsonBundle = peripheral.asWritableMap(); map.pushMap(jsonBundle); } } callback.invoke(null, map); } @ReactMethod public void getConnectedPeripherals(ReadableArray serviceUUIDs, Callback callback) { Log.d(LOG_TAG, "Get connected peripherals"); WritableArray map = Arguments.createArray(); if (getBluetoothAdapter() == null) { Log.d(LOG_TAG, "No bluetooth support"); callback.invoke("No bluetooth support"); return; } List<BluetoothDevice> periperals = getBluetoothManager().getConnectedDevices(GATT); for (BluetoothDevice entry : periperals) { Peripheral peripheral = savePeripheral(entry); WritableMap jsonBundle = peripheral.asWritableMap(); map.pushMap(jsonBundle); } callback.invoke(null, map); } @ReactMethod public void getBondedPeripherals(Callback callback) { Log.d(LOG_TAG, "Get bonded peripherals"); WritableArray map = Arguments.createArray(); Set<BluetoothDevice> deviceSet = getBluetoothAdapter().getBondedDevices(); for (BluetoothDevice device : deviceSet) { Peripheral peripheral; if (Build.VERSION.SDK_INT >= LOLLIPOP && !forceLegacy) { peripheral = new LollipopPeripheral(device, reactContext); } else { peripheral = new Peripheral(device, reactContext); } WritableMap jsonBundle = peripheral.asWritableMap(); map.pushMap(jsonBundle); } callback.invoke(null, map); } @ReactMethod public void removePeripheral(String deviceUUID, Callback callback) { Log.d(LOG_TAG, "Removing from list: " + deviceUUID); Peripheral peripheral = peripherals.get(deviceUUID); if (peripheral != null) { synchronized (peripherals) { if (peripheral.isConnected()) { callback.invoke("Peripheral can not be removed while connected"); } else { peripherals.remove(deviceUUID); callback.invoke(); } } } else callback.invoke("Peripheral not found"); } @ReactMethod public void requestConnectionPriority(String deviceUUID, int connectionPriority, Callback callback) { Log.d(LOG_TAG, "Request connection priority of " + connectionPriority + " from: " + deviceUUID); Peripheral peripheral = peripherals.get(deviceUUID); if (peripheral != null) { peripheral.requestConnectionPriority(connectionPriority, callback); } else { callback.invoke("Peripheral not found", null); } } @ReactMethod public void requestMTU(String deviceUUID, int mtu, Callback callback) { Log.d(LOG_TAG, "Request MTU of " + mtu + " bytes from: " + deviceUUID); Peripheral peripheral = peripherals.get(deviceUUID); if (peripheral != null) { peripheral.requestMTU(mtu, callback); } else { callback.invoke("Peripheral not found", null); } } private final static char[] hexArray = "0123456789ABCDEF".toCharArray(); public static String bytesToHex(byte[] bytes) { char[] hexChars = new char[bytes.length * 2]; for (int j = 0; j < bytes.length; j++) { int v = bytes[j] & 0xFF; hexChars[j * 2] = hexArray[v >>> 4]; hexChars[j * 2 + 1] = hexArray[v & 0x0F]; } return new String(hexChars); } public static WritableArray bytesToWritableArray(byte[] bytes) { WritableArray value = Arguments.createArray(); for (int i = 0; i < bytes.length; i++) value.pushInt((bytes[i] & 0xFF)); return value; } @Override public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { Log.d(LOG_TAG, "onActivityResult"); if (requestCode == ENABLE_REQUEST && enableBluetoothCallback != null) { if (resultCode == RESULT_OK) { enableBluetoothCallback.invoke(); } else { enableBluetoothCallback.invoke("User refused to enable"); } enableBluetoothCallback = null; } } @Override public void onNewIntent(Intent intent) { } private Peripheral retrieveOrCreatePeripheral(String peripheralUUID) { Peripheral peripheral = peripherals.get(peripheralUUID); if (peripheral == null) { synchronized (peripherals) { if (peripheralUUID != null) { peripheralUUID = peripheralUUID.toUpperCase(); } if (BluetoothAdapter.checkBluetoothAddress(peripheralUUID)) { BluetoothDevice device = bluetoothAdapter.getRemoteDevice(peripheralUUID); if (Build.VERSION.SDK_INT >= LOLLIPOP && !forceLegacy) { peripheral = new LollipopPeripheral(device, reactContext); } else { peripheral = new Peripheral(device, reactContext); } peripherals.put(peripheralUUID, peripheral); } } } return peripheral; } }