import React from 'react'; import { Permission, PermissionsAndroid, Platform } from 'react-native'; import { BleError, BleErrorCode, BleManager, Device, State } from 'react-native-ble-plx'; import LocalLogger from '../bluetooth/LocalLogger'; import { createErrorFromCode, ErrorTranslations, SFPToolboxError, SFPToolboxErrorCode, userMessageOfError, createErrorFromBleError, } from '../error/error'; import Mutex from '../mutex/Mutex'; import { BluetoothState, DEVICE_NAME_PREFIX, MINIMUM_SIGNAL_STRENGTH, RECONNECT_TIMEOUT_MS, } from './constants'; import { BluetoothContext, BluetoothContextType, TimeOuts } from './context'; import { makeTimeLimited } from './promise'; import { filterIdProbe } from './utils'; import { generateAdditionalInformation, formatErrorForLog } from '../error/utils'; interface BlueProviderTranslations { unavailableState: string; unauthorized: string; poweredOff: string; permissionsTitle: string; permissionsMessage: string; permissionsNeutral: string; permissionsPositive: string; permissionsNegative: string; } interface Props { onDisconnected?: (err: any) => void; mockProbe?: boolean; translations: BlueProviderTranslations; timeOuts: TimeOuts; errorTranslations?: ErrorTranslations; children: React.ReactNode | JSX.Element; } export class BluetoothContextProvider extends React.PureComponent { manager: BleManager; constructor(props: Props) { super(props); this.manager = new BleManager(); this.state = { startScan: this.startScan.bind(this), stopScan: this.stopScan.bind(this), connect: this.connect.bind(this), reconnect: this.reconnect.bind(this), unlink: this.unlink.bind(this), autoConnect: this.autoConnect.bind(this), setTargetDevice: this.setTargetDevice.bind(this), setBatteryPercentage: this.setBatteryPercentage.bind(this), getConnectedDeviceSerialNumber: this.getConnectedDeviceSerialNumber.bind(this), setFirmware: this.setFirmware.bind(this), error2Message: this.error2Message.bind(this), scannedDevices: [], connectionError: null, scanError: null, status: BluetoothState.IDLE, bluetoothState: props.mockProbe ? BluetoothState.CONNECTED : BluetoothState.IDLE, connectedDevice: props.mockProbe ? { name: 'Sonda_41234132', version: '1.1.1', } : null, targetDevice: null, mutex: new Mutex(), batteryPercentage: props.mockProbe ? 85 : null, version: props.mockProbe ? '1.1.1' : '', batteryLoading: props.mockProbe ? false : true, UNKNOWN_FIRMWARE_VERSION: 'Desconocida', mockProbe: props.mockProbe ?? false, timeOuts: props.timeOuts, isConnected: this.isConnected.bind(this), manager: this.manager, isBluetoothUnavailable: this.isBluetoothUnavailable.bind(this), }; if (!props.mockProbe) { this.subscribeStateChanges(); this.manager.state().then(this.bluetoothStateUpdater); } } componentWillUnmount() { (this.manager as any).destroyed = true; this.manager.destroy(); } setBatteryPercentage(aBatteryPercentage) { this.setState(prevState => ({ batteryPercentage: aBatteryPercentage ?? prevState.batteryPercentage, batteryLoading: false, })); } subscribeStateChanges() { this.manager.onStateChange(this.bluetoothStateUpdater); } connectionError(error) { this.setState({ connectionError: error }); } bluetoothState(state) { this.setState({ bluetoothState: state }); } setFirmware(version) { this.setState({ version }); } getConnectedDeviceSerialNumber() { if (this.state.connectedDevice == null) { if (this.state.targetDevice) { return filterIdProbe(this.state.targetDevice.name); } return null; } return filterIdProbe(this.state.connectedDevice.name); } error2Message(error: SFPToolboxError): string { return userMessageOfError(error, this.props.errorTranslations); } async unlink() { if (!this.state.mockProbe) { try { if (this.state.connectedDevice != null) { await this.state.connectedDevice.cancelConnection(); } } catch (error) { if ( ![BleErrorCode.DeviceDisconnected, BleErrorCode.DeviceNotConnected].includes( (error as BleError)?.errorCode, ) ) { throw error; } } } this.setState({ targetDevice: null, connectedDevice: null, batteryPercentage: null, version: '', connectionError: null, scannedDevices: [], batteryLoading: false, bluetoothState: BluetoothState.IDLE, }); } isConnected() { return this.state.bluetoothState === BluetoothState.CONNECTED; } isBluetoothUnavailable() { return this.state.bluetoothState === BluetoothState.UNAVAILABLE; } bluetoothStateUpdater = state => { switch (state) { case State.Unsupported: { return this.bluetoothState(BluetoothState.UNAVAILABLE); } case State.Unauthorized: { return this.bluetoothState(BluetoothState.UNAVAILABLE); } case State.PoweredOff: { return this.bluetoothState(BluetoothState.UNAVAILABLE); } case State.PoweredOn: { this.setState({ scanError: null, }); return this.bluetoothState(BluetoothState.IDLE); } } }; status(status) { this.setState({ status }); } cleanScan() { this.setState({ scanError: null, scannedDevices: [] }); } requestScanPermissions = async () => { const permissionsToAsk = [PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION as Permission].concat( Platform.Version >= 31 ? [ PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT as Permission, PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN as Permission, ] : [], ); const response = await PermissionsAndroid.requestMultiple(permissionsToAsk); if (permissionsToAsk.some(permission => response[permission] !== 'granted')) { throw createErrorFromCode(SFPToolboxErrorCode.InsufficientPermissions); } }; startScan() { this.cleanScan(); const onError = error => { this.setState({ scanError: error, scannedDevices: [], bluetoothState: BluetoothState.IDLE, }); }; this.bluetoothState(BluetoothState.SCANNING); this.requestScanPermissions() .then(() => { this.manager.startDeviceScan(null, null, (bleError, device) => { if (bleError || device == null) { const error = createErrorFromBleError(bleError as BleError); LocalLogger.log(`SCAN_DEVICE_ERROR: ${formatErrorForLog(error)}`); return onError(error); } if ( device.name?.startsWith(DEVICE_NAME_PREFIX) && device.rssi && device.rssi > MINIMUM_SIGNAL_STRENGTH ) { this.setState((prevState: BluetoothContextType) => { const previousScannedDevices = prevState.scannedDevices; if (!previousScannedDevices.find(deviceInList => deviceInList.id === device.id)) { return { scannedDevices: [...prevState.scannedDevices, device], }; } else { return prevState; } }); } }); }) .catch(onError); } stopScan() { this.cleanScan(); this.bluetoothState(BluetoothState.IDLE); this.manager.stopDeviceScan(); } setTargetDevice(targetDevice: { name: string; id: string }) { this.setState({ targetDevice, }); } async reconnect(deviceName) { this.setState({ bluetoothState: BluetoothState.SCANNING, }); return this.requestScanPermissions() .then(async () => { return makeTimeLimited( new Promise((resolve, reject) => { this.manager.startDeviceScan(null, null, (error, device) => { if (error) { reject(error); } if (device?.name === deviceName) { resolve(device); } }); }), this.props.timeOuts.reconnectInMs || RECONNECT_TIMEOUT_MS, ); }) .finally(() => { this.manager.stopDeviceScan(); }) .catch(async bleError => { const error = createErrorFromBleError(bleError); LocalLogger.log( `RECONNECTION_WITH_PROBE_ERROR: ${formatErrorForLog( error, )}\n${await generateAdditionalInformation()}`, ); this.setState({ bluetoothState: BluetoothState.IDLE, connectionError: error, }); throw error; }) .then(device => { return this.connect(device); }); } onDisconnected = error => { this.setState({ connectedDevice: null, batteryPercentage: null, version: '', connectionError: createErrorFromCode(BleErrorCode.DeviceDisconnected), scannedDevices: [], batteryLoading: false, bluetoothState: BluetoothState.IDLE, }); this.props.onDisconnected?.(error); }; async autoConnect(): Promise { this.setState({ connectionError: null, }); if (this.isBluetoothUnavailable()) { const error = createErrorFromCode(SFPToolboxErrorCode.BluetoothNotAvailable); LocalLogger.log( `AUTOCONNECTION_WITH_PROBE_BLUETOOTH_ERROR: ${formatErrorForLog( error, )}\n${await generateAdditionalInformation()}`, ); this.connectionError(error); throw error; } this.setState({ bluetoothState: BluetoothState.CONNECTING, connectionError: null, }); const aDeviceId = this.state.targetDevice?.id; if (!aDeviceId) { throw new Error('Device id not found'); } return this.manager .connectToDevice(aDeviceId, { timeout: this.props.timeOuts.reconnectInMs || RECONNECT_TIMEOUT_MS, }) .then(device => { return device.discoverAllServicesAndCharacteristics(); }) .then(device => { if (!device.name) { device.name = this.state.targetDevice?.name ?? null; } this.setState({ connectedDevice: device, bluetoothState: BluetoothState.CONNECTED, batteryLoading: true, mutex: new Mutex(), connectionError: null, }); device.onDisconnected(this.onDisconnected); return device; }) .catch(async bleError => { const error = createErrorFromBleError(bleError); LocalLogger.log( `AUTOCONNECTION_WITH_PROBE_ERROR: ${formatErrorForLog( error, )}\n${await generateAdditionalInformation()}`, ); this.setState({ bluetoothState: BluetoothState.IDLE, connectionError: error, }); throw error; }); } connect(aDevice: Device, onSetTargetDevice?: Function): Promise { const targetDevice = { name: aDevice.name as string, id: aDevice.id }; this.manager.stopDeviceScan(); this.setState({ bluetoothState: BluetoothState.CONNECTING, connectionError: null, targetDevice, }); onSetTargetDevice?.(targetDevice); return aDevice .connect() .then(device => { return device.discoverAllServicesAndCharacteristics(); }) .then(device => { this.setState({ connectedDevice: device, bluetoothState: BluetoothState.CONNECTED, batteryLoading: true, mutex: new Mutex(), }); device.onDisconnected(this.onDisconnected); return device; }) .catch(async bleError => { const error = createErrorFromBleError(bleError); LocalLogger.log( `CONNECTION_WITH_PROBE_ERROR: ${formatErrorForLog( error, )}\n${await generateAdditionalInformation()}`, ); this.setState({ bluetoothState: BluetoothState.IDLE, connectionError: error, }); throw error; }); } render() { const { children } = this.props; return {children}; } } export default BluetoothContextProvider;