Loading android/app/src/com/android/bluetooth/le_audio/LeAudioService.java +430 −90 File changed.Preview size limit exceeded, changes collapsed. Show changes android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioBroadcastServiceTest.java +28 −57 Original line number Diff line number Diff line Loading @@ -150,85 +150,56 @@ public class LeAudioBroadcastServiceTest { @Test public void testCreateBroadcastNative() { int broadcast_profile = 0; byte[] meta = new byte[]{0x02, 0x01, 0x02}; byte[] code = {0x00, 0x01, 0x00}; mService.createBroadcast(meta, broadcast_profile, code); verify(mNativeInterface, times(1)).createBroadcast(eq(meta), eq(broadcast_profile), eq(code)); } BluetoothLeAudioContentMetadata.Builder meta_builder = new BluetoothLeAudioContentMetadata.Builder(); meta_builder.setLanguage("EN"); meta_builder.setProgramInfo("Public broadcast info"); BluetoothLeAudioContentMetadata meta = meta_builder.build(); mService.createBroadcast(meta, code); @Test public void testStartBroadcastNative() { int broadcast_profile = 0; byte[] meta = new byte[]{0x02, 0x01, 0x02}; byte[] code = {0x00, 0x01, 0x00}; mService.createBroadcast(meta, broadcast_profile, code); int broadcast_id = 243; mService.startBroadcast(broadcast_id); verify(mNativeInterface, times(1)).startBroadcast(eq(broadcast_id)); verify(mNativeInterface, times(1)).createBroadcast(eq(meta.getRawMetadata()), eq(1), eq(code)); } @Test public void testStopBroadcastNative() { public void testStartStopBroadcastNative() { int broadcast_profile = 0; byte[] meta = new byte[]{0x02, 0x01, 0x02}; byte[] code = {0x00, 0x01, 0x00}; mService.createBroadcast(meta, broadcast_profile, code); int broadcast_id = 243; mService.stopBroadcast(broadcast_id); verify(mNativeInterface, times(1)).stopBroadcast(eq(broadcast_id)); } BluetoothLeAudioContentMetadata.Builder meta_builder = new BluetoothLeAudioContentMetadata.Builder(); meta_builder.setLanguage("EN"); meta_builder.setProgramInfo("Public broadcast info"); BluetoothLeAudioContentMetadata meta = meta_builder.build(); mService.createBroadcast(meta, code); @Test public void testPauseBroadcastNative() { int broadcast_profile = 0; byte[] meta = new byte[]{0x02, 0x01, 0x02}; byte[] code = {0x00, 0x01, 0x00}; mService.createBroadcast(meta, broadcast_profile, code); int instance_id = 243; mService.startBroadcast(instance_id); verify(mNativeInterface, times(1)).startBroadcast(eq(instance_id)); int broadcast_id = 243; mService.pauseBroadcast(broadcast_id); verify(mNativeInterface, times(1)).pauseBroadcast(eq(broadcast_id)); mService.stopBroadcast(instance_id); verify(mNativeInterface, times(1)).stopBroadcast(eq(instance_id)); } @Test public void testDestroyBroadcastNative() { int broadcast_profile = 0; byte[] meta = new byte[]{0x02, 0x01, 0x02}; byte[] code = {0x00, 0x01, 0x00}; mService.createBroadcast(meta, broadcast_profile, code); BluetoothLeAudioContentMetadata.Builder meta_builder = new BluetoothLeAudioContentMetadata.Builder(); meta_builder.setLanguage("ENG"); meta_builder.setProgramInfo("Public broadcast info"); BluetoothLeAudioContentMetadata meta = meta_builder.build(); mService.createBroadcast(meta, code); int broadcast_id = 243; mService.destroyBroadcast(broadcast_id); verify(mNativeInterface, times(1)).destroyBroadcast(eq(broadcast_id)); } @Test public void testGetBroadcastAddressNative() { int broadcast_profile = 0; byte[] meta = new byte[]{0x02, 0x01, 0x02}; byte[] code = {0x00, 0x01, 0x00}; mService.createBroadcast(meta, broadcast_profile, code); int broadcast_id = 243; mService.getBroadcastId(broadcast_id); verify(mNativeInterface, times(1)).getBroadcastId(eq(broadcast_id)); } @Test public void testGetAllBroadcastStates() { int broadcast_profile = 0; byte[] meta = new byte[]{0x02, 0x01, 0x02}; byte[] code = {0x00, 0x01, 0x00}; mService.createBroadcast(meta, broadcast_profile, code); int broadcast_id = 243; mService.getAllBroadcastStates(); verify(mNativeInterface, times(1)).getAllBroadcastStates(); } // FIXME: Add the missign API test cases private class LeAudioIntentReceiver extends BroadcastReceiver { @Override Loading framework/java/android/bluetooth/BluetoothAdapter.java +7 −0 Original line number Diff line number Diff line Loading @@ -3511,6 +3511,9 @@ public final class BluetoothAdapter { } else if (profile == BluetoothProfile.LE_AUDIO) { BluetoothLeAudio leAudio = new BluetoothLeAudio(context, listener, this); return true; } else if (profile == BluetoothProfile.LE_AUDIO_BROADCAST) { BluetoothLeBroadcast leAudio = new BluetoothLeBroadcast(context, listener); return true; } else if (profile == BluetoothProfile.VOLUME_CONTROL) { BluetoothVolumeControl vcs = new BluetoothVolumeControl(context, listener, this); return true; Loading Loading @@ -3618,6 +3621,10 @@ public final class BluetoothAdapter { BluetoothLeAudio leAudio = (BluetoothLeAudio) proxy; leAudio.close(); break; case BluetoothProfile.LE_AUDIO_BROADCAST: BluetoothLeBroadcast leAudioBroadcast = (BluetoothLeBroadcast) proxy; leAudioBroadcast.close(); break; case BluetoothProfile.VOLUME_CONTROL: BluetoothVolumeControl vcs = (BluetoothVolumeControl) proxy; vcs.close(); Loading framework/java/android/bluetooth/BluetoothLeBroadcast.java +344 −10 Original line number Diff line number Diff line Loading @@ -16,21 +16,33 @@ package android.bluetooth; import static android.bluetooth.BluetoothUtils.getSyncTimeout; import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.bluetooth.annotations.RequiresBluetoothConnectPermission; import android.content.AttributionSource; import android.content.Context; import android.os.IBinder; import android.os.RemoteException; import android.util.CloseGuard; import android.util.Log; import com.android.modules.utils.SynchronousResultReceiver; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.TimeoutException; /** * This class provides the public APIs to control the BAP Broadcast Source profile. Loading @@ -45,6 +57,148 @@ import java.util.concurrent.Executor; public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfile { private static final String TAG = "BluetoothLeBroadcast"; private static final boolean DBG = true; private static final boolean VDBG = false; private CloseGuard mCloseGuard; private final BluetoothAdapter mAdapter; private final AttributionSource mAttributionSource; private final BluetoothProfileConnector<IBluetoothLeAudio> mProfileConnector = new BluetoothProfileConnector(this, BluetoothProfile.LE_AUDIO_BROADCAST, "BluetoothLeAudioBroadcast", IBluetoothLeAudio.class.getName()) { @Override public IBluetoothLeAudio getServiceInterface(IBinder service) { return IBluetoothLeAudio.Stub.asInterface(service); } }; private final Map<Callback, Executor> mCallbackExecutorMap = new HashMap<>(); @SuppressLint("AndroidFrameworkBluetoothPermission") private final IBluetoothLeBroadcastCallback mCallback = new IBluetoothLeBroadcastCallback.Stub() { @Override public void onBroadcastStarted(int reason, int broadcastId) { for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry: mCallbackExecutorMap.entrySet()) { BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); Executor executor = callbackExecutorEntry.getValue(); executor.execute(() -> callback.onBroadcastStarted(reason, broadcastId)); } } @Override public void onBroadcastStartFailed(int reason) { for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry: mCallbackExecutorMap.entrySet()) { BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); Executor executor = callbackExecutorEntry.getValue(); executor.execute(() -> callback.onBroadcastStartFailed(reason)); } } @Override public void onBroadcastStopped(int reason, int broadcastId) { for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry: mCallbackExecutorMap.entrySet()) { BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); Executor executor = callbackExecutorEntry.getValue(); executor.execute(() -> callback.onBroadcastStopped(reason, broadcastId)); } } @Override public void onBroadcastStopFailed(int reason) { for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry: mCallbackExecutorMap.entrySet()) { BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); Executor executor = callbackExecutorEntry.getValue(); executor.execute(() -> callback.onBroadcastStopFailed(reason)); } } @Override public void onPlaybackStarted(int reason, int broadcastId) { for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry: mCallbackExecutorMap.entrySet()) { BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); Executor executor = callbackExecutorEntry.getValue(); executor.execute(() -> callback.onPlaybackStarted(reason, broadcastId)); } } @Override public void onPlaybackStopped(int reason, int broadcastId) { for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry: mCallbackExecutorMap.entrySet()) { BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); Executor executor = callbackExecutorEntry.getValue(); executor.execute(() -> callback.onPlaybackStopped(reason, broadcastId)); } } @Override public void onBroadcastUpdated(int reason, int broadcastId) { for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry: mCallbackExecutorMap.entrySet()) { BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); Executor executor = callbackExecutorEntry.getValue(); executor.execute(() -> callback.onBroadcastUpdated(reason, broadcastId)); } } @Override public void onBroadcastUpdateFailed(int reason, int broadcastId) { for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry: mCallbackExecutorMap.entrySet()) { BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); Executor executor = callbackExecutorEntry.getValue(); executor.execute(() -> callback.onBroadcastUpdateFailed(reason, broadcastId)); } } @Override public void onBroadcastMetadataChanged(int broadcastId, BluetoothLeBroadcastMetadata metadata) { for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry: mCallbackExecutorMap.entrySet()) { BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); Executor executor = callbackExecutorEntry.getValue(); executor.execute(() -> callback.onBroadcastMetadataChanged(broadcastId, metadata)); } } }; @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.registerLeBroadcastCallback(mCallback, mAttributionSource, recv); recv.awaitResultNoInterrupt(getSyncTimeout()) .getValue(null); } } catch (TimeoutException e) { Log.e(TAG, "onBluetoothServiceUp: Failed to register " + "Le Broadcaster callback", e); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } } } } }; /** * Interface for receiving events related to Broadcast Source Loading Loading @@ -171,7 +325,33 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi * @param listener listens for service callbacks across binder * @hide */ /*package*/ BluetoothLeBroadcast(Context context, BluetoothProfile.ServiceListener listener) {} /*package*/ BluetoothLeBroadcast(Context context, BluetoothProfile.ServiceListener listener) { mAdapter = BluetoothAdapter.getDefaultAdapter(); mAttributionSource = mAdapter.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"); } /** * @hide */ protected void finalize() { if (mCloseGuard != null) { mCloseGuard.warnIfOpen(); } close(); } /** * Not supported since LE Audio Broadcasts do not establish a connection. Loading Loading @@ -246,8 +426,36 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi if (callback == null) { throw new IllegalArgumentException("callback cannot be null"); } log("registerCallback"); throw new UnsupportedOperationException("Not Implemented"); if (!isEnabled()) { throw new IllegalStateException("service not enabled"); } 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.registerLeBroadcastCallback(mCallback, mAttributionSource, recv); recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null); } } catch (TimeoutException | IllegalStateException e) { Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); } 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); } } /** Loading @@ -271,8 +479,30 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi if (callback == null) { throw new IllegalArgumentException("callback cannot be null"); } log("unregisterCallback"); throw new UnsupportedOperationException("Not Implemented"); 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.unregisterLeBroadcastCallback(mCallback, mAttributionSource, recv); recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null); } } catch (TimeoutException | IllegalStateException e) { Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } } /** Loading Loading @@ -323,6 +553,17 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi public void startBroadcast(@NonNull BluetoothLeAudioContentMetadata contentMetadata, @Nullable byte[] broadcastCode) { if (DBG) log("startBroadcasting"); final IBluetoothLeAudio service = getService(); if (service == null) { Log.w(TAG, "Proxy not attached to service"); if (DBG) log(Log.getStackTraceString(new Throwable())); } else if (isEnabled()) { try { service.startBroadcast(contentMetadata, broadcastCode, mAttributionSource); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } } /** Loading @@ -346,7 +587,18 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi }) public void updateBroadcast(int broadcastId, @NonNull BluetoothLeAudioContentMetadata contentMetadata) { if (DBG) log("updateBroadcast"); final IBluetoothLeAudio service = getService(); if (service == null) { Log.w(TAG, "Proxy not attached to service"); if (DBG) log(Log.getStackTraceString(new Throwable())); } else if (isEnabled()) { try { service.updateBroadcast(broadcastId, contentMetadata, mAttributionSource); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } } /** Loading @@ -368,6 +620,17 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi }) public void stopBroadcast(int broadcastId) { if (DBG) log("disableBroadcastMode"); final IBluetoothLeAudio service = getService(); if (service == null) { Log.w(TAG, "Proxy not attached to service"); if (DBG) log(Log.getStackTraceString(new Throwable())); } else if (isEnabled()) { try { service.stopBroadcast(broadcastId, mAttributionSource); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } } /** Loading @@ -385,7 +648,23 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi android.Manifest.permission.BLUETOOTH_PRIVILEGED, }) public boolean isPlaying(int broadcastId) { return false; final IBluetoothLeAudio service = getService(); final boolean defaultValue = false; if (service == null) { Log.w(TAG, "Proxy not attached to service"); if (DBG) log(Log.getStackTraceString(new Throwable())); } else if (isEnabled()) { try { final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver(); service.isPlaying(broadcastId, mAttributionSource, recv); return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); } catch (TimeoutException e) { Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } return defaultValue; } /** Loading @@ -402,7 +681,24 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi android.Manifest.permission.BLUETOOTH_PRIVILEGED, }) public @NonNull List<BluetoothLeBroadcastMetadata> getAllBroadcastMetadata() { return Collections.emptyList(); final IBluetoothLeAudio service = getService(); final List<BluetoothLeBroadcastMetadata> defaultValue = Collections.emptyList(); if (service == null) { Log.w(TAG, "Proxy not attached to service"); if (DBG) log(Log.getStackTraceString(new Throwable())); } else if (isEnabled()) { try { final SynchronousResultReceiver<List<BluetoothLeBroadcastMetadata>> recv = new SynchronousResultReceiver(); service.getAllBroadcastMetadata(mAttributionSource, recv); return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); } catch (TimeoutException e) { Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } return defaultValue; } /** Loading @@ -413,7 +709,23 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi @SystemApi @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public int getMaximumNumberOfBroadcast() { return 1; final IBluetoothLeAudio service = getService(); final int defaultValue = 1; if (service == null) { Log.w(TAG, "Proxy not attached to service"); if (DBG) log(Log.getStackTraceString(new Throwable())); } else if (isEnabled()) { try { final SynchronousResultReceiver<Integer> recv = new SynchronousResultReceiver(); service.getMaximumNumberOfBroadcast(mAttributionSource, recv); return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); } catch (TimeoutException e) { Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } return defaultValue; } /** Loading @@ -421,7 +733,29 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi * @hide */ @Override public void close() throws Exception {} public void close() { if (VDBG) log("close()"); IBluetoothManager mgr = mAdapter.getBluetoothManager(); if (mgr != null) { try { mgr.unregisterStateChangeCallback(mBluetoothStateChangeCallback); } catch (RemoteException e) { Log.e(TAG, "", e); } } mProfileConnector.disconnect(); } private boolean isEnabled() { if (mAdapter.getState() == BluetoothAdapter.STATE_ON) return true; return false; } private IBluetoothLeAudio getService() { return mProfileConnector.getService(); } private static void log(String msg) { Log.d(TAG, msg); Loading system/binder/Android.bp +1 −0 Original line number Diff line number Diff line Loading @@ -29,6 +29,7 @@ filegroup { "android/bluetooth/IBluetoothVolumeControl.aidl", "android/bluetooth/IBluetoothHidHost.aidl", "android/bluetooth/IBluetoothLeAudio.aidl", "android/bluetooth/IBluetoothLeBroadcastCallback.aidl", "android/bluetooth/IBluetoothManager.aidl", "android/bluetooth/IBluetoothManagerCallback.aidl", "android/bluetooth/IBluetoothMap.aidl", Loading Loading
android/app/src/com/android/bluetooth/le_audio/LeAudioService.java +430 −90 File changed.Preview size limit exceeded, changes collapsed. Show changes
android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioBroadcastServiceTest.java +28 −57 Original line number Diff line number Diff line Loading @@ -150,85 +150,56 @@ public class LeAudioBroadcastServiceTest { @Test public void testCreateBroadcastNative() { int broadcast_profile = 0; byte[] meta = new byte[]{0x02, 0x01, 0x02}; byte[] code = {0x00, 0x01, 0x00}; mService.createBroadcast(meta, broadcast_profile, code); verify(mNativeInterface, times(1)).createBroadcast(eq(meta), eq(broadcast_profile), eq(code)); } BluetoothLeAudioContentMetadata.Builder meta_builder = new BluetoothLeAudioContentMetadata.Builder(); meta_builder.setLanguage("EN"); meta_builder.setProgramInfo("Public broadcast info"); BluetoothLeAudioContentMetadata meta = meta_builder.build(); mService.createBroadcast(meta, code); @Test public void testStartBroadcastNative() { int broadcast_profile = 0; byte[] meta = new byte[]{0x02, 0x01, 0x02}; byte[] code = {0x00, 0x01, 0x00}; mService.createBroadcast(meta, broadcast_profile, code); int broadcast_id = 243; mService.startBroadcast(broadcast_id); verify(mNativeInterface, times(1)).startBroadcast(eq(broadcast_id)); verify(mNativeInterface, times(1)).createBroadcast(eq(meta.getRawMetadata()), eq(1), eq(code)); } @Test public void testStopBroadcastNative() { public void testStartStopBroadcastNative() { int broadcast_profile = 0; byte[] meta = new byte[]{0x02, 0x01, 0x02}; byte[] code = {0x00, 0x01, 0x00}; mService.createBroadcast(meta, broadcast_profile, code); int broadcast_id = 243; mService.stopBroadcast(broadcast_id); verify(mNativeInterface, times(1)).stopBroadcast(eq(broadcast_id)); } BluetoothLeAudioContentMetadata.Builder meta_builder = new BluetoothLeAudioContentMetadata.Builder(); meta_builder.setLanguage("EN"); meta_builder.setProgramInfo("Public broadcast info"); BluetoothLeAudioContentMetadata meta = meta_builder.build(); mService.createBroadcast(meta, code); @Test public void testPauseBroadcastNative() { int broadcast_profile = 0; byte[] meta = new byte[]{0x02, 0x01, 0x02}; byte[] code = {0x00, 0x01, 0x00}; mService.createBroadcast(meta, broadcast_profile, code); int instance_id = 243; mService.startBroadcast(instance_id); verify(mNativeInterface, times(1)).startBroadcast(eq(instance_id)); int broadcast_id = 243; mService.pauseBroadcast(broadcast_id); verify(mNativeInterface, times(1)).pauseBroadcast(eq(broadcast_id)); mService.stopBroadcast(instance_id); verify(mNativeInterface, times(1)).stopBroadcast(eq(instance_id)); } @Test public void testDestroyBroadcastNative() { int broadcast_profile = 0; byte[] meta = new byte[]{0x02, 0x01, 0x02}; byte[] code = {0x00, 0x01, 0x00}; mService.createBroadcast(meta, broadcast_profile, code); BluetoothLeAudioContentMetadata.Builder meta_builder = new BluetoothLeAudioContentMetadata.Builder(); meta_builder.setLanguage("ENG"); meta_builder.setProgramInfo("Public broadcast info"); BluetoothLeAudioContentMetadata meta = meta_builder.build(); mService.createBroadcast(meta, code); int broadcast_id = 243; mService.destroyBroadcast(broadcast_id); verify(mNativeInterface, times(1)).destroyBroadcast(eq(broadcast_id)); } @Test public void testGetBroadcastAddressNative() { int broadcast_profile = 0; byte[] meta = new byte[]{0x02, 0x01, 0x02}; byte[] code = {0x00, 0x01, 0x00}; mService.createBroadcast(meta, broadcast_profile, code); int broadcast_id = 243; mService.getBroadcastId(broadcast_id); verify(mNativeInterface, times(1)).getBroadcastId(eq(broadcast_id)); } @Test public void testGetAllBroadcastStates() { int broadcast_profile = 0; byte[] meta = new byte[]{0x02, 0x01, 0x02}; byte[] code = {0x00, 0x01, 0x00}; mService.createBroadcast(meta, broadcast_profile, code); int broadcast_id = 243; mService.getAllBroadcastStates(); verify(mNativeInterface, times(1)).getAllBroadcastStates(); } // FIXME: Add the missign API test cases private class LeAudioIntentReceiver extends BroadcastReceiver { @Override Loading
framework/java/android/bluetooth/BluetoothAdapter.java +7 −0 Original line number Diff line number Diff line Loading @@ -3511,6 +3511,9 @@ public final class BluetoothAdapter { } else if (profile == BluetoothProfile.LE_AUDIO) { BluetoothLeAudio leAudio = new BluetoothLeAudio(context, listener, this); return true; } else if (profile == BluetoothProfile.LE_AUDIO_BROADCAST) { BluetoothLeBroadcast leAudio = new BluetoothLeBroadcast(context, listener); return true; } else if (profile == BluetoothProfile.VOLUME_CONTROL) { BluetoothVolumeControl vcs = new BluetoothVolumeControl(context, listener, this); return true; Loading Loading @@ -3618,6 +3621,10 @@ public final class BluetoothAdapter { BluetoothLeAudio leAudio = (BluetoothLeAudio) proxy; leAudio.close(); break; case BluetoothProfile.LE_AUDIO_BROADCAST: BluetoothLeBroadcast leAudioBroadcast = (BluetoothLeBroadcast) proxy; leAudioBroadcast.close(); break; case BluetoothProfile.VOLUME_CONTROL: BluetoothVolumeControl vcs = (BluetoothVolumeControl) proxy; vcs.close(); Loading
framework/java/android/bluetooth/BluetoothLeBroadcast.java +344 −10 Original line number Diff line number Diff line Loading @@ -16,21 +16,33 @@ package android.bluetooth; import static android.bluetooth.BluetoothUtils.getSyncTimeout; import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.bluetooth.annotations.RequiresBluetoothConnectPermission; import android.content.AttributionSource; import android.content.Context; import android.os.IBinder; import android.os.RemoteException; import android.util.CloseGuard; import android.util.Log; import com.android.modules.utils.SynchronousResultReceiver; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.TimeoutException; /** * This class provides the public APIs to control the BAP Broadcast Source profile. Loading @@ -45,6 +57,148 @@ import java.util.concurrent.Executor; public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfile { private static final String TAG = "BluetoothLeBroadcast"; private static final boolean DBG = true; private static final boolean VDBG = false; private CloseGuard mCloseGuard; private final BluetoothAdapter mAdapter; private final AttributionSource mAttributionSource; private final BluetoothProfileConnector<IBluetoothLeAudio> mProfileConnector = new BluetoothProfileConnector(this, BluetoothProfile.LE_AUDIO_BROADCAST, "BluetoothLeAudioBroadcast", IBluetoothLeAudio.class.getName()) { @Override public IBluetoothLeAudio getServiceInterface(IBinder service) { return IBluetoothLeAudio.Stub.asInterface(service); } }; private final Map<Callback, Executor> mCallbackExecutorMap = new HashMap<>(); @SuppressLint("AndroidFrameworkBluetoothPermission") private final IBluetoothLeBroadcastCallback mCallback = new IBluetoothLeBroadcastCallback.Stub() { @Override public void onBroadcastStarted(int reason, int broadcastId) { for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry: mCallbackExecutorMap.entrySet()) { BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); Executor executor = callbackExecutorEntry.getValue(); executor.execute(() -> callback.onBroadcastStarted(reason, broadcastId)); } } @Override public void onBroadcastStartFailed(int reason) { for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry: mCallbackExecutorMap.entrySet()) { BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); Executor executor = callbackExecutorEntry.getValue(); executor.execute(() -> callback.onBroadcastStartFailed(reason)); } } @Override public void onBroadcastStopped(int reason, int broadcastId) { for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry: mCallbackExecutorMap.entrySet()) { BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); Executor executor = callbackExecutorEntry.getValue(); executor.execute(() -> callback.onBroadcastStopped(reason, broadcastId)); } } @Override public void onBroadcastStopFailed(int reason) { for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry: mCallbackExecutorMap.entrySet()) { BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); Executor executor = callbackExecutorEntry.getValue(); executor.execute(() -> callback.onBroadcastStopFailed(reason)); } } @Override public void onPlaybackStarted(int reason, int broadcastId) { for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry: mCallbackExecutorMap.entrySet()) { BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); Executor executor = callbackExecutorEntry.getValue(); executor.execute(() -> callback.onPlaybackStarted(reason, broadcastId)); } } @Override public void onPlaybackStopped(int reason, int broadcastId) { for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry: mCallbackExecutorMap.entrySet()) { BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); Executor executor = callbackExecutorEntry.getValue(); executor.execute(() -> callback.onPlaybackStopped(reason, broadcastId)); } } @Override public void onBroadcastUpdated(int reason, int broadcastId) { for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry: mCallbackExecutorMap.entrySet()) { BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); Executor executor = callbackExecutorEntry.getValue(); executor.execute(() -> callback.onBroadcastUpdated(reason, broadcastId)); } } @Override public void onBroadcastUpdateFailed(int reason, int broadcastId) { for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry: mCallbackExecutorMap.entrySet()) { BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); Executor executor = callbackExecutorEntry.getValue(); executor.execute(() -> callback.onBroadcastUpdateFailed(reason, broadcastId)); } } @Override public void onBroadcastMetadataChanged(int broadcastId, BluetoothLeBroadcastMetadata metadata) { for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry: mCallbackExecutorMap.entrySet()) { BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); Executor executor = callbackExecutorEntry.getValue(); executor.execute(() -> callback.onBroadcastMetadataChanged(broadcastId, metadata)); } } }; @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.registerLeBroadcastCallback(mCallback, mAttributionSource, recv); recv.awaitResultNoInterrupt(getSyncTimeout()) .getValue(null); } } catch (TimeoutException e) { Log.e(TAG, "onBluetoothServiceUp: Failed to register " + "Le Broadcaster callback", e); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } } } } }; /** * Interface for receiving events related to Broadcast Source Loading Loading @@ -171,7 +325,33 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi * @param listener listens for service callbacks across binder * @hide */ /*package*/ BluetoothLeBroadcast(Context context, BluetoothProfile.ServiceListener listener) {} /*package*/ BluetoothLeBroadcast(Context context, BluetoothProfile.ServiceListener listener) { mAdapter = BluetoothAdapter.getDefaultAdapter(); mAttributionSource = mAdapter.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"); } /** * @hide */ protected void finalize() { if (mCloseGuard != null) { mCloseGuard.warnIfOpen(); } close(); } /** * Not supported since LE Audio Broadcasts do not establish a connection. Loading Loading @@ -246,8 +426,36 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi if (callback == null) { throw new IllegalArgumentException("callback cannot be null"); } log("registerCallback"); throw new UnsupportedOperationException("Not Implemented"); if (!isEnabled()) { throw new IllegalStateException("service not enabled"); } 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.registerLeBroadcastCallback(mCallback, mAttributionSource, recv); recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null); } } catch (TimeoutException | IllegalStateException e) { Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); } 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); } } /** Loading @@ -271,8 +479,30 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi if (callback == null) { throw new IllegalArgumentException("callback cannot be null"); } log("unregisterCallback"); throw new UnsupportedOperationException("Not Implemented"); 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.unregisterLeBroadcastCallback(mCallback, mAttributionSource, recv); recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null); } } catch (TimeoutException | IllegalStateException e) { Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } } /** Loading Loading @@ -323,6 +553,17 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi public void startBroadcast(@NonNull BluetoothLeAudioContentMetadata contentMetadata, @Nullable byte[] broadcastCode) { if (DBG) log("startBroadcasting"); final IBluetoothLeAudio service = getService(); if (service == null) { Log.w(TAG, "Proxy not attached to service"); if (DBG) log(Log.getStackTraceString(new Throwable())); } else if (isEnabled()) { try { service.startBroadcast(contentMetadata, broadcastCode, mAttributionSource); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } } /** Loading @@ -346,7 +587,18 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi }) public void updateBroadcast(int broadcastId, @NonNull BluetoothLeAudioContentMetadata contentMetadata) { if (DBG) log("updateBroadcast"); final IBluetoothLeAudio service = getService(); if (service == null) { Log.w(TAG, "Proxy not attached to service"); if (DBG) log(Log.getStackTraceString(new Throwable())); } else if (isEnabled()) { try { service.updateBroadcast(broadcastId, contentMetadata, mAttributionSource); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } } /** Loading @@ -368,6 +620,17 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi }) public void stopBroadcast(int broadcastId) { if (DBG) log("disableBroadcastMode"); final IBluetoothLeAudio service = getService(); if (service == null) { Log.w(TAG, "Proxy not attached to service"); if (DBG) log(Log.getStackTraceString(new Throwable())); } else if (isEnabled()) { try { service.stopBroadcast(broadcastId, mAttributionSource); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } } /** Loading @@ -385,7 +648,23 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi android.Manifest.permission.BLUETOOTH_PRIVILEGED, }) public boolean isPlaying(int broadcastId) { return false; final IBluetoothLeAudio service = getService(); final boolean defaultValue = false; if (service == null) { Log.w(TAG, "Proxy not attached to service"); if (DBG) log(Log.getStackTraceString(new Throwable())); } else if (isEnabled()) { try { final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver(); service.isPlaying(broadcastId, mAttributionSource, recv); return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); } catch (TimeoutException e) { Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } return defaultValue; } /** Loading @@ -402,7 +681,24 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi android.Manifest.permission.BLUETOOTH_PRIVILEGED, }) public @NonNull List<BluetoothLeBroadcastMetadata> getAllBroadcastMetadata() { return Collections.emptyList(); final IBluetoothLeAudio service = getService(); final List<BluetoothLeBroadcastMetadata> defaultValue = Collections.emptyList(); if (service == null) { Log.w(TAG, "Proxy not attached to service"); if (DBG) log(Log.getStackTraceString(new Throwable())); } else if (isEnabled()) { try { final SynchronousResultReceiver<List<BluetoothLeBroadcastMetadata>> recv = new SynchronousResultReceiver(); service.getAllBroadcastMetadata(mAttributionSource, recv); return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); } catch (TimeoutException e) { Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } return defaultValue; } /** Loading @@ -413,7 +709,23 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi @SystemApi @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public int getMaximumNumberOfBroadcast() { return 1; final IBluetoothLeAudio service = getService(); final int defaultValue = 1; if (service == null) { Log.w(TAG, "Proxy not attached to service"); if (DBG) log(Log.getStackTraceString(new Throwable())); } else if (isEnabled()) { try { final SynchronousResultReceiver<Integer> recv = new SynchronousResultReceiver(); service.getMaximumNumberOfBroadcast(mAttributionSource, recv); return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); } catch (TimeoutException e) { Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } return defaultValue; } /** Loading @@ -421,7 +733,29 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi * @hide */ @Override public void close() throws Exception {} public void close() { if (VDBG) log("close()"); IBluetoothManager mgr = mAdapter.getBluetoothManager(); if (mgr != null) { try { mgr.unregisterStateChangeCallback(mBluetoothStateChangeCallback); } catch (RemoteException e) { Log.e(TAG, "", e); } } mProfileConnector.disconnect(); } private boolean isEnabled() { if (mAdapter.getState() == BluetoothAdapter.STATE_ON) return true; return false; } private IBluetoothLeAudio getService() { return mProfileConnector.getService(); } private static void log(String msg) { Log.d(TAG, msg); Loading
system/binder/Android.bp +1 −0 Original line number Diff line number Diff line Loading @@ -29,6 +29,7 @@ filegroup { "android/bluetooth/IBluetoothVolumeControl.aidl", "android/bluetooth/IBluetoothHidHost.aidl", "android/bluetooth/IBluetoothLeAudio.aidl", "android/bluetooth/IBluetoothLeBroadcastCallback.aidl", "android/bluetooth/IBluetoothManager.aidl", "android/bluetooth/IBluetoothManagerCallback.aidl", "android/bluetooth/IBluetoothMap.aidl", Loading