react-native-bluetooth-classic
react-native-bluetooth-classic copied to clipboard
[Help needed for iOS] A2DP support, currently only Android.
After some considerations:
- I have limited knowledge of reading a Android or iOS Native module. So, it's a bit hard to add A2DP support directly to this library.
- My barrier to market require Android 4.1 support, although the MVP does not require one.
I decided to write an Android Native Module for BluetoothProfile.SPP and BluetoothProfile.A2DP. I hope this helps the development for this library.
Mobile Device Environment
- Device: Android 4.1
- OS: API 16
Is your feature request related to a problem? Please describe.
My goal is to make a Walkie Talkie app with Bluetooth Microphone. BluetoothProfile.SPP will emit event to the React Native Bridge while BluetoothProfile.A2DP will call AudioManager.startBluetoothSco()
.
Describe the solution you'd like I would like to use this library with react-native-recording. The steps:
- This library connects to a Bluetooth Microphone.
- This library listens to
BluetoothSocket.getInputStream()
, this library emits the event to the React Native bridge. - The React Native bridge calls the
AudioManager.startBluetoothSco()
to start recording audio from the Bluetooth Microphone. -
react-native-recording
will start getting input from the Audio Manager.
Additional context This is only for Android Native Module, I hope this can help.
Know bugs to fix:
- [ ] The AudioManager refuse to record audio from the Bluetooth Microphone if the app went to the Background Mode after 2 minutes.
RNBluetoothNMPackage.java
package com.satpam.RNBluetoothNM;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Intent;
import android.util.Log;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.BaseActivityEventListener;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.satpam.RNBluetoothNM.A2DP.A2DPService;
import com.satpam.RNBluetoothNM.SPP.SPPService;
import java.util.Set;
public class RNBluetoothNMPackage extends ReactContextBaseJavaModule {
@NonNull
@Override
public String getName() {
return "RNBluetoothNM";
}
private static final int getBondedDeviceRequestCode = 0;
private Promise mPromise;
private final ActivityEventListener activityListener = new BaseActivityEventListener() {
@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
super.onActivityResult(activity, requestCode, resultCode, data);
if (requestCode == getBondedDeviceRequestCode && resultCode == activity.RESULT_OK) {
getBondedDevices(mPromise);
mPromise = null;
}
}
};
public RNBluetoothNMPackage(ReactApplicationContext reactContext) {
super(reactContext);
// Add a listener for `onActivityResult`
reactContext.addActivityEventListener(activityListener);
}
private void _requestEnableBluetooth(Promise promise) {
Intent enableAdapter = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
Activity activity = getCurrentActivity();
// Store the promise to resolve/reject when onActivityResult returns value;
mPromise = promise;
activity.startActivityForResult(enableAdapter, getBondedDeviceRequestCode);
}
@ReactMethod
public void getBondedDevices(final Promise promise) {
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter == null) {
promise.reject("getBondedDevices", "BluetoothAdapter is not supported");
} else {
if (!bluetoothAdapter.isEnabled()) {
_requestEnableBluetooth(promise);
}
Set<BluetoothDevice> devices = bluetoothAdapter.getBondedDevices();
if (devices.size() > 0) {
WritableArray array = Arguments.createArray();
for (BluetoothDevice device : devices) {
WritableMap writableMap = Arguments.createMap();
writableMap.putString("name", device.getName());
writableMap.putString("address", device.getAddress());
writableMap.putInt("bondState", device.getBondState());
array.pushMap(writableMap);
}
promise.resolve(array);
}
}
}
private A2DPService a2DPService;
private SPPService sppService;
@ReactMethod
public void connectDevice(String address, final Promise promise) {
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter == null) {
promise.reject("connectDevice", "BluetoothAdapter is not supported");
} else {
if (!bluetoothAdapter.isEnabled()) {
_requestEnableBluetooth(promise);
}
BluetoothDevice bluetoothDevice = bluetoothAdapter.getRemoteDevice(address);
a2DPService = new A2DPService(getReactApplicationContext());
boolean a2dpIsBonded = a2DPService.createBond(bluetoothDevice);
if (a2dpIsBonded == false) {
promise.reject("A2DPService", "A2DP failed to bond");
} else {
boolean a2dpIsConnected = a2DPService.connectA2DP(bluetoothDevice);
if (a2dpIsConnected == false) {
promise.reject("A2DP", "A2DP failed to connect");
} else {
sppService = new SPPService(getReactApplicationContext());
boolean sppIsConnected = sppService.connectSPP(bluetoothDevice);
if (sppIsConnected == false) {
promise.reject("SPPService", "A2DP failed to bond");
} else {
promise.resolve(true);
Thread listener = new Thread(sppService);
listener.start();
}
}
}
}
}
@ReactMethod
public void startBluetoothSco(final Promise promise) {
if (a2DPService == null) {
promise.reject("startBluetoothSco", "A2DPService is null");
} else {
boolean isBluetoothScoStarted = a2DPService.startBluetoothSco();
if (isBluetoothScoStarted == false) {
promise.reject("startBluetoothSco", "A2DPService is not connected");
} else {
promise.resolve(true);
}
}
}
@ReactMethod
public void stopBluetoothSco(final Promise promise) {
if (a2DPService == null) {
promise.reject("stopBluetoothSco", "A2DPService is null");
} else {
boolean isBluetoothScoStopped = a2DPService.stopBluetoothSco();
if (isBluetoothScoStopped == false) {
promise.reject("stopBluetoothSco", "A2DPService is not connected");
} else {
promise.resolve(true);
}
}
}
}
A2DPService
package com.satpam.RNBluetoothNM.A2DP;
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.media.AudioManager;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class A2DPService {
private BluetoothDevice mBluetoothDevice;
private BluetoothA2dp mBluetoothA2dp;
private AudioManager mAudioManager;
public A2DPService(ReactApplicationContext reactContext) {
mAudioManager = (AudioManager) reactContext.getSystemService(Context.AUDIO_SERVICE);
BluetoothAdapter
.getDefaultAdapter()
.getProfileProxy(reactContext, new BluetoothProfile.ServiceListener() {
@Override
public void onServiceConnected(int i, BluetoothProfile bluetoothProfile) {
if (i == BluetoothProfile.A2DP) {
mBluetoothA2dp = (BluetoothA2dp) bluetoothProfile;
}
}
@Override
public void onServiceDisconnected(int i) {
if (i == BluetoothProfile.A2DP) {
mBluetoothA2dp = null;
}
}
}, BluetoothProfile.A2DP);
}
public boolean createBond(BluetoothDevice bluetoothDevice) {
mBluetoothDevice = bluetoothDevice;
return mBluetoothDevice.createBond();
}
public boolean connectA2DP(BluetoothDevice bluetoothDevice) {
if (mBluetoothDevice == null) {
return false;
} else {
try {
Method method = BluetoothA2dp.class.getMethod("connect", BluetoothDevice.class);
method.invoke(mBluetoothA2dp, bluetoothDevice);
return true;
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
return false;
}
}
}
public int getConnectedState() {
return mBluetoothA2dp.getConnectionState(mBluetoothDevice);
}
public boolean startBluetoothSco() {
if (getConnectedState() != BluetoothProfile.STATE_CONNECTED) {
return false;
} else {
mAudioManager.setBluetoothScoOn(true);
mAudioManager.startBluetoothSco();
return true;
}
}
public boolean stopBluetoothSco() {
if (getConnectedState() != BluetoothProfile.STATE_CONNECTED) {
return false;
} else {
mAudioManager.setBluetoothScoOn(false);
mAudioManager.stopBluetoothSco();
return true;
}
}
}
SPPService.java
package com.satpam.RNBluetoothNM.SPP;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.util.Log;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import java.io.IOException;
import java.util.Arrays;
import java.util.UUID;
public class SPPService implements Runnable {
private static final UUID BLUETOOTH_SPP = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
private ReactApplicationContext mReactContext;
public SPPService(ReactApplicationContext reactContext) {
mReactContext = reactContext;
}
private BluetoothSocket bluetoothSocket;
public boolean connectSPP(BluetoothDevice bluetoothDevice) {
try {
bluetoothSocket = bluetoothDevice.createRfcommSocketToServiceRecord(BLUETOOTH_SPP);
bluetoothSocket.connect();
return bluetoothSocket.isConnected();
} catch (IOException e) {
return false;
}
}
@Override
public void run() {
byte[] buffer = new byte[1024];
int len;
while (bluetoothSocket.isConnected()) {
try {
len = bluetoothSocket.getInputStream().read(buffer);
byte[] data = Arrays.copyOf(buffer, len);
if (len > 0) {
mReactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("SPPService", new String(data));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Hey, this looks good, but I don't think there is anything I need to add in the library. The current library gives you the ability to:
- Connect to SPP
- Listen to SPP
- Disconnect from SPP
which you're essentially replicating in your version. You also have the A2DP service which you're using to control the AudoManager. I don't see any real reason why you couldn't continue to use this library for your SPP management, and use your own custom library solely for the A2DP/AudioManager logic? Unless I'm missing something, the only reason I see to merge them is just so you can use the same nativeModules.BluetoothNMModule
in order to communicate.
All this should be possible by implementing your own custom ConnectionConnector
and DeviceConnection
classes.
This will probably never work on IOS using Bluetooth Classic, as the framework locks down all audio communication by the operating system. You'd also need to work with the Bluetooth vendor to get the MFi codes, which is an extremely annoying/expensive process.
In no way do I expect this library to be the be-all and end-all to Bluetooth communication, but in my opinion for the lengths that you're using it and your requirements, you should definitely be using a 100% custom module - specialized to your process.
I get your point. Thank you for the earlier explanation regarding Bluetooth on different issue thread
. It helps me to write my first Android Native Module.
I will start writing a custom module for the Bluetooth and audio recorder & player.
Do you think we should close this issue or leave it open for someone else looking for A2DP support?
Feel free to leave it open.
I honestly love the idea of helping you get this project completed, and will do my best to answer any questions / provide any feedback that I can. Some of the stuff in here I see positives to, I just need to figure out the best way to manage the additions/changes.
At this point I feel like creating a new react-native-bluetooth-profiles
project would be better suited for this. From what I've read there is no reason why the two cannot work together (from your sample code) it's definitely possible. For example, in your sample code you:
connectToDevice
First you do this
BluetoothDevice bluetoothDevice = bluetoothAdapter.getRemoteDevice(address);
a2DPService = new A2DPService(getReactApplicationContext());
boolean a2dpIsBonded = a2DPService.createBond(bluetoothDevice)
which is an async action, and would just returns true
if the process begun, and not actually if the device was paired. Therefore it's possible you call the connect
method before it's even paired. These two things should be separated in my mind. For example, you should only try connecting if the device is already paired/bonded.
Then you connect to SPP. Does the order matter? Does A2DP need to be connected before SPP in order for this to work? If not, this could be done just as easily (as mentioned) with a customized SPPAndA2dpDeviceConnection
since they are managed together, and a connection is only valid when both of them are complete.
Anyhow, definitely keep posting details and questions.