Mon 21 Jul 22:43:21 CEST 2025

This commit is contained in:
sbosse 2025-07-21 23:28:31 +02:00
parent bdad691719
commit b0d1c8a801

View File

@ -0,0 +1,639 @@
// Copyright 2016 Franco Bugnano
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cordova.plugin.networking.bluetooth;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaArgs;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.Manifest;
import android.os.ParcelUuid;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
public class NetworkingBluetooth extends CordovaPlugin {
public static final String TAG = "CordovaNetworkingBluetooth";
public static final String SERVICE_NAME = "CordovaNetworkingBluetooth";
public static final int REQUEST_ENABLE_BT = 1773;
public static final int REQUEST_DISCOVERABLE_BT = 1885;
public static final int START_DISCOVERY_REQ_CODE = 1997;
public static final int READ_BUFFER_SIZE = 4096;
public class SocketSendData {
public CallbackContext mCallbackContext;
public BluetoothSocket mSocket;
public byte[] mData;
public SocketSendData(CallbackContext callbackContext, BluetoothSocket socket, byte[] data) {
this.mCallbackContext = callbackContext;
this.mSocket = socket;
this.mData = data;
}
}
public BluetoothAdapter mBluetoothAdapter = null;
public ConcurrentHashMap<Integer, CallbackContext> mContextForActivity = new ConcurrentHashMap<Integer, CallbackContext>();
public ConcurrentHashMap<Integer, CallbackContext> mContextForPermission = new ConcurrentHashMap<Integer, CallbackContext>();
public CallbackContext mContextForAdapterStateChanged = null;
public CallbackContext mContextForDeviceAdded = null;
public CallbackContext mContextForReceive = null;
public CallbackContext mContextForReceiveError = null;
public CallbackContext mContextForAccept = null;
public CallbackContext mContextForAcceptError = null;
public CallbackContext mContextForEnable = null;
public CallbackContext mContextForDisable = null;
public boolean mDeviceAddedRegistered = false;
public int mPreviousScanMode = BluetoothAdapter.SCAN_MODE_NONE;
public AtomicInteger mSocketId = new AtomicInteger(1);
public ConcurrentHashMap<Integer, BluetoothSocket> mClientSockets = new ConcurrentHashMap<Integer, BluetoothSocket>();
public ConcurrentHashMap<Integer, BluetoothServerSocket> mServerSockets = new ConcurrentHashMap<Integer, BluetoothServerSocket>();
public LinkedBlockingQueue<SocketSendData> mSendQueue = new LinkedBlockingQueue<SocketSendData>();
@Override
public void initialize(CordovaInterface cordova, CordovaWebView webView) {
super.initialize(cordova, webView);
this.mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (this.mBluetoothAdapter != null) {
this.mPreviousScanMode = this.mBluetoothAdapter.getScanMode();
}
cordova.getThreadPool().execute(new Runnable() {
public void run() {
writeLoop();
}
});
}
@Override
public boolean execute(String action, CordovaArgs args, final CallbackContext callbackContext) throws JSONException {
IntentFilter filter;
if (this.mBluetoothAdapter == null) {
callbackContext.error("Device does not support Bluetooth");
return false;
}
if (action.equals("registerAdapterStateChanged")) {
this.mContextForAdapterStateChanged = callbackContext;
filter = new IntentFilter();
filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
filter.addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);
cordova.getActivity().registerReceiver(this.mReceiver, filter);
return true;
} else if (action.equals("registerDeviceAdded")) {
this.mContextForDeviceAdded = callbackContext;
return true;
} else if (action.equals("registerReceive")) {
this.mContextForReceive = callbackContext;
return true;
} else if (action.equals("registerReceiveError")) {
this.mContextForReceiveError = callbackContext;
return true;
} else if (action.equals("registerAccept")) {
this.mContextForAccept = callbackContext;
return true;
} else if (action.equals("registerAcceptError")) {
this.mContextForAcceptError = callbackContext;
return true;
} else if (action.equals("getAdapterState")) {
this.getAdapterState(callbackContext, false);
return true;
} else if (action.equals("requestEnable")) {
if (!this.mBluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
this.prepareActivity(action, args, callbackContext, enableBtIntent, REQUEST_ENABLE_BT);
} else {
callbackContext.success();
}
return true;
} else if (action.equals("enable")) {
// If there already is another enable action pending, call the error callback in order
// to notify that the previous action has been cancelled
if (this.mContextForEnable != null) {
this.mContextForEnable.error(1);
this.mContextForEnable = null;
}
if (!this.mBluetoothAdapter.isEnabled()) {
if (!this.mBluetoothAdapter.enable()) {
callbackContext.error(0);
} else {
// Save the context, in order to send the result once the action has been completed
this.mContextForEnable = callbackContext;
}
} else {
callbackContext.success();
}
return true;
} else if (action.equals("disable")) {
// If there already is another disable action pending, call the error callback in order
// to notify that the previous action has been cancelled
if (this.mContextForDisable != null) {
this.mContextForDisable.error(1);
this.mContextForDisable = null;
}
if (this.mBluetoothAdapter.isEnabled()) {
if (!this.mBluetoothAdapter.disable()) {
callbackContext.error(0);
} else {
// Save the context, in order to send the result once the action has been completed
this.mContextForDisable = callbackContext;
}
} else {
callbackContext.success();
}
return true;
} else if (action.equals("getDevice")) {
String address = args.getString(0);
BluetoothDevice device = this.mBluetoothAdapter.getRemoteDevice(address);
callbackContext.success(this.getDeviceInfo(device));
return true;
} else if (action.equals("getDevices")) {
Set<BluetoothDevice> devices = this.mBluetoothAdapter.getBondedDevices();
JSONArray deviceInfos = new JSONArray();
for (BluetoothDevice device : devices) {
deviceInfos.put(this.getDeviceInfo(device));
}
callbackContext.success(deviceInfos);
return true;
} else if (action.equals("startDiscovery")) {
// Automatically cancel any previous discovery
if (this.mBluetoothAdapter.isDiscovering()) {
this.mBluetoothAdapter.cancelDiscovery();
}
// @blab TODO if (cordova.hasPermission(Manifest.permission.ACCESS_COARSE_LOCATION)) {
if (true) {
this.startDiscovery(callbackContext);
} else {
this.getPermission(callbackContext, START_DISCOVERY_REQ_CODE, Manifest.permission.ACCESS_COARSE_LOCATION);
}
return true;
} else if (action.equals("stopDiscovery")) {
if (this.mBluetoothAdapter.isDiscovering()) {
if (this.mBluetoothAdapter.cancelDiscovery()) {
callbackContext.success();
} else {
callbackContext.error(0);
}
} else {
callbackContext.success();
}
return true;
} else if (action.equals("requestDiscoverable")) {
Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
this.prepareActivity(action, args, callbackContext, discoverableIntent, REQUEST_DISCOVERABLE_BT);
return true;
} else if (action.equals("connect")) {
final String address = args.getString(0);
final String uuid = args.getString(1);
cordova.getThreadPool().execute(new Runnable() {
public void run() {
int socketId;
BluetoothSocket socket;
try {
BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
socket = device.createRfcommSocketToServiceRecord(UUID.fromString(uuid));
// Note: You should always ensure that the device is not performing
// device discovery when you call connect().
// If discovery is in progress, then the connection attempt will be
// significantly slowed and is more likely to fail.
mBluetoothAdapter.cancelDiscovery();
socket.connect();
socketId = mSocketId.getAndIncrement();
mClientSockets.put(socketId, socket);
callbackContext.success(socketId);
} catch (NullPointerException e) {
callbackContext.error(e.getMessage());
return;
} catch (IllegalArgumentException e) {
callbackContext.error(e.getMessage());
return;
} catch (IOException e) {
callbackContext.error(e.getMessage());
return;
}
// Now that the connection has been made, begin the read loop
readLoop(socketId, socket);
}
});
return true;
} else if (action.equals("close")) {
int socketId = args.getInt(0);
BluetoothSocket socket = this.mClientSockets.remove(socketId);
if (socket != null) {
// The socketId refers to a client socket
try {
socket.close();
callbackContext.success();
} catch (IOException e) {
callbackContext.error(e.getMessage());
}
} else {
BluetoothServerSocket serverSocket = this.mServerSockets.remove(socketId);
if (serverSocket != null) {
// The socketId refers to a server socket
try {
serverSocket.close();
callbackContext.success();
} catch (IOException e) {
callbackContext.error(e.getMessage());
}
} else {
// Closing an already closed socket is not an error
callbackContext.success();
}
}
return true;
} else if (action.equals("send")) {
int socketId = args.getInt(0);
byte[] data = args.getArrayBuffer(1);
BluetoothSocket socket = this.mClientSockets.get(socketId);
if (socket != null) {
try {
// The send operation occurs in a separate thread
this.mSendQueue.put(new SocketSendData(callbackContext, socket, data));
} catch (InterruptedException e) {
callbackContext.error(e.getMessage());
}
} else {
callbackContext.error("Invalid socketId");
}
return true;
} else if (action.equals("listenUsingRfcomm")) {
final String uuid = args.getString(0);
cordova.getThreadPool().execute(new Runnable() {
public void run() {
int serverSocketId;
BluetoothServerSocket serverSocket;
try {
serverSocket = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(SERVICE_NAME, UUID.fromString(uuid));
serverSocketId = mSocketId.getAndIncrement();
mServerSockets.put(serverSocketId, serverSocket);
callbackContext.success(serverSocketId);
} catch (NullPointerException e) {
callbackContext.error(e.getMessage());
return;
} catch (IllegalArgumentException e) {
callbackContext.error(e.getMessage());
return;
} catch (IOException e) {
callbackContext.error(e.getMessage());
return;
}
// Now that the server socket has been made, begin the accept loop
acceptLoop(serverSocketId, serverSocket);
}
});
return true;
} else {
callbackContext.error("Invalid action");
return false;
}
}
public void getAdapterState(CallbackContext callbackContext, boolean keepCallback) {
PluginResult pluginResult;
try {
JSONObject adapterState = new JSONObject();
adapterState.put("address", this.mBluetoothAdapter.getAddress());
adapterState.put("name", this.mBluetoothAdapter.getName());
adapterState.put("enabled", this.mBluetoothAdapter.isEnabled());
adapterState.put("discovering", this.mBluetoothAdapter.isDiscovering());
adapterState.put("discoverable", this.mBluetoothAdapter.getScanMode() == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
pluginResult = new PluginResult(PluginResult.Status.OK, adapterState);
pluginResult.setKeepCallback(keepCallback);
callbackContext.sendPluginResult(pluginResult);
} catch (JSONException e) {
pluginResult = new PluginResult(PluginResult.Status.ERROR, e.getMessage());
pluginResult.setKeepCallback(keepCallback);
callbackContext.sendPluginResult(pluginResult);
}
}
public JSONObject getDeviceInfo(BluetoothDevice device) throws JSONException {
JSONObject deviceInfo = new JSONObject();
deviceInfo.put("address", device.getAddress());
deviceInfo.put("name", device.getName());
deviceInfo.put("paired", device.getBondState() == BluetoothDevice.BOND_BONDED);
JSONArray deviceUUIDs = new JSONArray();
ParcelUuid[] uuids = device.getUuids();
if (uuids != null) {
for (int i = 0; i < uuids.length; i++) {
deviceUUIDs.put(uuids[i].toString());
}
}
deviceInfo.put("uuids", deviceUUIDs);
return deviceInfo;
}
public void prepareActivity(String action, CordovaArgs args, CallbackContext callbackContext, Intent intent, int requestCode) {
// If there already is another activity with this request code, call the error callback in order
// to notify that the activity has been cancelled
if (this.mContextForActivity.containsKey(requestCode)) {
callbackContext.error("Attempted to start the same activity twice");
return;
}
// Store the callbackContext, in order to send the result once the activity has been completed
this.mContextForActivity.put(requestCode, callbackContext);
// Store the callbackContext, in order to send the result once the activity has been completed
cordova.startActivityForResult(this, intent, requestCode);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
CallbackContext callbackContext = this.mContextForActivity.remove(requestCode);
if (callbackContext != null) {
if (resultCode == Activity.RESULT_CANCELED) {
callbackContext.error(0);
} else {
callbackContext.success();
}
} else {
// TO DO -- This may be a bug on the JavaScript side, as we get here only if the
// activity has been started twice, before waiting the completion of the first one.
Log.e(TAG, "BUG: onActivityResult -- (callbackContext == null)");
}
}
public final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
PluginResult pluginResult;
if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
int previousState = intent.getIntExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, -1);
// If there was an enable request pending, send the result
if ((previousState == BluetoothAdapter.STATE_TURNING_ON) && (mContextForEnable != null)) {
if (state == BluetoothAdapter.STATE_ON) {
mContextForEnable.success();
} else {
mContextForEnable.error(2);
}
mContextForEnable = null;
}
// If there was a disable request pending, send the result
if ((previousState == BluetoothAdapter.STATE_TURNING_OFF) && (mContextForDisable != null)) {
if (state == BluetoothAdapter.STATE_OFF) {
mContextForDisable.success();
} else {
mContextForDisable.error(2);
}
mContextForDisable = null;
}
// Send the state changed event only if the state is not a transitioning one
if ((state == BluetoothAdapter.STATE_OFF) || (state == BluetoothAdapter.STATE_ON)) {
getAdapterState(mContextForAdapterStateChanged, true);
}
} else if (action.equals(BluetoothAdapter.ACTION_DISCOVERY_STARTED) || action.equals(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) {
getAdapterState(mContextForAdapterStateChanged, true);
} else if (action.equals(BluetoothDevice.ACTION_FOUND)) {
try {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
JSONObject deviceInfo = getDeviceInfo(device);
pluginResult = new PluginResult(PluginResult.Status.OK, deviceInfo);
pluginResult.setKeepCallback(true);
mContextForDeviceAdded.sendPluginResult(pluginResult);
} catch (JSONException e) {
pluginResult = new PluginResult(PluginResult.Status.ERROR, e.getMessage());
pluginResult.setKeepCallback(true);
mContextForDeviceAdded.sendPluginResult(pluginResult);
}
} else if (action.equals(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED)) {
// BUG: The documented EXTRA_PREVIOUS_SCAN_MODE field of the intent is not implemented on Android.
// For details see:
// http://stackoverflow.com/questions/30553911/extra-previous-scan-mode-always-returns-an-error-for-android-bluetooth
// As a workaround, the previous scan mode is handled manually here
int scanMode = intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE, -1);
// Report only the transitions from/to SCAN_MODE_CONNECTABLE_DISCOVERABLE
if ((scanMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) || (mPreviousScanMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE)) {
getAdapterState(mContextForAdapterStateChanged, true);
}
mPreviousScanMode = scanMode;
}
}
};
public void readLoop(int socketId, BluetoothSocket socket) {
byte[] readBuffer = new byte[READ_BUFFER_SIZE];
byte[] data;
ArrayList<PluginResult> multipartMessages;
PluginResult pluginResult;
try {
InputStream stream = socket.getInputStream();
int bytesRead;
while (socket.isConnected()) {
bytesRead = stream.read(readBuffer);
if (bytesRead < 0) {
throw new IOException("Disconnected");
} else if (bytesRead > 0) {
data = Arrays.copyOf(readBuffer, bytesRead);
multipartMessages = new ArrayList<PluginResult>();
multipartMessages.add(new PluginResult(PluginResult.Status.OK, socketId));
multipartMessages.add(new PluginResult(PluginResult.Status.OK, data));
pluginResult = new PluginResult(PluginResult.Status.OK, multipartMessages);
pluginResult.setKeepCallback(true);
this.mContextForReceive.sendPluginResult(pluginResult);
}
}
} catch (IOException e) {
try {
JSONObject info = new JSONObject();
info.put("socketId", socketId);
info.put("errorMessage", e.getMessage());
pluginResult = new PluginResult(PluginResult.Status.OK, info);
pluginResult.setKeepCallback(true);
this.mContextForReceiveError.sendPluginResult(pluginResult);
} catch (JSONException ex) {}
}
try {
socket.close();
} catch (IOException e) {}
// The socket has been closed, remove its socketId
this.mClientSockets.remove(socketId);
}
public void acceptLoop(int serverSocketId, BluetoothServerSocket serverSocket) {
int clientSocketId;
BluetoothSocket clientSocket;
ArrayList<PluginResult> multipartMessages;
PluginResult pluginResult;
try {
while (true) {
clientSocket = serverSocket.accept();
if (clientSocket == null) {
throw new IOException("Disconnected");
}
clientSocketId = this.mSocketId.getAndIncrement();
this.mClientSockets.put(clientSocketId, clientSocket);
multipartMessages = new ArrayList<PluginResult>();
multipartMessages.add(new PluginResult(PluginResult.Status.OK, serverSocketId));
multipartMessages.add(new PluginResult(PluginResult.Status.OK, clientSocketId));
pluginResult = new PluginResult(PluginResult.Status.OK, multipartMessages);
pluginResult.setKeepCallback(true);
this.mContextForAccept.sendPluginResult(pluginResult);
this.newReadLoopThread(clientSocketId, clientSocket);
}
} catch (IOException e) {
try {
JSONObject info = new JSONObject();
info.put("socketId", serverSocketId);
info.put("errorMessage", e.getMessage());
pluginResult = new PluginResult(PluginResult.Status.OK, info);
pluginResult.setKeepCallback(true);
this.mContextForAcceptError.sendPluginResult(pluginResult);
} catch (JSONException ex) {}
}
try {
serverSocket.close();
} catch (IOException e) {}
// The socket has been closed, remove its socketId
this.mServerSockets.remove(serverSocketId);
}
public void newReadLoopThread(final int socketId, final BluetoothSocket socket) {
cordova.getThreadPool().execute(new Runnable() {
public void run() {
readLoop(socketId, socket);
}
});
}
public void writeLoop() {
SocketSendData sendData;
try {
while (true) {
sendData = this.mSendQueue.take();
try {
sendData.mSocket.getOutputStream().write(sendData.mData);
sendData.mCallbackContext.success(sendData.mData.length);
} catch (IOException e) {
sendData.mCallbackContext.error(e.getMessage());
}
}
} catch (InterruptedException e) {}
}
public void startDiscovery(CallbackContext callbackContext) {
if (!this.mDeviceAddedRegistered) {
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
cordova.getActivity().registerReceiver(this.mReceiver, filter);
this.mDeviceAddedRegistered = true;
}
if (this.mBluetoothAdapter.startDiscovery()) {
callbackContext.success();
} else {
callbackContext.error(0);
}
}
public void getPermission(CallbackContext callbackContext, int requestCode, String permission) {
// If there already is another permission request with this request code, call the error callback in order
// to notify that the request has been cancelled
if (this.mContextForPermission.containsKey(requestCode)) {
callbackContext.error("Attempted to request the same permission twice");
return;
}
// Store the callbackContext, in order to send the result once the activity has been completed
this.mContextForPermission.put(requestCode, callbackContext);
// @blab TODO
// cordova.requestPermission(this, requestCode, permission);
}
// @blab TODO @Override
public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults) throws JSONException {
CallbackContext callbackContext = this.mContextForPermission.remove(requestCode);
if (requestCode == START_DISCOVERY_REQ_CODE) {
if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
this.startDiscovery(callbackContext);
} else {
callbackContext.error(0);
}
}
}
}