Introduces mechanism for background rfcomm servers
This change adds a mechanism in the AdapterService which allows it to
register RFCOMM listeners requested by a BluetoothAdapter.
This is so that applications can request the framework to listen for
incoming RFCOMM connections in the background.
Apps can request that the bluetooth manager, via a BluetoothAdapter,
listen on a specific service record, and then transact incoming socket
connections after receiving a notification of availability via a
PendingIntent.
Tag: #feature
Test: Run the CTS Bluetooth Rfcomm Handoff service Test. This requires
two devices.
Bug: 186494155
Ignore-AOSP-First: Need to commit to downstream first in order to
resolve a merge conflict.
Change-Id: Ia0c71969e691e6353f22fe3b7dae4a7500230e03
diff --git a/android/app/src/com/android/bluetooth/btservice/AdapterService.java b/android/app/src/com/android/bluetooth/btservice/AdapterService.java
index 7490cc3..2887246 100644
--- a/android/app/src/com/android/bluetooth/btservice/AdapterService.java
+++ b/android/app/src/com/android/bluetooth/btservice/AdapterService.java
@@ -48,6 +48,8 @@
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothProtoEnums;
+import android.bluetooth.BluetoothServerSocket;
+import android.bluetooth.BluetoothSocket;
import android.bluetooth.BluetoothStatusCodes;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.BufferConstraints;
@@ -57,6 +59,7 @@
import android.bluetooth.IBluetoothMetadataListener;
import android.bluetooth.IBluetoothOobDataCallback;
import android.bluetooth.IBluetoothSocketManager;
+import android.bluetooth.IncomingRfcommSocketInfo;
import android.bluetooth.OobData;
import android.bluetooth.UidTraffic;
import android.companion.CompanionDeviceManager;
@@ -92,6 +95,7 @@
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
+import android.util.Slog;
import android.util.SparseArray;
import com.android.bluetooth.BluetoothMetricsProto;
@@ -136,13 +140,19 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
+import java.time.Duration;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.Executor;
import java.util.function.Predicate;
import java.util.regex.Pattern;
@@ -153,6 +163,7 @@
private static final int MIN_ADVT_INSTANCES_FOR_MA = 5;
private static final int MIN_OFFLOADED_FILTERS = 10;
private static final int MIN_OFFLOADED_SCAN_STORAGE_BYTES = 1024;
+ private static final Duration PENDING_SOCKET_HANDOFF_TIMEOUT = Duration.ofMinutes(1);
private final Object mEnergyInfoLock = new Object();
private int mStackReportedState;
@@ -249,6 +260,7 @@
}
}
+ private BluetoothAdapter mAdapter;
private AdapterProperties mAdapterProperties;
private AdapterState mAdapterStateMachine;
private BondStateMachine mBondStateMachine;
@@ -270,6 +282,10 @@
private boolean mQuietmode = false;
private HashMap<String, CallerInfo> mBondAttemptCallerInfo = new HashMap<>();
+ private final Map<UUID, RfcommListenerData> mBluetoothServerSockets =
+ Collections.synchronizedMap(new HashMap<>());
+ private final Executor mSocketServersExecutor = r -> new Thread(r).start();
+
private AlarmManager mAlarmManager;
private PendingIntent mPendingAlarm;
private BatteryStatsManager mBatteryStatsManager;
@@ -459,6 +475,7 @@
mRemoteDevices.init();
clearDiscoveringPackages();
mBinder = new AdapterServiceBinder(this);
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
mAdapterProperties = new AdapterProperties(this);
mAdapterStateMachine = AdapterState.make(this);
mJniCallbacks = new JniCallbacks(this, mAdapterProperties);
@@ -854,6 +871,8 @@
unregisterReceiver(mAlarmBroadcastReceiver);
+ stopRfcommServerSockets();
+
if (mPendingAlarm != null) {
mAlarmManager.cancel(mPendingAlarm);
mPendingAlarm = null;
@@ -1255,6 +1274,219 @@
mLeAudioService = LeAudioService.getLeAudioService();
}
+ @BluetoothAdapter.RfcommListenerResult
+ private int startRfcommListener(
+ String name,
+ ParcelUuid uuid,
+ PendingIntent pendingIntent,
+ AttributionSource attributionSource) {
+ if (mBluetoothServerSockets.containsKey(uuid.getUuid())) {
+ Slog.d(TAG,
+ String.format(
+ "Cannot start RFCOMM listener: UUID %s already in use.",
+ uuid.getUuid()));
+ return BluetoothStatusCodes.RFCOMM_LISTENER_START_FAILED_UUID_IN_USE;
+ }
+
+ try {
+ startRfcommListenerInternal(name, uuid.getUuid(), pendingIntent, attributionSource);
+ } catch (IOException e) {
+ return BluetoothStatusCodes.RFCOMM_LISTENER_FAILED_TO_CREATE_SERVER_SOCKET;
+ }
+
+ return BluetoothStatusCodes.SUCCESS;
+ }
+
+ @BluetoothAdapter.RfcommListenerResult
+ private int stopRfcommListener(ParcelUuid uuid, AttributionSource attributionSource) {
+ RfcommListenerData listenerData = mBluetoothServerSockets.get(uuid.getUuid());
+
+ if (listenerData == null) {
+ Slog.d(TAG,
+ String.format(
+ "Cannot stop RFCOMM listener: UUID %s is not registered.",
+ uuid.getUuid()));
+ return BluetoothStatusCodes.RFCOMM_LISTENER_OPERATION_FAILED_NO_MATCHING_SERVICE_RECORD;
+ }
+
+ if (attributionSource.getUid() != listenerData.mAttributionSource.getUid()) {
+ return BluetoothStatusCodes.RFCOMM_LISTENER_OPERATION_FAILED_DIFFERENT_APP;
+ }
+
+ // Remove the entry so that it does not try and restart the server socket.
+ mBluetoothServerSockets.remove(uuid.getUuid());
+
+ return listenerData.closeServerAndPendingSockets(mHandler);
+ }
+
+ private IncomingRfcommSocketInfo retrievePendingSocketForServiceRecord(
+ ParcelUuid uuid, AttributionSource attributionSource) {
+ IncomingRfcommSocketInfo socketInfo = new IncomingRfcommSocketInfo();
+
+ RfcommListenerData listenerData = mBluetoothServerSockets.get(uuid.getUuid());
+
+ if (listenerData == null) {
+ socketInfo.status =
+ BluetoothStatusCodes
+ .RFCOMM_LISTENER_OPERATION_FAILED_NO_MATCHING_SERVICE_RECORD;
+ return socketInfo;
+ }
+
+ if (attributionSource.getUid() != listenerData.mAttributionSource.getUid()) {
+ socketInfo.status = BluetoothStatusCodes.RFCOMM_LISTENER_OPERATION_FAILED_DIFFERENT_APP;
+ return socketInfo;
+ }
+
+ BluetoothSocket socket = listenerData.mPendingSockets.poll();
+
+ if (socket == null) {
+ socketInfo.status = BluetoothStatusCodes.RFCOMM_LISTENER_NO_SOCKET_AVAILABLE;
+ return socketInfo;
+ }
+
+ mHandler.removeCallbacksAndEqualMessages(socket);
+
+ socketInfo.bluetoothDevice = socket.getRemoteDevice();
+ socketInfo.pfd = socket.getParcelFileDescriptor();
+ socketInfo.status = BluetoothStatusCodes.SUCCESS;
+
+ return socketInfo;
+ }
+
+ private void handleIncomingRfcommConnections(UUID uuid) {
+ RfcommListenerData listenerData = mBluetoothServerSockets.get(uuid);
+ for (;;) {
+ BluetoothSocket socket;
+ try {
+ socket = listenerData.mServerSocket.accept();
+ } catch (IOException e) {
+ if (mBluetoothServerSockets.containsKey(uuid)) {
+ // The uuid still being in the map indicates that the accept failure is
+ // unexpected. Try and restart the listener.
+ Slog.e(TAG, "Failed to accept socket on " + listenerData.mServerSocket, e);
+ restartRfcommListener(listenerData, uuid);
+ }
+ return;
+ }
+
+ listenerData.mPendingSockets.add(socket);
+ try {
+ listenerData.mPendingIntent.send();
+ } catch (PendingIntent.CanceledException e) {
+ Slog.e(TAG, "PendingIntent for RFCOMM socket notifications cancelled.", e);
+ // The pending intent was cancelled, close the server as there is no longer any way
+ // to notify the app that registered the listener.
+ listenerData.closeServerAndPendingSockets(mHandler);
+ mBluetoothServerSockets.remove(uuid);
+ return;
+ }
+ mHandler.postDelayed(
+ () -> pendingSocketTimeoutRunnable(listenerData, socket),
+ socket,
+ PENDING_SOCKET_HANDOFF_TIMEOUT.toMillis());
+ }
+ }
+
+ // Tries to restart the rfcomm listener for the given UUID
+ private void restartRfcommListener(RfcommListenerData listenerData, UUID uuid) {
+ listenerData.closeServerAndPendingSockets(mHandler);
+ try {
+ startRfcommListenerInternal(
+ listenerData.mName,
+ uuid,
+ listenerData.mPendingIntent,
+ listenerData.mAttributionSource);
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to recreate rfcomm server socket", e);
+
+ mBluetoothServerSockets.remove(uuid);
+ }
+ }
+
+ private void pendingSocketTimeoutRunnable(
+ RfcommListenerData listenerData, BluetoothSocket socket) {
+ boolean socketFound = listenerData.mPendingSockets.remove(socket);
+ if (socketFound) {
+ try {
+ socket.close();
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to close bt socket", e);
+ // We don't care if closing the socket failed, just continue on.
+ }
+ }
+ }
+
+ private void startRfcommListenerInternal(
+ String name, UUID uuid, PendingIntent intent, AttributionSource attributionSource)
+ throws IOException {
+ BluetoothServerSocket bluetoothServerSocket =
+ mAdapter.listenUsingRfcommWithServiceRecord(name, uuid);
+
+ RfcommListenerData listenerData =
+ new RfcommListenerData(bluetoothServerSocket, name, intent, attributionSource);
+
+ mBluetoothServerSockets.put(uuid, listenerData);
+
+ mSocketServersExecutor.execute(() -> handleIncomingRfcommConnections(uuid));
+ }
+
+ private void stopRfcommServerSockets() {
+ synchronized (mBluetoothServerSockets) {
+ mBluetoothServerSockets.forEach((key, value) -> {
+ mBluetoothServerSockets.remove(key);
+ value.closeServerAndPendingSockets(mHandler);
+ });
+ }
+ }
+
+ private static class RfcommListenerData {
+ final BluetoothServerSocket mServerSocket;
+ // Service record name
+ final String mName;
+ // The Intent which contains the Service info to which the incoming socket connections are
+ // handed off to.
+ final PendingIntent mPendingIntent;
+ // AttributionSource for the requester of the RFCOMM listener
+ final AttributionSource mAttributionSource;
+ // Contains the connected sockets which are pending transfer to the app which requested the
+ // listener.
+ final ConcurrentLinkedQueue<BluetoothSocket> mPendingSockets =
+ new ConcurrentLinkedQueue<>();
+
+ RfcommListenerData(
+ BluetoothServerSocket serverSocket,
+ String name,
+ PendingIntent pendingIntent,
+ AttributionSource attributionSource) {
+ mServerSocket = serverSocket;
+ mName = name;
+ mPendingIntent = pendingIntent;
+ mAttributionSource = attributionSource;
+ }
+
+ int closeServerAndPendingSockets(Handler handler) {
+ int result = BluetoothStatusCodes.SUCCESS;
+ try {
+ mServerSocket.close();
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to call close on rfcomm server socket", e);
+ result = BluetoothStatusCodes.RFCOMM_LISTENER_FAILED_TO_CLOSE_SERVER_SOCKET;
+ }
+ mPendingSockets.forEach(
+ pendingSocket -> {
+ handler.removeCallbacksAndEqualMessages(pendingSocket);
+ try {
+ pendingSocket.close();
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to close socket", e);
+ }
+ });
+ mPendingSockets.clear();
+
+ return result;
+ }
+ }
+
private boolean isAvailable() {
return !mCleaningUp;
}
@@ -2676,6 +2908,26 @@
enforceBluetoothPrivilegedPermission(service);
return service.allowLowLatencyAudio(allowed, device);
}
+
+ @Override
+ public int startRfcommListener(
+ String name,
+ ParcelUuid uuid,
+ PendingIntent pendingIntent,
+ AttributionSource attributionSource) {
+ return mService.startRfcommListener(name, uuid, pendingIntent, attributionSource);
+ }
+
+ @Override
+ public int stopRfcommListener(ParcelUuid uuid, AttributionSource attributionSource) {
+ return mService.stopRfcommListener(uuid, attributionSource);
+ }
+
+ @Override
+ public IncomingRfcommSocketInfo retrievePendingSocketForServiceRecord(
+ ParcelUuid uuid, AttributionSource attributionSource) {
+ return mService.retrievePendingSocketForServiceRecord(uuid, attributionSource);
+ }
}
// ----API Methods--------
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index 8c1c896..7cfc442 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -44,6 +44,7 @@
public final class BluetoothAdapter {
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public boolean addOnMetadataChangedListener(@NonNull android.bluetooth.BluetoothDevice, @NonNull java.util.concurrent.Executor, @NonNull android.bluetooth.BluetoothAdapter.OnMetadataChangedListener);
+ method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public int closeRfcommServer(@NonNull java.util.UUID);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public boolean disable(boolean);
method @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public boolean disableBLE();
method @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public boolean enableBLE();
@@ -56,9 +57,11 @@
method public boolean isLeEnabled();
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED, android.Manifest.permission.MODIFY_PHONE_STATE}) public boolean removeActiveDevice(int);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public boolean removeOnMetadataChangedListener(@NonNull android.bluetooth.BluetoothDevice, @NonNull android.bluetooth.BluetoothAdapter.OnMetadataChangedListener);
+ method @NonNull @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public android.bluetooth.BluetoothSocket retrieveConnectedRfcommSocket(@NonNull java.util.UUID);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED, android.Manifest.permission.MODIFY_PHONE_STATE}) public boolean setActiveDevice(@NonNull android.bluetooth.BluetoothDevice, int);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_SCAN, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public int setDiscoverableTimeout(@NonNull java.time.Duration);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_SCAN, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public int setScanMode(int);
+ method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public int startRfcommServer(@NonNull String, @NonNull java.util.UUID, @NonNull android.app.PendingIntent);
field public static final String ACTION_BLE_STATE_CHANGED = "android.bluetooth.adapter.action.BLE_STATE_CHANGED";
field public static final String ACTION_REQUEST_BLE_SCAN_ALWAYS_AVAILABLE = "android.bluetooth.adapter.action.REQUEST_BLE_SCAN_ALWAYS_AVAILABLE";
field public static final int ACTIVE_DEVICE_ALL = 2; // 0x2
@@ -279,6 +282,12 @@
field public static final int ERROR_PROFILE_NOT_CONNECTED = 14; // 0xe
field public static final int ERROR_TIMEOUT = 15; // 0xf
field public static final int NOT_ALLOWED = 401; // 0x191
+ field public static final int RFCOMM_LISTENER_FAILED_TO_CLOSE_SERVER_SOCKET = 2004; // 0x7d4
+ field public static final int RFCOMM_LISTENER_FAILED_TO_CREATE_SERVER_SOCKET = 2003; // 0x7d3
+ field public static final int RFCOMM_LISTENER_NO_SOCKET_AVAILABLE = 2005; // 0x7d5
+ field public static final int RFCOMM_LISTENER_OPERATION_FAILED_DIFFERENT_APP = 2002; // 0x7d2
+ field public static final int RFCOMM_LISTENER_OPERATION_FAILED_NO_MATCHING_SERVICE_RECORD = 2001; // 0x7d1
+ field public static final int RFCOMM_LISTENER_START_FAILED_UUID_IN_USE = 2000; // 0x7d0
}
public final class BluetoothUuid {
diff --git a/framework/java/android/bluetooth/BluetoothAdapter.java b/framework/java/android/bluetooth/BluetoothAdapter.java
index 7b4cba2..1b085bd 100644
--- a/framework/java/android/bluetooth/BluetoothAdapter.java
+++ b/framework/java/android/bluetooth/BluetoothAdapter.java
@@ -29,6 +29,7 @@
import android.annotation.SdkConstant.SdkConstantType;
import android.annotation.SuppressLint;
import android.annotation.SystemApi; //import android.app.PropertyInvalidatedCache;
+import android.app.PendingIntent;
import android.bluetooth.BluetoothDevice.Transport;
import android.bluetooth.BluetoothFrameworkInitializer;
import android.bluetooth.BluetoothProfile.ConnectionPolicy;
@@ -235,6 +236,31 @@
UUID.fromString("2d410339-82b6-42aa-b34e-e2e01df8cc1a");
/**
+ * Used as an optional extra field for the {@link PendingIntent} provided to {@link
+ * #startRfcommServer(String, UUID, PendingIntent)}. This is useful for when an
+ * application registers multiple RFCOMM listeners, and needs a way to determine which service
+ * record the incoming {@link BluetoothSocket} is using.
+ *
+ * @hide
+ */
+ public static final String EXTRA_RFCOMM_LISTENER_ID =
+ "android.bluetooth.adapter.extra.RFCOMM_LISTENER_ID";
+
+ /** @hide */
+ @IntDef(value = {
+ BluetoothStatusCodes.SUCCESS,
+ BluetoothStatusCodes.ERROR_TIMEOUT,
+ BluetoothStatusCodes.RFCOMM_LISTENER_START_FAILED_UUID_IN_USE,
+ BluetoothStatusCodes.RFCOMM_LISTENER_OPERATION_FAILED_NO_MATCHING_SERVICE_RECORD,
+ BluetoothStatusCodes.RFCOMM_LISTENER_OPERATION_FAILED_DIFFERENT_APP,
+ BluetoothStatusCodes.RFCOMM_LISTENER_FAILED_TO_CREATE_SERVER_SOCKET,
+ BluetoothStatusCodes.RFCOMM_LISTENER_FAILED_TO_CLOSE_SERVER_SOCKET,
+ BluetoothStatusCodes.RFCOMM_LISTENER_NO_SOCKET_AVAILABLE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RfcommListenerResult {}
+
+ /**
* Human-readable string helper for AdapterState
*
* @hide
@@ -2825,6 +2851,135 @@
}
/**
+ * Requests the framework to start an RFCOMM socket server which listens based on the provided
+ * {@code name} and {@code uuid}.
+ * <p>
+ * Incoming connections will cause the system to start the component described in the {@link
+ * PendingIntent}, {@code pendingIntent}. After the component is started, it should obtain a
+ * {@link BluetoothAdapter} and retrieve the {@link BluetoothSocket} via {@link
+ * #retrieveConnectedRfcommSocket(UUID)}.
+ * <p>
+ * An application may register multiple RFCOMM listeners. It is recommended to set the extra
+ * field {@link #EXTRA_RFCOMM_LISTENER_ID} to help determine which service record the incoming
+ * {@link BluetoothSocket} is using.
+ * <p>
+ * The provided {@link PendingIntent} must be created with the {@link
+ * PendingIntent#FLAG_IMMUTABLE} flag.
+ *
+ * @param name service name for SDP record
+ * @param uuid uuid for SDP record
+ * @param pendingIntent component which is called when a new RFCOMM connection is available
+ * @return a status code from {@link BluetoothStatusCodes}
+ * @throws IllegalArgumentException if {@code pendingIntent} is not created with the {@link
+ * PendingIntent#FLAG_IMMUTABLE} flag.
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED
+ })
+ @RfcommListenerResult
+ public int startRfcommServer(@NonNull String name, @NonNull UUID uuid,
+ @NonNull PendingIntent pendingIntent) {
+ if (!pendingIntent.isImmutable()) {
+ throw new IllegalArgumentException("The provided PendingIntent is not immutable");
+ }
+ try {
+ return mService.startRfcommListener(
+ name, new ParcelUuid(uuid), pendingIntent, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to transact RFCOMM listener start request", e);
+ return BluetoothStatusCodes.ERROR_TIMEOUT;
+ }
+ }
+
+ /**
+ * Closes the RFCOMM socket server listening on the given SDP record name and UUID. This can be
+ * called by applications after calling {@link #startRfcommServer(String, UUID,
+ * PendingIntent)} to stop listening for incoming RFCOMM connections.
+ *
+ * @param uuid uuid for SDP record
+ * @return a status code from {@link BluetoothStatusCodes}
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @RfcommListenerResult
+ public int closeRfcommServer(@NonNull UUID uuid) {
+ try {
+ return mService.stopRfcommListener(new ParcelUuid(uuid), mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to transact RFCOMM listener stop request", e);
+ return BluetoothStatusCodes.ERROR_TIMEOUT;
+ }
+ }
+
+ /**
+ * Retrieves a connected {@link BluetoothSocket} for the given service record from a RFCOMM
+ * listener which was registered with {@link #startRfcommServer(String, UUID, PendingIntent)}.
+ * <p>
+ * This method should be called by the component started by the {@link PendingIntent} which was
+ * registered during the call to {@link #startRfcommServer(String, UUID, PendingIntent)} in
+ * order to retrieve the socket.
+ *
+ * @param uuid the same UUID used to register the listener previously
+ * @return a connected {@link BluetoothSocket} or {@code null} if no socket is available
+ * @throws IllegalStateException if the socket could not be retrieved because the application is
+ * trying to obtain a socket for a listener it did not register (incorrect {@code
+ * uuid}).
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @NonNull BluetoothSocket retrieveConnectedRfcommSocket(@NonNull UUID uuid) {
+ IncomingRfcommSocketInfo socketInfo;
+
+ try {
+ socketInfo =
+ mService.retrievePendingSocketForServiceRecord(
+ new ParcelUuid(uuid), mAttributionSource);
+ } catch (RemoteException e) {
+ return null;
+ }
+
+ switch (socketInfo.status) {
+ case BluetoothStatusCodes.SUCCESS:
+ try {
+ return BluetoothSocket.createSocketFromOpenFd(
+ socketInfo.pfd,
+ socketInfo.bluetoothDevice,
+ new ParcelUuid(uuid));
+ } catch (IOException e) {
+ return null;
+ }
+ case BluetoothStatusCodes.RFCOMM_LISTENER_OPERATION_FAILED_DIFFERENT_APP:
+ throw new IllegalStateException(
+ String.format(
+ "RFCOMM listener for UUID %s was not registered by this app",
+ uuid));
+ case BluetoothStatusCodes.RFCOMM_LISTENER_NO_SOCKET_AVAILABLE:
+ return null;
+ default:
+ Log.e(TAG,
+ String.format(
+ "Unexpected result: (%d), from the adapter service while retrieving"
+ + " an rfcomm socket",
+ socketInfo.status));
+ return null;
+ }
+ }
+
+ /**
* Create a listening, insecure RFCOMM Bluetooth socket with Service Record.
* <p>The link key is not required to be authenticated, i.e the communication may be
* vulnerable to Person In the Middle attacks. For Bluetooth 2.1 devices,
diff --git a/framework/java/android/bluetooth/BluetoothSocket.java b/framework/java/android/bluetooth/BluetoothSocket.java
index 9d58447..032f9df 100644
--- a/framework/java/android/bluetooth/BluetoothSocket.java
+++ b/framework/java/android/bluetooth/BluetoothSocket.java
@@ -236,6 +236,33 @@
mOutputStream = new BluetoothOutputStream(this);
}
+ /**
+ * Creates a BluetoothSocket from a {@link ParcelFileDescriptor}. This is used for when the
+ * underlying mPfd is transferred to a separate process (e.g. over a binder), and the socket
+ * must be reconstructed.
+ * <p>
+ * The socket should already be connected in this case, so {@link #connect()} should not be
+ * called.
+ *
+ * @param pfd is the {@link ParcelFileDescriptor} for an already connected BluetoothSocket
+ * @param device is the remote {@link BluetoothDevice} that this socket is connected to
+ * @param uuid is the service ID that this RFCOMM connection is using
+ * @throws IOException if socket creation fails.
+ */
+ /*package*/ static BluetoothSocket createSocketFromOpenFd(
+ ParcelFileDescriptor pfd, BluetoothDevice device, ParcelUuid uuid) throws IOException {
+ BluetoothSocket bluetoothSocket =
+ new BluetoothSocket(TYPE_RFCOMM, pfd.getFd(), true, true, device, -1, uuid);
+
+ bluetoothSocket.mPfd = pfd;
+ bluetoothSocket.mSocket = new LocalSocket(pfd.getFileDescriptor());
+ bluetoothSocket.mSocketIS = bluetoothSocket.mSocket.getInputStream();
+ bluetoothSocket.mSocketOS = bluetoothSocket.mSocket.getOutputStream();
+ bluetoothSocket.mSocketState = SocketState.CONNECTED;
+
+ return bluetoothSocket;
+ }
+
private BluetoothSocket(BluetoothSocket s) {
if (VDBG) Log.d(TAG, "Creating new Private BluetoothSocket of type: " + s.mType);
mUuid = s.mUuid;
@@ -718,6 +745,11 @@
}
}
+ /** @hide */
+ public ParcelFileDescriptor getParcelFileDescriptor() {
+ return mPfd;
+ }
+
private String convertAddr(final byte[] addr) {
return String.format(Locale.US, "%02X:%02X:%02X:%02X:%02X:%02X",
addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
diff --git a/framework/java/android/bluetooth/BluetoothStatusCodes.java b/framework/java/android/bluetooth/BluetoothStatusCodes.java
index b1aa27d..5017198 100644
--- a/framework/java/android/bluetooth/BluetoothStatusCodes.java
+++ b/framework/java/android/bluetooth/BluetoothStatusCodes.java
@@ -371,6 +371,57 @@
public static final int ERROR_CALL_ACTIVE = 1119;
/**
+ * Indicates that the RFCOMM listener could not be started due to the requested UUID already
+ * being in use.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int RFCOMM_LISTENER_START_FAILED_UUID_IN_USE = 2000;
+
+ /**
+ * Indicates that the operation could not be competed because the service record on which the
+ * operation was requested on does not exist.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int RFCOMM_LISTENER_OPERATION_FAILED_NO_MATCHING_SERVICE_RECORD = 2001;
+
+ /**
+ * Indicates that the operation could not be completed because the application requesting the
+ * operation on the RFCOMM listener was not the one which registered it.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int RFCOMM_LISTENER_OPERATION_FAILED_DIFFERENT_APP = 2002;
+
+ /**
+ * Indicates that the creation of the underlying BluetoothServerSocket failed.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int RFCOMM_LISTENER_FAILED_TO_CREATE_SERVER_SOCKET = 2003;
+
+ /**
+ * Indicates that closing the underlying BluetoothServerSocket failed.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int RFCOMM_LISTENER_FAILED_TO_CLOSE_SERVER_SOCKET = 2004;
+
+ /**
+ * Indicates that there is no socket available to retrieve from the given listener.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int RFCOMM_LISTENER_NO_SOCKET_AVAILABLE = 2005;
+
+ /**
* Indicates that an unknown error has occurred has occurred.
*/
public static final int ERROR_UNKNOWN = Integer.MAX_VALUE;
diff --git a/system/binder/Android.bp b/system/binder/Android.bp
index 5baca98..54b7f01 100644
--- a/system/binder/Android.bp
+++ b/system/binder/Android.bp
@@ -50,6 +50,7 @@
"android/bluetooth/IBluetoothOobDataCallback.aidl",
"android/bluetooth/IBluetoothLeCallControl.aidl",
"android/bluetooth/IBluetoothLeCallControlCallback.aidl",
+ "android/bluetooth/IncomingRfcommSocketInfo.aidl",
"android/bluetooth/le/IAdvertisingSetCallback.aidl",
"android/bluetooth/le/IPeriodicAdvertisingCallback.aidl",
"android/bluetooth/le/IScannerCallback.aidl",
diff --git a/system/binder/android/bluetooth/IBluetooth.aidl b/system/binder/android/bluetooth/IBluetooth.aidl
index 95ee1cb..c472873 100644
--- a/system/binder/android/bluetooth/IBluetooth.aidl
+++ b/system/binder/android/bluetooth/IBluetooth.aidl
@@ -16,6 +16,7 @@
package android.bluetooth;
+import android.app.PendingIntent;
import android.bluetooth.IBluetoothCallback;
import android.bluetooth.IBluetoothConnectionCallback;
import android.bluetooth.IBluetoothMetadataListener;
@@ -25,6 +26,7 @@
import android.bluetooth.BluetoothActivityEnergyInfo;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.IncomingRfcommSocketInfo;
import android.bluetooth.OobData;
import android.content.AttributionSource;
import android.os.ParcelUuid;
@@ -258,4 +260,11 @@
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})")
boolean allowLowLatencyAudio(in boolean allowed, in BluetoothDevice device);
+
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
+ int startRfcommListener(String name, in ParcelUuid uuid, in PendingIntent intent, in AttributionSource attributionSource);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
+ int stopRfcommListener(in ParcelUuid uuid, in AttributionSource attributionSource);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
+ IncomingRfcommSocketInfo retrievePendingSocketForServiceRecord(in ParcelUuid uuid, in AttributionSource attributionSource);
}
diff --git a/system/binder/android/bluetooth/IncomingRfcommSocketInfo.aidl b/system/binder/android/bluetooth/IncomingRfcommSocketInfo.aidl
new file mode 100644
index 0000000..38ffe6f
--- /dev/null
+++ b/system/binder/android/bluetooth/IncomingRfcommSocketInfo.aidl
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2022, The Android Open Source Project
+ *
+ * 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 android.bluetooth;
+
+import android.bluetooth.BluetoothDevice;
+import android.os.ParcelFileDescriptor;
+
+/**
+ * Output parameters for IBluetooth.retrievePendingSocketForServiceRecord
+ *
+ * @hide
+ */
+parcelable IncomingRfcommSocketInfo {
+ /** The underlying file descriptor for the socket. */
+ ParcelFileDescriptor pfd;
+ /** The bluetooth device info for the remote device that this socket is connected to. */
+ BluetoothDevice bluetoothDevice;
+ /**
+ * Status info on whether or not the socket was retrieved successfully. See
+ * BluetoothAdapter.RfcommListenerResult for possible values.
+ */
+ int status;
+}
+