summaryrefslogtreecommitdiff
path: root/framework/java/android/bluetooth/BluetoothLeAudio.java
diff options
context:
space:
mode:
Diffstat (limited to 'framework/java/android/bluetooth/BluetoothLeAudio.java')
-rw-r--r--framework/java/android/bluetooth/BluetoothLeAudio.java209
1 files changed, 192 insertions, 17 deletions
diff --git a/framework/java/android/bluetooth/BluetoothLeAudio.java b/framework/java/android/bluetooth/BluetoothLeAudio.java
index 23e9a039f4..83fdd03dcf 100644
--- a/framework/java/android/bluetooth/BluetoothLeAudio.java
+++ b/framework/java/android/bluetooth/BluetoothLeAudio.java
@@ -20,11 +20,13 @@ package android.bluetooth;
import static android.bluetooth.BluetoothUtils.getSyncTimeout;
import android.Manifest;
+import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
@@ -38,7 +40,11 @@ import android.util.Log;
import com.android.modules.utils.SynchronousResultReceiver;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executor;
import java.util.concurrent.TimeoutException;
/**
@@ -56,9 +62,46 @@ public final class BluetoothLeAudio implements BluetoothProfile, AutoCloseable {
private static final boolean DBG = false;
private static final boolean VDBG = false;
+ private final Map<Callback, Executor> mCallbackExecutorMap = new HashMap<>();
+
private CloseGuard mCloseGuard;
/**
+ * This class provides a callback that is invoked when audio codec config changes on
+ * the remote device.
+ *
+ * @hide
+ */
+ @SystemApi
+ public interface Callback {
+ /**
+ * Callback invoked when callback is registered and when codec config
+ * changes on the remote device.
+ *
+ * @param groupId the group id
+ * @param status latest codec status for this group
+ * @hide
+ */
+ @SystemApi
+ void onCodecConfigChanged(int groupId,
+ @NonNull BluetoothLeAudioCodecStatus status);
+ }
+
+ @SuppressLint("AndroidFrameworkBluetoothPermission")
+ private final IBluetoothLeAudioCallback mCallback = new IBluetoothLeAudioCallback.Stub() {
+ @Override
+ public void onCodecConfigChanged(int groupId,
+ @NonNull BluetoothLeAudioCodecStatus status) {
+ for (Map.Entry<BluetoothLeAudio.Callback, Executor> callbackExecutorEntry:
+ mCallbackExecutorMap.entrySet()) {
+ BluetoothLeAudio.Callback callback = callbackExecutorEntry.getKey();
+ Executor executor = callbackExecutorEntry.getValue();
+ executor.execute(() -> callback.onCodecConfigChanged(groupId, status));
+ }
+ }
+ };
+
+ /**
* Intent used to broadcast the change in connection state of the LeAudio
* profile. Please note that in the binaural case, there will be two different LE devices for
* the left and right side and each device will have their own connection state changes.
@@ -166,23 +209,6 @@ public final class BluetoothLeAudio implements BluetoothProfile, AutoCloseable {
public static final String ACTION_LE_AUDIO_CONF_CHANGED =
"android.bluetooth.action.LE_AUDIO_CONF_CHANGED";
- /**
- * Intent used to broadcast the audio codec config changed information.
- *
- * <p>This intent will have 2 extras:
- * <ul>
- * <li> {@link BluetoothLeAudioCodecStatus#EXTRA_LE_AUDIO_CODEC_STATUS} - The codec status.
- * </li>
- * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device if the device is currently
- * connected, otherwise it is not included.</li>
- * </ul>
- *
- * @hide
- */
- @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
- @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
- public static final String ACTION_LE_AUDIO_CODEC_CONFIG_CHANGED =
- "android.bluetooth.action.LE_AUDIO_CODEC_CONFIG_CHANGED";
/**
* Indicates unspecified audio content.
@@ -377,6 +403,37 @@ public final class BluetoothLeAudio implements BluetoothProfile, AutoCloseable {
}
};
+
+ @SuppressLint("AndroidFrameworkBluetoothPermission")
+ private final IBluetoothStateChangeCallback mBluetoothStateChangeCallback =
+ new IBluetoothStateChangeCallback.Stub() {
+ public void onBluetoothStateChange(boolean up) {
+ if (DBG) Log.d(TAG, "onBluetoothStateChange: up=" + up);
+ if (up) {
+ // re-register the service-to-app callback
+ synchronized (mCallbackExecutorMap) {
+ if (!mCallbackExecutorMap.isEmpty()) {
+ try {
+ final IBluetoothLeAudio service = getService();
+ if (service != null) {
+ final SynchronousResultReceiver<Integer> recv =
+ new SynchronousResultReceiver();
+ service.registerCallback(mCallback,
+ mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout())
+ .getValue(null);
+ }
+ } catch (TimeoutException e) {
+ Log.e(TAG, "Failed to register callback", e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+ }
+ }
+ };
+
/**
* Create a BluetoothLeAudio proxy object for interacting with the local
* Bluetooth LeAudio service.
@@ -386,6 +443,16 @@ public final class BluetoothLeAudio implements BluetoothProfile, AutoCloseable {
mAdapter = adapter;
mAttributionSource = adapter.getAttributionSource();
mProfileConnector.connect(context, listener);
+
+ IBluetoothManager mgr = mAdapter.getBluetoothManager();
+ if (mgr != null) {
+ try {
+ mgr.registerStateChangeCallback(mBluetoothStateChangeCallback);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
mCloseGuard = new CloseGuard();
mCloseGuard.open("close");
}
@@ -394,6 +461,15 @@ public final class BluetoothLeAudio implements BluetoothProfile, AutoCloseable {
* @hide
*/
public void close() {
+ IBluetoothManager mgr = mAdapter.getBluetoothManager();
+ if (mgr != null) {
+ try {
+ mgr.unregisterStateChangeCallback(mBluetoothStateChangeCallback);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
mProfileConnector.disconnect();
}
@@ -611,6 +687,105 @@ public final class BluetoothLeAudio implements BluetoothProfile, AutoCloseable {
}
/**
+ * Register a {@link Callback} that will be invoked during the
+ * operation of this profile.
+ *
+ * Repeated registration of the same <var>callback</var> object will have no effect after
+ * the first call to this method, even when the <var>executor</var> is different. API caller
+ * would have to call {@link #unregisterCallback(Callback)} with
+ * the same callback object before registering it again.
+ *
+ * @param executor an {@link Executor} to execute given callback
+ * @param callback user implementation of the {@link Callback}
+ * @throws NullPointerException if a null executor or callback is given
+ * @throws IllegalArgumentException the callback is already registered
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public void registerCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull Callback callback) {
+ Objects.requireNonNull(executor, "executor cannot be null");
+ Objects.requireNonNull(callback, "callback cannot be null");
+ if (DBG) log("registerCallback");
+
+ synchronized (mCallbackExecutorMap) {
+ // If the callback map is empty, we register the service-to-app callback
+ if (mCallbackExecutorMap.isEmpty()) {
+ try {
+ final IBluetoothLeAudio service = getService();
+ if (service != null) {
+ final SynchronousResultReceiver<Integer> recv =
+ new SynchronousResultReceiver();
+ service.registerCallback(mCallback, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ }
+ } catch (IllegalStateException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ throw new IllegalStateException("Unexpected error", e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ // Adds the passed in callback to our map of callbacks to executors
+ if (mCallbackExecutorMap.containsKey(callback)) {
+ throw new IllegalArgumentException("This callback has already been registered");
+ }
+ mCallbackExecutorMap.put(callback, executor);
+ }
+ }
+
+ /**
+ * Unregister the specified {@link Callback}.
+ * <p>The same {@link Callback} object used when calling
+ * {@link #registerCallback(Executor, Callback)} must be used.
+ *
+ * <p>Callbacks are automatically unregistered when application process goes away
+ *
+ * @param callback user implementation of the {@link Callback}
+ * @throws NullPointerException when callback is null
+ * @throws IllegalArgumentException when no callback is registered
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public void unregisterCallback(@NonNull Callback callback) {
+ Objects.requireNonNull(callback, "callback cannot be null");
+ if (DBG) log("unregisterCallback");
+
+ synchronized (mCallbackExecutorMap) {
+ if (mCallbackExecutorMap.remove(callback) != null) {
+ throw new IllegalArgumentException("This callback has not been registered");
+ }
+ }
+
+ // If the callback map is empty, we unregister the service-to-app callback
+ if (mCallbackExecutorMap.isEmpty()) {
+ try {
+ final IBluetoothLeAudio service = getService();
+ if (service != null) {
+ final SynchronousResultReceiver<Integer> recv = new SynchronousResultReceiver();
+ service.unregisterCallback(mCallback, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ }
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
* Select a connected device as active.
*
* The active device selection is per profile. An active device's