Merge "Synchronize group-related variables"
diff --git a/android/app/Android.bp b/android/app/Android.bp
index dc76c5a..8cbdac8 100644
--- a/android/app/Android.bp
+++ b/android/app/Android.bp
@@ -137,8 +137,8 @@
 genrule {
     name: "statslog-bluetooth-java-gen",
     tools: ["stats-log-api-gen"],
-    cmd: "$(location stats-log-api-gen) --java $(out) --module bluetooth"
-        + " --javaPackage com.android.bluetooth --javaClass BluetoothStatsLog"
-        + " --minApiLevel 32",
+    cmd: "$(location stats-log-api-gen) --java $(out) --module bluetooth" +
+        " --javaPackage com.android.bluetooth --javaClass BluetoothStatsLog" +
+        " --minApiLevel 33",
     out: ["com/android/bluetooth/BluetoothStatsLog.java"],
 }
diff --git a/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java b/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java
index 81b3911..12fdd62 100644
--- a/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java
+++ b/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java
@@ -1855,6 +1855,10 @@
         public void connect(BluetoothDevice device, AttributionSource source,
                 SynchronousResultReceiver receiver) {
             try {
+                Objects.requireNonNull(device, "device cannot be null");
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
                 LeAudioService service = getService(source);
                 boolean defaultValue = false;
                 if (service != null) {
@@ -1870,6 +1874,10 @@
         public void disconnect(BluetoothDevice device, AttributionSource source,
                 SynchronousResultReceiver receiver) {
             try {
+                Objects.requireNonNull(device, "device cannot be null");
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
                 LeAudioService service = getService(source);
                 boolean defaultValue = false;
                 if (service != null) {
@@ -1885,6 +1893,9 @@
         public void getConnectedDevices(AttributionSource source,
                 SynchronousResultReceiver receiver) {
             try {
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
                 LeAudioService service = getService(source);
                 List<BluetoothDevice> defaultValue = new ArrayList<>(0);
                 if (service != null) {
@@ -1900,6 +1911,9 @@
         public void getConnectedGroupLeadDevice(int groupId, AttributionSource source,
                 SynchronousResultReceiver receiver) {
             try {
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
                 LeAudioService service = getService(source);
                 BluetoothDevice defaultValue = null;
                 if (service != null) {
@@ -1915,6 +1929,9 @@
         public void getDevicesMatchingConnectionStates(int[] states,
                 AttributionSource source, SynchronousResultReceiver receiver) {
             try {
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
                 LeAudioService service = getService(source);
                 List<BluetoothDevice> defaultValue = new ArrayList<>(0);
                 if (service != null) {
@@ -1930,6 +1947,10 @@
         public void getConnectionState(BluetoothDevice device, AttributionSource source,
                 SynchronousResultReceiver receiver) {
             try {
+                Objects.requireNonNull(device, "device cannot be null");
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
                 LeAudioService service = getService(source);
                 int defaultValue = BluetoothProfile.STATE_DISCONNECTED;
                 if (service != null) {
@@ -1945,6 +1966,10 @@
         public void setActiveDevice(BluetoothDevice device, AttributionSource source,
                 SynchronousResultReceiver receiver) {
             try {
+                Objects.requireNonNull(device, "device cannot be null");
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
                 LeAudioService service = getService(source);
                 boolean defaultValue = false;
                 if (service != null) {
@@ -1959,6 +1984,9 @@
         @Override
         public void getActiveDevices(AttributionSource source, SynchronousResultReceiver receiver) {
             try {
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
                 LeAudioService service = getService(source);
                 List<BluetoothDevice> defaultValue = new ArrayList<>();
                 if (service != null) {
@@ -1974,6 +2002,10 @@
         public void getAudioLocation(BluetoothDevice device, AttributionSource source,
                 SynchronousResultReceiver receiver) {
             try {
+                Objects.requireNonNull(device, "device cannot be null");
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
                 LeAudioService service = getService(source);
                 int defaultValue = BluetoothLeAudio.AUDIO_LOCATION_INVALID;
                 if (service != null) {
@@ -1988,6 +2020,10 @@
         @Override
         public void setConnectionPolicy(BluetoothDevice device, int connectionPolicy,
                 AttributionSource source, SynchronousResultReceiver receiver) {
+            Objects.requireNonNull(device, "device cannot be null");
+            Objects.requireNonNull(source, "source cannot be null");
+            Objects.requireNonNull(receiver, "receiver cannot be null");
+
             try {
                 LeAudioService service = getService(source);
                 boolean defaultValue = false;
@@ -2004,11 +2040,17 @@
         public void getConnectionPolicy(BluetoothDevice device, AttributionSource source,
                 SynchronousResultReceiver receiver) {
             try {
+                Objects.requireNonNull(device, "device cannot be null");
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
                 LeAudioService service = getService(source);
                 int defaultValue = BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
-                if (service != null) {
-                    defaultValue = service.getConnectionPolicy(device);
+                if (service == null) {
+                    throw new IllegalStateException("service is null");
                 }
+                enforceBluetoothPrivilegedPermission(service);
+                defaultValue = service.getConnectionPolicy(device);
                 receiver.send(defaultValue);
             } catch (RuntimeException e) {
                 receiver.propagateException(e);
@@ -2019,12 +2061,17 @@
         public void getGroupId(BluetoothDevice device, AttributionSource source,
                 SynchronousResultReceiver receiver) {
             try {
+                Objects.requireNonNull(device, "device cannot be null");
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
                 LeAudioService service = getService(source);
                 int defaultValue = LE_AUDIO_GROUP_ID_INVALID;
-                if (service != null) {
-
-                    defaultValue = service.getGroupId(device);
+                if (service == null) {
+                    throw new IllegalStateException("service is null");
                 }
+                enforceBluetoothPrivilegedPermission(service);
+                defaultValue = service.getGroupId(device);
                 receiver.send(defaultValue);
             } catch (RuntimeException e) {
                 receiver.propagateException(e);
@@ -2035,11 +2082,17 @@
         public void groupAddNode(int group_id, BluetoothDevice device,
                 AttributionSource source, SynchronousResultReceiver receiver) {
             try {
+                Objects.requireNonNull(device, "device cannot be null");
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
                 LeAudioService service = getService(source);
                 boolean defaultValue = false;
-                if (service != null) {
-                    defaultValue = service.groupAddNode(group_id, device);
+                if (service == null) {
+                    throw new IllegalStateException("service is null");
                 }
+                enforceBluetoothPrivilegedPermission(service);
+                defaultValue = service.groupAddNode(group_id, device);
                 receiver.send(defaultValue);
             } catch (RuntimeException e) {
                 receiver.propagateException(e);
@@ -2050,11 +2103,17 @@
         public void groupRemoveNode(int groupId, BluetoothDevice device,
                 AttributionSource source, SynchronousResultReceiver receiver) {
             try {
+                Objects.requireNonNull(device, "device cannot be null");
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
                 LeAudioService service = getService(source);
                 boolean defaultValue = false;
-                if (service != null) {
-                    defaultValue = service.groupRemoveNode(groupId, device);
+                if (service == null) {
+                    throw new IllegalStateException("service is null");
                 }
+                enforceBluetoothPrivilegedPermission(service);
+                defaultValue = service.groupRemoveNode(groupId, device);
                 receiver.send(defaultValue);
             } catch (RuntimeException e) {
                 receiver.propagateException(e);
@@ -2065,10 +2124,15 @@
         public void setVolume(int volume, AttributionSource source,
                 SynchronousResultReceiver receiver) {
             try {
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
                 LeAudioService service = getService(source);
-                if (service != null) {
-                    service.setVolume(volume);
+                if (service == null) {
+                    throw new IllegalStateException("service is null");
                 }
+                enforceBluetoothPrivilegedPermission(service);
+                service.setVolume(volume);
                 receiver.send(null);
             } catch (RuntimeException e) {
                 receiver.propagateException(e);
@@ -2078,14 +2142,17 @@
         @Override
         public void registerCallback(IBluetoothLeAudioCallback callback,
                 AttributionSource source, SynchronousResultReceiver receiver) {
-            LeAudioService service = getService(source);
-            if ((service == null) || (service.mLeAudioCallbacks == null)) {
-                receiver.propagateException(new IllegalStateException("Service is unavailable"));
-                return;
-            }
-
-            enforceBluetoothPrivilegedPermission(service);
             try {
+                Objects.requireNonNull(callback, "callback cannot be null");
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
+                LeAudioService service = getService(source);
+                if ((service == null) || (service.mLeAudioCallbacks == null)) {
+                    throw new IllegalStateException("Service is unavailable: " + service);
+                }
+
+                enforceBluetoothPrivilegedPermission(service);
                 service.mLeAudioCallbacks.register(callback);
                 receiver.send(null);
             } catch (RuntimeException e) {
@@ -2096,14 +2163,18 @@
         @Override
         public void unregisterCallback(IBluetoothLeAudioCallback callback,
                 AttributionSource source, SynchronousResultReceiver receiver) {
-            LeAudioService service = getService(source);
-            if ((service == null) || (service.mLeAudioCallbacks == null)) {
-                receiver.propagateException(new IllegalStateException("Service is unavailable"));
-                return;
-            }
-
-            enforceBluetoothPrivilegedPermission(service);
             try {
+                Objects.requireNonNull(callback, "callback cannot be null");
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
+                LeAudioService service = getService(source);
+                if ((service == null) || (service.mLeAudioCallbacks == null)) {
+                    throw new IllegalStateException("Service is unavailable");
+                }
+
+                enforceBluetoothPrivilegedPermission(service);
+
                 service.mLeAudioCallbacks.unregister(callback);
                 receiver.send(null);
             } catch (RuntimeException e) {
@@ -2114,14 +2185,18 @@
         @Override
         public void registerLeBroadcastCallback(IBluetoothLeBroadcastCallback callback,
                 AttributionSource source, SynchronousResultReceiver receiver) {
-            LeAudioService service = getService(source);
-            if ((service == null) || (service.mBroadcastCallbacks == null)) {
-                receiver.propagateException(new IllegalStateException("Service is unavailable"));
-                return;
-            }
-
-            enforceBluetoothPrivilegedPermission(service);
             try {
+                Objects.requireNonNull(callback, "callback cannot be null");
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
+                LeAudioService service = getService(source);
+                if ((service == null) || (service.mBroadcastCallbacks == null)) {
+                    throw new IllegalStateException("Service is unavailable");
+                }
+
+                enforceBluetoothPrivilegedPermission(service);
+
                 service.mBroadcastCallbacks.register(callback);
                 receiver.send(null);
             } catch (RuntimeException e) {
@@ -2132,14 +2207,17 @@
         @Override
         public void unregisterLeBroadcastCallback(IBluetoothLeBroadcastCallback callback,
                 AttributionSource source, SynchronousResultReceiver receiver) {
-            LeAudioService service = getService(source);
-            if ((service == null) || (service.mBroadcastCallbacks == null)) {
-                receiver.propagateException(new IllegalStateException("Service is unavailable"));
-                return;
-            }
-
-            enforceBluetoothPrivilegedPermission(service);
             try {
+                LeAudioService service = getService(source);
+                if ((service == null) || (service.mBroadcastCallbacks == null)) {
+                    throw new IllegalStateException("Service is unavailable");
+                }
+
+                enforceBluetoothPrivilegedPermission(service);
+                Objects.requireNonNull(callback, "callback cannot be null");
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
                 service.mBroadcastCallbacks.unregister(callback);
                 receiver.send(null);
             } catch (RuntimeException e) {
diff --git a/android/app/src/com/android/bluetooth/vc/VolumeControlService.java b/android/app/src/com/android/bluetooth/vc/VolumeControlService.java
index 87a415e..caf7d4a 100644
--- a/android/app/src/com/android/bluetooth/vc/VolumeControlService.java
+++ b/android/app/src/com/android/bluetooth/vc/VolumeControlService.java
@@ -412,6 +412,11 @@
      */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     public boolean okToConnect(BluetoothDevice device) {
+        /* Make sure device is valid */
+        if (device == null) {
+            Log.e(TAG, "okToConnect: Invalid device");
+            return false;
+        }
         // Check if this is an incoming connection in Quiet mode.
         if (mAdapterService.isQuietModeEnabled()) {
             Log.e(TAG, "okToConnect: cannot connect to " + device + " : quiet mode enabled");
@@ -921,6 +926,10 @@
         public void connect(BluetoothDevice device, AttributionSource source,
                 SynchronousResultReceiver receiver) {
             try {
+                Objects.requireNonNull(device, "device cannot be null");
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
                 VolumeControlService service = getService(source);
                 boolean defaultValue = false;
                 if (service != null) {
@@ -936,6 +945,10 @@
         public void disconnect(BluetoothDevice device, AttributionSource source,
                 SynchronousResultReceiver receiver) {
             try {
+                Objects.requireNonNull(device, "device cannot be null");
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
                 VolumeControlService service = getService(source);
                 boolean defaultValue = false;
                 if (service != null) {
@@ -951,6 +964,9 @@
         public void getConnectedDevices(AttributionSource source,
                 SynchronousResultReceiver receiver) {
             try {
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
                 VolumeControlService service = getService(source);
                 List<BluetoothDevice> defaultValue = new ArrayList<>();
                 if (service != null) {
@@ -967,6 +983,9 @@
         public void getDevicesMatchingConnectionStates(int[] states,
                 AttributionSource source, SynchronousResultReceiver receiver) {
             try {
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
                 VolumeControlService service = getService(source);
                 List<BluetoothDevice> defaultValue = new ArrayList<>();
                 if (service != null) {
@@ -982,6 +1001,10 @@
         public void getConnectionState(BluetoothDevice device, AttributionSource source,
                 SynchronousResultReceiver receiver) {
             try {
+                Objects.requireNonNull(device, "device cannot be null");
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
                 VolumeControlService service = getService(source);
                 int defaultValue = BluetoothProfile.STATE_DISCONNECTED;
                 if (service != null) {
@@ -997,6 +1020,10 @@
         public void setConnectionPolicy(BluetoothDevice device, int connectionPolicy,
                 AttributionSource source, SynchronousResultReceiver receiver) {
             try {
+                Objects.requireNonNull(device, "device cannot be null");
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
                 VolumeControlService service = getService(source);
                 boolean defaultValue = false;
                 if (service != null) {
@@ -1012,6 +1039,10 @@
         public void getConnectionPolicy(BluetoothDevice device, AttributionSource source,
                 SynchronousResultReceiver receiver) {
             try {
+                Objects.requireNonNull(device, "device cannot be null");
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
                 VolumeControlService service = getService(source);
                 int defaultValue = BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
                 if (service != null) {
@@ -1027,6 +1058,10 @@
         public void isVolumeOffsetAvailable(BluetoothDevice device,
                 AttributionSource source, SynchronousResultReceiver receiver) {
             try {
+                Objects.requireNonNull(device, "device cannot be null");
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
                 boolean defaultValue = false;
                 VolumeControlService service = getService(source);
                 if (service != null) {
@@ -1042,6 +1077,10 @@
         public void setVolumeOffset(BluetoothDevice device, int volumeOffset,
                 AttributionSource source, SynchronousResultReceiver receiver) {
             try {
+                Objects.requireNonNull(device, "device cannot be null");
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
                 VolumeControlService service = getService(source);
                 if (service != null) {
                     service.setVolumeOffset(device, volumeOffset);
@@ -1056,6 +1095,9 @@
         public void setVolumeGroup(int groupId, int volume, AttributionSource source,
                 SynchronousResultReceiver receiver) {
             try {
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
                 VolumeControlService service = getService(source);
                 if (service != null) {
                     service.setVolumeGroup(groupId, volume);
@@ -1069,38 +1111,44 @@
         @Override
         public void registerCallback(IBluetoothVolumeControlCallback callback,
                 AttributionSource source, SynchronousResultReceiver receiver) {
-            VolumeControlService service = getService(source);
-            if (service == null) {
-                throw new IllegalStateException("Service is unavailable");
-            }
-
-            enforceBluetoothPrivilegedPermission(service);
-
             try {
+                Objects.requireNonNull(callback, "callback cannot be null");
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
+                VolumeControlService service = getService(source);
+                if (service == null) {
+                    throw new IllegalStateException("Service is unavailable");
+                }
+
+                enforceBluetoothPrivilegedPermission(service);
+
                 service.mCallbacks.register(callback);
                 receiver.send(null);
             } catch (RuntimeException e) {
-                Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
-                throw new IllegalArgumentException(" Invalid callback");
+                receiver.propagateException(e);
             }
         }
 
         @Override
         public void unregisterCallback(IBluetoothVolumeControlCallback callback,
                 AttributionSource source, SynchronousResultReceiver receiver) {
-            VolumeControlService service = getService(source);
-            if (service == null) {
-                throw new IllegalStateException("Service is unavailable");
-            }
-
-            enforceBluetoothPrivilegedPermission(service);
-
             try {
+                Objects.requireNonNull(callback, "callback cannot be null");
+                Objects.requireNonNull(source, "source cannot be null");
+                Objects.requireNonNull(receiver, "receiver cannot be null");
+
+                VolumeControlService service = getService(source);
+                if (service == null) {
+                    throw new IllegalStateException("Service is unavailable");
+                }
+
+                enforceBluetoothPrivilegedPermission(service);
+
                 service.mCallbacks.unregister(callback);
                 receiver.send(null);
             } catch (RuntimeException e) {
-                Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
-                throw new IllegalArgumentException(" Invalid callback ");
+                receiver.propagateException(e);
             }
         }
     }
diff --git a/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java
index 5408207..cbf4074 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java
@@ -318,6 +318,10 @@
 
         // Send a connect request
         Assert.assertTrue("Connect expected to succeed", mService.connect(mDevice));
+
+        // Verify the connection state broadcast, and that we are in Connecting state
+        verifyConnectionStateIntent(TIMEOUT_MS, mDevice, BluetoothProfile.STATE_CONNECTING,
+                BluetoothProfile.STATE_DISCONNECTED);
     }
 
     /**
diff --git a/android/leaudio/app/src/main/AndroidManifest.xml b/android/leaudio/app/src/main/AndroidManifest.xml
index 996dcee..120142d 100644
--- a/android/leaudio/app/src/main/AndroidManifest.xml
+++ b/android/leaudio/app/src/main/AndroidManifest.xml
@@ -36,6 +36,11 @@
             android:excludeFromRecents="true"
             android:theme="@style/AppTheme.NoActionBar">
         </activity>
+        <activity
+            android:name=".BroadcastScanActivity"
+            android:excludeFromRecents="true"
+            android:theme="@style/AppTheme.NoActionBar">
+        </activity>
     </application>
 
 </manifest>
diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BluetoothProxy.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BluetoothProxy.java
index 0e628ab..0b931ad 100644
--- a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BluetoothProxy.java
+++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BluetoothProxy.java
@@ -26,40 +26,45 @@
 import android.os.ParcelUuid;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
 import androidx.core.util.Pair;
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
 
-import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 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.ListIterator;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.stream.Collectors;
 
-import com.android.bluetooth.leaudio.R;
-
 public class BluetoothProxy {
     private static BluetoothProxy INSTANCE;
     private final Application application;
     private final BluetoothAdapter bluetoothAdapter;
     private BluetoothLeAudio bluetoothLeAudio = null;
     private BluetoothLeBroadcast mBluetoothLeBroadcast = null;
+    private BluetoothLeBroadcastAssistant mBluetoothLeBroadcastAssistant = null;
+    private Set<BluetoothDevice> mBroadcastScanOnBehalfDevices = new HashSet<>();
     private BluetoothCsipSetCoordinator bluetoothCsis = null;
     private BluetoothVolumeControl bluetoothVolumeControl = null;
     private BluetoothHapClient bluetoothHapClient = null;
     private BluetoothProfile.ServiceListener profileListener = null;
     private BluetoothHapClient.Callback hapCallback = null;
+    private OnBassEventListener mBassEventListener;
+    private OnLocalBroadcastEventListener mLocalBroadcastEventListener;
     private final IntentFilter adapterIntentFilter;
+    private final IntentFilter bassIntentFilter;
     private IntentFilter intentFilter;
     private final ExecutorService mExecutor;
 
@@ -290,6 +295,9 @@
                     }
 
                     mBroadcastAddedMutableLive.postValue(broadcastId);
+                    if (mLocalBroadcastEventListener != null) {
+                        mLocalBroadcastEventListener.onBroadcastStarted(broadcastId);
+                    }
                 }
 
                 @Override
@@ -301,6 +309,9 @@
                 @Override
                 public void onBroadcastStopped(int reason, int broadcastId) {
                     mBroadcastRemovedMutableLive.postValue(new Pair<>(reason, broadcastId));
+                    if (mLocalBroadcastEventListener != null) {
+                        mLocalBroadcastEventListener.onBroadcastStopped(broadcastId);
+                    }
                 }
 
                 @Override
@@ -323,6 +334,9 @@
                 public void onBroadcastUpdated(int reason, int broadcastId) {
                     mBroadcastStatusMutableLive.postValue("Broadcast " + broadcastId
                             + "has been updated due to reason: " + reason);
+                    if (mLocalBroadcastEventListener != null) {
+                        mLocalBroadcastEventListener.onBroadcastUpdated(broadcastId);
+                    }
                 }
 
                 @Override
@@ -335,9 +349,147 @@
                 public void onBroadcastMetadataChanged(int broadcastId,
                         BluetoothLeBroadcastMetadata metadata) {
                     mBroadcastUpdateMutableLive.postValue(metadata);
+                    if (mLocalBroadcastEventListener != null) {
+                        mLocalBroadcastEventListener.onBroadcastMetadataChanged(
+                                broadcastId, metadata);
+                    }
                 }
             };
 
+    // TODO: Add behaviors in empty methods if necessary.
+    private final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
+            new BluetoothLeBroadcastAssistant.Callback() {
+        @Override
+        public void onSearchStarted(int reason) {}
+
+        @Override
+        public void onSearchStartFailed(int reason) {}
+
+        @Override
+        public void onSearchStopped(int reason) {}
+
+        @Override
+        public void onSearchStopFailed(int reason) {}
+
+        @Override
+        public void onSourceFound(BluetoothLeBroadcastMetadata source) {
+            if (mBassEventListener != null) {
+                mBassEventListener.onSourceFound(source);
+            }
+        }
+
+        @Override
+        public void onSourceAdded(BluetoothDevice sink, int sourceId, int reason) {}
+
+        @Override
+        public void onSourceAddFailed(BluetoothDevice sink,
+                BluetoothLeBroadcastMetadata source, int reason) {}
+
+        @Override
+        public void onSourceModified(BluetoothDevice sink, int sourceId, int reason) {}
+
+        @Override
+        public void onSourceModifyFailed(BluetoothDevice sink, int sourceId, int reason) {}
+
+        @Override
+        public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {}
+
+        @Override
+        public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) {}
+
+        @Override
+        public void onReceiveStateChanged(BluetoothDevice sink, int sourceId,
+                BluetoothLeBroadcastReceiveState state) {
+            if (allLeAudioDevicesMutable.getValue() != null) {
+                Optional<LeAudioDeviceStateWrapper> valid_device_opt = allLeAudioDevicesMutable
+                        .getValue().stream()
+                        .filter(stateWrapper -> stateWrapper.device.getAddress().equals(
+                                sink.getAddress()))
+                        .findAny();
+
+                if (!valid_device_opt.isPresent())
+                    return;
+
+                LeAudioDeviceStateWrapper valid_device = valid_device_opt.get();
+                LeAudioDeviceStateWrapper.BassData svc_data = valid_device.bassData;
+
+                // TODO: Is the receiver_id same with BluetoothLeBroadcastReceiveState.getSourceId()?
+                //       If not, find getSourceId() usages and fix the issues.
+//                rstate.receiver_id = intent.getIntExtra(
+//                        BluetoothBroadcastAudioScan.EXTRA_BASS_RECEIVER_ID, -1);
+                /**
+                 * From "Introducing-Bluetooth-LE-Audio-book" 8.6.3.1:
+                 *
+                 * The Source_ID is an Acceptor generated number which is used to identify a
+                 * specific set of
+                 * broadcast device and BIG information. It is local to an Acceptor and used as a
+                 * reference for
+                 * a Broadcast Assistant. In the case of a Coordinated Set of Acceptors, such as
+                 * a left and right
+                 * earbud, the Source_IDs are not related and may be different, even if both are
+                 * receiving the
+                 * same BIS, as each Acceptor independently creates their own Source ID values
+                 */
+
+                /**
+                 * From BluetoothBroadcastAudioScan.EXTRA_BASS_RECEIVER_ID:
+                 *
+                 * Broadcast receiver's endpoint identifier.
+                 */
+
+                HashMap<Integer, BluetoothLeBroadcastReceiveState> states =
+                        svc_data.receiverStatesMutable.getValue();
+                if (states == null)
+                    states = new HashMap<>();
+                states.put(state.getSourceId(), state);
+
+                // Use SetValue instead of PostValue() since we want to make it
+                // synchronous due to getValue() we do here as well
+                // Otherwise we could miss the update and store only the last
+                // receiver ID
+                svc_data.receiverStatesMutable.setValue(states);
+            }
+        }
+    };
+
+    private final BroadcastReceiver bassIntentReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action.equals(BluetoothLeBroadcastAssistant.ACTION_CONNECTION_STATE_CHANGED)) {
+                final BluetoothDevice device = intent.getParcelableExtra(
+                        BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class);
+
+                if (allLeAudioDevicesMutable.getValue() != null) {
+                    if (device != null) {
+                        Optional<LeAudioDeviceStateWrapper> valid_device_opt =
+                                allLeAudioDevicesMutable
+                                        .getValue().stream()
+                                        .filter(state -> state.device.getAddress().equals(
+                                                device.getAddress()))
+                                        .findAny();
+
+                        if (valid_device_opt.isPresent()) {
+                            LeAudioDeviceStateWrapper valid_device = valid_device_opt.get();
+                            LeAudioDeviceStateWrapper.BassData svc_data = valid_device.bassData;
+
+                            final int toState = intent
+                                    .getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
+                            if (toState == BluetoothProfile.STATE_CONNECTED
+                                    || toState == BluetoothProfile.STATE_DISCONNECTED)
+                                svc_data.isConnectedMutable.postValue(
+                                        toState == BluetoothProfile.STATE_CONNECTED);
+                        }
+                    }
+                }
+            }
+            // TODO: Remove this if unnecessary.
+//          case BluetoothBroadcastAudioScan.ACTION_BASS_BROADCAST_ANNONCEMENT_AVAILABLE:
+//              // FIXME: Never happen since there is no valid device with this intent
+//              break;
+        }
+    };
+
     private BluetoothProxy(Application application) {
         this.application = application;
         bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
@@ -360,6 +512,10 @@
         adapterIntentFilter = new IntentFilter();
         adapterIntentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
         application.registerReceiver(adapterIntentReceiver, adapterIntentFilter);
+
+        bassIntentFilter = new IntentFilter();
+        bassIntentFilter.addAction(BluetoothLeBroadcastAssistant.ACTION_CONNECTION_STATE_CHANGED);
+        application.registerReceiver(bassIntentReceiver, bassIntentFilter);
     }
 
     // Lazy constructing Singleton acquire method
@@ -512,6 +668,12 @@
                         mBluetoothLeBroadcast = (BluetoothLeBroadcast) bluetoothProfile;
                         mBluetoothLeBroadcast.registerCallback(mExecutor, mBroadcasterCallback);
                         break;
+                    case BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT:
+                        mBluetoothLeBroadcastAssistant = (BluetoothLeBroadcastAssistant)
+                                bluetoothProfile;
+                        mBluetoothLeBroadcastAssistant.registerCallback(mExecutor,
+                                mBroadcastAssistantCallback);
+                        break;
                 }
                 queryLeAudioDevices();
             }
@@ -525,6 +687,7 @@
         initVolumeControlProxy();
         initHapProxy();
         initLeAudioBroadcastProxy();
+        initBassProxy();
     }
 
     public void cleanupProfiles() {
@@ -535,6 +698,7 @@
         cleanupVolumeControlProxy();
         cleanupHapProxy();
         cleanupLeAudioBroadcastProxy();
+        cleanupBassProxy();
 
         profileListener = null;
     }
@@ -605,6 +769,19 @@
         }
     }
 
+    private void initBassProxy() {
+        bluetoothAdapter.getProfileProxy(this.application, profileListener,
+                BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
+    }
+
+    private void cleanupBassProxy() {
+        if (mBluetoothLeBroadcastAssistant != null) {
+            mBluetoothLeBroadcastAssistant.unregisterCallback(mBroadcastAssistantCallback);
+            bluetoothAdapter.closeProfileProxy(BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT,
+                    mBluetoothLeBroadcastAssistant);
+        }
+    }
+
     private Boolean checkForEnabledBluetooth() {
         Boolean current_state = bluetoothAdapter.isEnabled();
 
@@ -703,6 +880,21 @@
                     }
                 }
 
+                if (Arrays.asList(dev.getUuids() != null ? dev.getUuids() : new ParcelUuid[0])
+                        .contains(ParcelUuid.fromString(
+                                application.getString(R.string.svc_uuid_broadcast_audio)))) {
+                    if (state_wrapper.bassData == null)
+                        state_wrapper.bassData = new LeAudioDeviceStateWrapper.BassData();
+                    valid_device = true;
+
+                    if (mBluetoothLeBroadcastAssistant != null) {
+                        boolean is_connected = mBluetoothLeBroadcastAssistant
+                                .getConnectionState(dev) == BluetoothProfile.STATE_CONNECTED;
+                        state_wrapper.bassData.isConnectedMutable.setValue(is_connected);
+                    }
+                }
+
+
                 if (valid_device) validDevices.add(state_wrapper);
             }
 
@@ -822,6 +1014,88 @@
         }
     }
 
+    public void connectBass(BluetoothDevice device, boolean connect) {
+        if (mBluetoothLeBroadcastAssistant != null) {
+            if (connect) {
+                mBluetoothLeBroadcastAssistant.setConnectionPolicy(device,
+                        BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+            } else {
+                mBluetoothLeBroadcastAssistant.setConnectionPolicy(device,
+                        BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
+            }
+        }
+    }
+
+    public boolean scanForBroadcasts(@NonNull BluetoothDevice onBehalfDevice, boolean scan) {
+        if (mBluetoothLeBroadcastAssistant != null) {
+            // Note: startSearchingForSources() does not support scanning on behalf of
+            // a specific device - it only searches for all BASS connected devices.
+            // Therefore, we manage the list of the devices and start/stop the scanning.
+            if (scan) {
+                mBroadcastScanOnBehalfDevices.add(onBehalfDevice);
+                mBluetoothLeBroadcastAssistant.startSearchingForSources(new ArrayList<>());
+                if (mBassEventListener != null) {
+                    mBassEventListener.onScanningStateChanged(true);
+                }
+            } else {
+                mBroadcastScanOnBehalfDevices.remove(onBehalfDevice);
+                if (mBroadcastScanOnBehalfDevices.isEmpty()) {
+                    mBluetoothLeBroadcastAssistant.stopSearchingForSources();
+                    if (mBassEventListener != null) {
+                        mBassEventListener.onScanningStateChanged(false);
+                    }
+                }
+            }
+            return true;
+        }
+        return false;
+    }
+
+    public boolean stopBroadcastObserving() {
+        if (mBluetoothLeBroadcastAssistant != null) {
+            mBroadcastScanOnBehalfDevices.clear();
+            mBluetoothLeBroadcastAssistant.stopSearchingForSources();
+            if (mBassEventListener != null) {
+                mBassEventListener.onScanningStateChanged(false);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    // TODO: Uncomment this method if necessary
+//    public boolean getBroadcastReceiverState(BluetoothDevice device, int receiver_id) {
+//        if (mBluetoothLeBroadcastAssistant != null) {
+//            return mBluetoothLeBroadcastAssistant.getBroadcastReceiverState(device, receiver_id);
+//        }
+//        return false;
+//    }
+
+    public boolean addBroadcastSource(BluetoothDevice sink, BluetoothLeBroadcastMetadata sourceMetadata) {
+        if (mBluetoothLeBroadcastAssistant != null) {
+            mBluetoothLeBroadcastAssistant.addSource(sink, sourceMetadata, true /* isGroupOp */);
+            return true;
+        }
+        return false;
+    }
+
+    public boolean modifyBroadcastSource(BluetoothDevice sink, int sourceId,
+            BluetoothLeBroadcastMetadata metadata) {
+        if (mBluetoothLeBroadcastAssistant != null) {
+            mBluetoothLeBroadcastAssistant.modifySource(sink, sourceId, metadata);
+            return true;
+        }
+        return false;
+    }
+
+    public boolean removeBroadcastSource(BluetoothDevice sink, int sourceId) {
+        if (mBluetoothLeBroadcastAssistant != null) {
+            mBluetoothLeBroadcastAssistant.removeSource(sink, sourceId);
+            return true;
+        }
+        return false;
+    }
+
     public void setVolume(BluetoothDevice device, int volume) {
         if (bluetoothLeAudio != null) {
             bluetoothLeAudio.setVolume(volume);
@@ -843,7 +1117,7 @@
                         BluetoothProfile.CONNECTION_POLICY_ALLOWED);
             } else {
                 bluetoothHapClient.setConnectionPolicy(device,
-                        BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
+                        BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
             }
         }
     }
@@ -1056,7 +1330,7 @@
         return true;
     }
 
-    public List<BluetoothLeBroadcastMetadata> getAllBroadcastMetadata() {
+    public List<BluetoothLeBroadcastMetadata> getAllLocalBroadcasts() {
         if (mBluetoothLeBroadcast == null) return Collections.emptyList();
         return mBluetoothLeBroadcast.getAllBroadcastMetadata();
     }
@@ -1086,4 +1360,27 @@
         return (bluetoothAdapter
                 .isLeAudioBroadcastSourceSupported() == BluetoothStatusCodes.FEATURE_SUPPORTED);
     }
+
+    public void setOnBassEventListener(OnBassEventListener listener) {
+        mBassEventListener = listener;
+    }
+
+    // Used by BroadcastScanViewModel
+    public interface OnBassEventListener {
+        void onSourceFound(BluetoothLeBroadcastMetadata source);
+        void onScanningStateChanged(boolean isScanning);
+    }
+
+    public void setOnLocalBroadcastEventListener(OnLocalBroadcastEventListener listener) {
+        mLocalBroadcastEventListener = listener;
+    }
+
+    // Used by BroadcastScanViewModel
+    public interface OnLocalBroadcastEventListener {
+        // TODO: Add arguments in methods
+        void onBroadcastStarted(int broadcastId);
+        void onBroadcastStopped(int broadcastId);
+        void onBroadcastUpdated(int broadcastId);
+        void onBroadcastMetadataChanged(int broadcastId, BluetoothLeBroadcastMetadata metadata);
+    }
 }
diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastItemsAdapter.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastItemsAdapter.java
index 215d71b..e98e0a9 100644
--- a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastItemsAdapter.java
+++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastItemsAdapter.java
@@ -34,12 +34,10 @@
 import java.util.List;
 import java.util.Map;
 
-import com.android.bluetooth.leaudio.R;
-
 public class BroadcastItemsAdapter
         extends RecyclerView.Adapter<BroadcastItemsAdapter.BroadcastItemHolder> {
-    private List<BluetoothLeBroadcastMetadata> mBroadcastMetadata = new ArrayList<>();
-    private final Map<Integer /* broadcastId */, Boolean /* isPlaying */> mBroadcastPlayback =
+    private List<BluetoothLeBroadcastMetadata> mBroadcastMetadataList = new ArrayList<>();
+    private final Map<Integer /* broadcastId */, Boolean /* isPlaying */> mBroadcastPlaybackMap =
             new HashMap<>();
     private OnItemClickListener mOnItemClickListener;
 
@@ -57,8 +55,8 @@
 
     @Override
     public void onBindViewHolder(@NonNull BroadcastItemHolder holder, int position) {
-        Integer broadcastId = (Integer) mBroadcastPlayback.keySet().toArray()[position];
-        Boolean isPlaying = mBroadcastPlayback.get(broadcastId);
+        Integer broadcastId = (Integer) mBroadcastPlaybackMap.keySet().toArray()[position];
+        Boolean isPlaying = mBroadcastPlaybackMap.get(broadcastId);
 
         // Set card color based on the playback state
         if (isPlaying) {
@@ -76,33 +74,47 @@
 
     @Override
     public int getItemCount() {
-        return mBroadcastPlayback.size();
+        return mBroadcastPlaybackMap.size();
     }
 
     public void updateBroadcastsMetadata(List<BluetoothLeBroadcastMetadata> broadcasts) {
-        mBroadcastMetadata = broadcasts;
+        mBroadcastMetadataList = broadcasts;
         notifyDataSetChanged();
     }
 
     public void updateBroadcastMetadata(BluetoothLeBroadcastMetadata broadcast) {
-        mBroadcastMetadata.removeIf(bc -> (bc.getBroadcastId() == broadcast.getBroadcastId()));
-        mBroadcastMetadata.add(broadcast);
+        mBroadcastMetadataList.removeIf(bc -> (bc.getBroadcastId() == broadcast.getBroadcastId()));
+        mBroadcastMetadataList.add(broadcast);
         notifyDataSetChanged();
     }
 
     public void addBroadcasts(Integer broadcastId) {
-        if (!mBroadcastPlayback.containsKey(broadcastId))
-            mBroadcastPlayback.put(broadcastId, false);
+        if (!mBroadcastPlaybackMap.containsKey(broadcastId))
+            mBroadcastPlaybackMap.put(broadcastId, false);
     }
 
     public void removeBroadcast(Integer broadcastId) {
-        mBroadcastMetadata.removeIf(bc -> (broadcastId.equals(bc.getBroadcastId())));
-        mBroadcastPlayback.remove(broadcastId);
+        mBroadcastMetadataList.removeIf(bc -> (broadcastId.equals(bc.getBroadcastId())));
+        mBroadcastPlaybackMap.remove(broadcastId);
+        notifyDataSetChanged();
+    }
+
+    public void setBroadcasts(List<BluetoothLeBroadcastMetadata> broadcasts) {
+        mBroadcastMetadataList.clear();
+        mBroadcastMetadataList.addAll(broadcasts);
+
+        for (BluetoothLeBroadcastMetadata b : broadcasts) {
+            int broadcastId = b.getBroadcastId();
+            if (mBroadcastPlaybackMap.containsKey(broadcastId)) {
+                continue;
+            }
+            mBroadcastPlaybackMap.remove(broadcastId);
+        }
         notifyDataSetChanged();
     }
 
     public void updateBroadcastPlayback(Integer broadcastId, boolean isPlaying) {
-        mBroadcastPlayback.put(broadcastId, isPlaying);
+        mBroadcastPlaybackMap.put(broadcastId, isPlaying);
         notifyDataSetChanged();
     }
 
@@ -125,7 +137,7 @@
 
                 int position = getAdapterPosition();
                 if (position != RecyclerView.NO_POSITION) {
-                    Integer broadcastId = (Integer) mBroadcastPlayback.keySet().toArray()[position];
+                    Integer broadcastId = (Integer) mBroadcastPlaybackMap.keySet().toArray()[position];
                     listener.onItemClick(broadcastId);
                 }
             });
diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastScanActivity.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastScanActivity.java
new file mode 100644
index 0000000..17cf738
--- /dev/null
+++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastScanActivity.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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 com.android.bluetooth.leaudio;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.ViewModelProviders;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.Objects;
+
+
+public class BroadcastScanActivity extends AppCompatActivity {
+    // Integer key used for sending/receiving receiver ID.
+    public static final String EXTRA_BASS_RECEIVER_ID = "receiver_id";
+
+    private static final int BIS_ALL = 0xFFFFFFFF;
+
+    private BluetoothDevice device;
+    private BroadcastScanViewModel mViewModel;
+    private BroadcastItemsAdapter adapter;
+    private String mLocalBluetoothAddress;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.broadcast_scan_activity);
+
+        RecyclerView recyclerView = findViewById(R.id.broadcast_recycler_view);
+        recyclerView.setLayoutManager(new LinearLayoutManager(this));
+        recyclerView.setHasFixedSize(true);
+
+        mLocalBluetoothAddress = BluetoothAdapter.getDefaultAdapter().getAddress();
+
+        adapter = new BroadcastItemsAdapter();
+        adapter.setOnItemClickListener(broadcastId -> {
+            mViewModel.scanForBroadcasts(device, false);
+
+            BluetoothLeBroadcastMetadata broadcast = null;
+            for (BluetoothLeBroadcastMetadata b : mViewModel.getAllBroadcasts().getValue()) {
+                if (Objects.equals(b.getBroadcastId(), broadcastId)) {
+                    broadcast = b;
+                    break;
+                }
+            }
+
+            if (broadcast == null) {
+                Toast.makeText(recyclerView.getContext(), "Matching broadcast not found."
+                                + " broadcastId=" + broadcastId, Toast.LENGTH_SHORT).show();
+                return;
+            }
+
+            // TODO: Support selecting the subgroups instead of using all
+            mViewModel.addBroadcastSource(device, broadcast);
+            if (TextUtils.equals(mLocalBluetoothAddress, broadcast.getSourceDevice().getAddress())) {
+                Toast.makeText(recyclerView.getContext(), "Add local broadcast",
+                        Toast.LENGTH_SHORT).show();
+            } else {
+                Toast.makeText(recyclerView.getContext(), "Add remote broadcast",
+                        Toast.LENGTH_SHORT).show();
+            }
+        });
+        recyclerView.setAdapter(adapter);
+
+        mViewModel = ViewModelProviders.of(this).get(BroadcastScanViewModel.class);
+        mViewModel.getAllBroadcasts().observe(this, audioBroadcasts -> {
+            // Update Broadcast list in the adapter
+            adapter.setBroadcasts(audioBroadcasts);
+        });
+
+        Intent intent = getIntent();
+        device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+
+        mViewModel.scanForBroadcasts(device, false);
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+
+        if (mViewModel.getAllBroadcasts().getValue() != null)
+            adapter.setBroadcasts(mViewModel.getAllBroadcasts().getValue());
+
+        mViewModel.scanForBroadcasts(device, true);
+        mViewModel.refreshBroadcasts();
+    }
+
+    @Override
+    public void onBackPressed() {
+        Intent intent = new Intent(this, MainActivity.class);
+        intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+        startActivity(intent);
+        finish();
+    }
+}
diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastScanViewModel.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastScanViewModel.java
new file mode 100644
index 0000000..3e352e3
--- /dev/null
+++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastScanViewModel.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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 com.android.bluetooth.leaudio;
+
+import android.app.Application;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+public class BroadcastScanViewModel extends AndroidViewModel {
+    private final String TAG = "BroadcastScanViewModel";
+    boolean mIsActivityScanning = false;
+    BluetoothDevice mOnBehalfDevice;
+
+    // TODO: Remove these variables if they are unnecessary
+//    // AddBroadcast context
+//    BluetoothDevice mSetSrcTargetDevice;
+//    List<BluetoothBroadcastAudioScanBaseConfig> mSetSrcConfigs;
+//    boolean mSetSrcSyncPa;
+
+    BluetoothProxy mBluetooth;
+    Application mApplication;
+    private MutableLiveData<List<BluetoothLeBroadcastMetadata>> mAllBroadcasts = new MutableLiveData<>();
+    private HashMap<Integer, BluetoothLeBroadcastMetadata> mScanSessionBroadcasts = new HashMap<>();
+
+    private final BluetoothProxy.OnBassEventListener mBassEventListener =
+            new BluetoothProxy.OnBassEventListener() {
+        @Override
+        public void onSourceFound(BluetoothLeBroadcastMetadata source) {
+            mScanSessionBroadcasts.put(source.getBroadcastId(), source);
+        }
+
+        @Override
+        public void onScanningStateChanged(boolean isScanning) {
+            if (!isScanning) {
+                // Update the live broadcast list and clear scan session results
+                List<BluetoothLeBroadcastMetadata> localSessionBroadcasts =
+                        mBluetooth.getAllLocalBroadcasts();
+                ArrayList<BluetoothLeBroadcastMetadata> new_arr;
+                if (localSessionBroadcasts != null) {
+                    new_arr = new ArrayList<>(localSessionBroadcasts);
+                } else {
+                    new_arr = new ArrayList<>();
+                }
+                new_arr.addAll(mScanSessionBroadcasts.values());
+                mAllBroadcasts.postValue(new_arr);
+
+                // Continue as long as the main activity wants
+                if (mIsActivityScanning) {
+                    if (mOnBehalfDevice != null) {
+                        mBluetooth.scanForBroadcasts(mOnBehalfDevice, true);
+                    }
+                }
+            } else {
+                // FIXME: Clear won't work - it would auto-update the mutable and clear it as
+                // mutable uses reference to its values
+                mScanSessionBroadcasts = new HashMap<>();
+            }
+        }
+    };
+
+    private final BluetoothProxy.OnLocalBroadcastEventListener mLocalBroadcastEventListener =
+            new BluetoothProxy.OnLocalBroadcastEventListener() {
+        @Override
+        public void onBroadcastStarted(int broadcastId) {
+            // FIXME: We need a finer grain control over updating individual broadcast state
+            //        and not just the entire list of broadcasts
+            refreshBroadcasts();
+        }
+
+        @Override
+        public void onBroadcastStopped(int broadcastId) {
+            refreshBroadcasts();
+        }
+
+        @Override
+        public void onBroadcastUpdated(int broadcastId) {
+            refreshBroadcasts();
+        }
+
+        @Override
+        public void onBroadcastMetadataChanged(int broadcastId,
+                BluetoothLeBroadcastMetadata metadata) {
+            refreshBroadcasts();
+        }
+    };
+
+    public BroadcastScanViewModel(@NonNull Application application) {
+        super(application);
+        mApplication = application;
+        mBluetooth = BluetoothProxy.getBluetoothProxy(application);
+
+        mBluetooth.setOnBassEventListener(mBassEventListener);
+        mBluetooth.setOnLocalBroadcastEventListener(mLocalBroadcastEventListener);
+    }
+
+    @Override
+    public void onCleared() {
+        mBluetooth.setOnBassEventListener(null);
+        mBluetooth.setOnLocalBroadcastEventListener(null);
+    }
+
+    public LiveData<List<BluetoothLeBroadcastMetadata>> getAllBroadcasts() {
+        return mAllBroadcasts;
+    }
+
+    public void scanForBroadcasts(BluetoothDevice device, boolean scan) {
+        if (device == null) {
+            Log.e(TAG, "scanForBroadcasts: device is null. Ignoring.");
+            return;
+        }
+
+        mIsActivityScanning = scan;
+        mOnBehalfDevice = scan ? device : null;
+
+        // First update the live broadcast list
+        List<BluetoothLeBroadcastMetadata> localSessionBroadcasts =
+                mBluetooth.getAllLocalBroadcasts();
+        ArrayList<BluetoothLeBroadcastMetadata> new_arr;
+        if (localSessionBroadcasts != null) {
+            new_arr = new ArrayList<>(localSessionBroadcasts);
+        } else {
+            new_arr = new ArrayList<>();
+        }
+        new_arr.addAll(mScanSessionBroadcasts.values());
+        mAllBroadcasts.postValue(new_arr);
+
+        mBluetooth.scanForBroadcasts(device, scan);
+    }
+
+    public void addBroadcastSource(BluetoothDevice sink, BluetoothLeBroadcastMetadata sourceMetadata) {
+        mBluetooth.addBroadcastSource(sink, sourceMetadata);
+    }
+
+    public void refreshBroadcasts() {
+        // Concatenate local broadcasts to the scanned broadcast list
+        List<BluetoothLeBroadcastMetadata> localSessionBroadcasts =
+                mBluetooth.getAllLocalBroadcasts();
+        ArrayList<BluetoothLeBroadcastMetadata> new_arr = new ArrayList<>(
+                localSessionBroadcasts);
+        new_arr.addAll(mScanSessionBroadcasts.values());
+        mAllBroadcasts.postValue(new_arr);
+    }
+}
diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcasterViewModel.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcasterViewModel.java
index 94d4948..bf22c8e 100644
--- a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcasterViewModel.java
+++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcasterViewModel.java
@@ -56,11 +56,11 @@
     }
 
     public List<BluetoothLeBroadcastMetadata> getAllBroadcastMetadata() {
-        return mBluetooth.getAllBroadcastMetadata();
+        return mBluetooth.getAllLocalBroadcasts();
     }
 
     public int getBroadcastCount() {
-        return mBluetooth.getAllBroadcastMetadata().size();
+        return mBluetooth.getAllLocalBroadcasts().size();
     }
 
     public LiveData<BluetoothLeBroadcastMetadata> getBroadcastUpdateMetadataLive() {
diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioDeviceStateWrapper.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioDeviceStateWrapper.java
index e948856..3183d6b 100644
--- a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioDeviceStateWrapper.java
+++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioDeviceStateWrapper.java
@@ -19,6 +19,8 @@
 
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHapPresetInfo;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.bluetooth.BluetoothLeBroadcastSubgroup;
 
 import androidx.core.util.Pair;
 import androidx.lifecycle.MutableLiveData;
@@ -100,16 +102,10 @@
                 public Object viewsData = null;
         }
 
-        public static class ReceiverState {
-                int receiver_id;
-                int state;
-        }
-
         public static class BassData {
-                public MutableLiveData<Boolean> isValidBassDevice = new MutableLiveData<>();
                 public MutableLiveData<Boolean> isConnectedMutable = new MutableLiveData<>();
-                public MutableLiveData<HashMap<Integer, ReceiverState>> receiverStatesMutable =
-                                new MutableLiveData<>();
+                public MutableLiveData<HashMap<Integer, BluetoothLeBroadcastReceiveState>>
+                        receiverStatesMutable = new MutableLiveData<>();
 
                 public Object viewsData = null;
         }
diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioRecycleViewAdapter.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioRecycleViewAdapter.java
index 6c8384b..74ae408 100644
--- a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioRecycleViewAdapter.java
+++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioRecycleViewAdapter.java
@@ -17,17 +17,42 @@
 
 package com.android.bluetooth.leaudio;
 
+import static android.bluetooth.BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_BAD_CODE;
+import static android.bluetooth.BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_CODE_REQUIRED;
+import static android.bluetooth.BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING;
+import static android.bluetooth.BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_NOT_ENCRYPTED;
+import static android.bluetooth.BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_FAILED_TO_SYNCHRONIZE;
+import static android.bluetooth.BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_IDLE;
+import static android.bluetooth.BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED;
+import static android.bluetooth.BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCINFO_REQUEST;
+
+import static com.android.bluetooth.leaudio.BroadcastScanActivity.EXTRA_BASS_RECEIVER_ID;
+
 import android.animation.ObjectAnimator;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHapClient;
 import android.bluetooth.BluetoothLeAudio;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.content.Intent;
 import android.content.res.Configuration;
+import android.content.res.Resources;
 import android.os.ParcelUuid;
+import android.text.InputFilter;
 import android.text.InputType;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.*;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.NumberPicker;
+import android.widget.SeekBar;
+import android.widget.Spinner;
+import android.widget.Switch;
+import android.widget.TextView;
+import android.widget.Toast;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -39,14 +64,11 @@
 import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.BitSet;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 
-import com.android.bluetooth.leaudio.R;
-
 public class LeAudioRecycleViewAdapter
         extends RecyclerView.Adapter<LeAudioRecycleViewAdapter.ViewHolder> {
     private final AppCompatActivity parent;
@@ -99,6 +121,10 @@
                 holder.itemView.findViewById(R.id.hap_switch).setEnabled(
                         Arrays.asList(leAudioDeviceStateWrapper.device.getUuids()).contains(
                                 ParcelUuid.fromString(parent.getString(R.string.svc_uuid_has))));
+
+                holder.itemView.findViewById(R.id.bass_switch)
+                        .setEnabled(Arrays.asList(leAudioDeviceStateWrapper.device.getUuids())
+                                .contains(ParcelUuid.fromString(parent.getString(R.string.svc_uuid_broadcast_audio))));
             }
         }
 
@@ -108,6 +134,7 @@
         setVolumeControlUiStateObservers(holder, leAudioDeviceStateWrapper);
         setBassStateObservers(holder, leAudioDeviceStateWrapper);
         setHasStateObservers(holder, leAudioDeviceStateWrapper);
+        setBassUiStateObservers(holder, leAudioDeviceStateWrapper);
     }
 
     private void setLeAudioStateObservers(@NonNull ViewHolder holder,
@@ -626,6 +653,80 @@
         }
     }
 
+    private void setBassUiStateObservers(@NonNull ViewHolder holder, LeAudioDeviceStateWrapper leAudioDeviceStateWrapper) {
+        if (leAudioDeviceStateWrapper.bassData == null)
+            return;
+
+        ViewHolderBassPersistentData vData = (ViewHolderBassPersistentData)leAudioDeviceStateWrapper.bassData.viewsData;
+        if (vData == null)
+            return;
+
+        if (vData.selectedReceiverPositionMutable.hasObservers())
+            vData.selectedReceiverPositionMutable.removeObservers(this.parent);
+
+        vData.selectedReceiverPositionMutable.observe(this.parent, aInteger -> {
+            int receiver_id = Integer.parseInt(holder.bassReceiverIdSpinner.getItemAtPosition(aInteger).toString());
+            bassInteractionListener.onReceiverSelected(leAudioDeviceStateWrapper, receiver_id);
+
+            Map<Integer, BluetoothLeBroadcastReceiveState> states =
+                    leAudioDeviceStateWrapper.bassData.receiverStatesMutable.getValue();
+
+            if (states != null) {
+                if (states.containsKey(receiver_id)) {
+                    BluetoothLeBroadcastReceiveState state =
+                            states.get(holder.bassReceiverIdSpinner.getSelectedItem());
+                    final int paSyncState = state.getPaSyncState();
+                    final int bigEncryptionState = state.getBigEncryptionState();
+
+                    Resources res = this.parent.getResources();
+                    String stateName = null;
+
+                    // Set the icon
+                    if (paSyncState == PA_SYNC_STATE_IDLE) {
+                        holder.bassScanButton.setImageResource(R.drawable.ic_cast_black_24dp);
+                        stateName = res.getString(R.string.broadcast_state_idle);
+                    } else if (paSyncState == PA_SYNC_STATE_FAILED_TO_SYNCHRONIZE) {
+                        holder.bassScanButton.setImageResource(R.drawable.ic_warning_black_24dp);
+                        stateName = res.getString(R.string.broadcast_state_sync_pa_failed);
+                    } else if (paSyncState == PA_SYNC_STATE_SYNCHRONIZED) {
+                        switch (bigEncryptionState) {
+                            case BIG_ENCRYPTION_STATE_NOT_ENCRYPTED:
+                            case BIG_ENCRYPTION_STATE_DECRYPTING:
+                                holder.bassScanButton.setImageResource(
+                                        R.drawable.ic_bluetooth_searching_black_24dp);
+                                stateName = res.getString(R.string.broadcast_state_receiving_broadcast);
+                                break;
+                            case BIG_ENCRYPTION_STATE_CODE_REQUIRED:
+                                holder.bassScanButton.setImageResource(
+                                        R.drawable.ic_vpn_key_black_24dp);
+                                stateName = res.getString(R.string.broadcast_state_code_required);
+                                break;
+                            case BIG_ENCRYPTION_STATE_BAD_CODE:
+                                holder.bassScanButton.setImageResource(R.drawable.ic_warning_black_24dp);
+                                stateName = res.getString(R.string.broadcast_state_code_invalid);
+                                break;
+                        }
+                    }
+
+                    // TODO: Seems no appropriate state matching exists for RECEIVER_STATE_SYNCING
+                    //       and RECEIVER_STATE_SET_SOURCE_FAILED.
+                    //       What does "receiver source configuration has failed" mean?
+//                    else if (state == BluetoothBroadcastAudioScan.RECEIVER_STATE_SYNCING) {
+//                        holder.bassScanButton.setImageResource(R.drawable.ic_bluetooth_dots_black);
+//                        stateName = res.getString(R.string.broadcast_state_syncing);
+//                    }
+//                    } else if (state == BluetoothBroadcastAudioScan.RECEIVER_STATE_SET_SOURCE_FAILED) {
+//                        holder.bassScanButton.setImageResource(R.drawable.ic_refresh_black_24dp);
+//                        stateName = res.getString(R.string.broadcast_state_set_source_failed);
+//                    }
+
+                    holder.bassReceiverStateText.setText(
+                            stateName != null ? stateName : res.getString(R.string.unknown));
+                }
+            }
+        });
+    }
+
     @Override
     public long getItemId(int position) {
         return devices.get(position).device.getAddress().hashCode();
@@ -766,21 +867,6 @@
                 int output_id, String description);
     }
 
-    public interface OnBassInteractionListener {
-        void onConnectClick(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper);
-
-        void onDisconnectClick(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper);
-
-        void onReceiverSelected(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper,
-                int receiver_id);
-
-        void onStopSyncReq(BluetoothDevice device, int receiver_id);
-
-        void onRemoveSourceReq(BluetoothDevice device, int receiver_id);
-
-        void onStopObserving();
-    }
-
     public interface OnHapInteractionListener {
         void onConnectClick(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper);
 
@@ -801,6 +887,22 @@
         void onPreviousGroupPresetClicked(BluetoothDevice device);
     }
 
+    public interface OnBassInteractionListener {
+        void onConnectClick(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper);
+
+        void onDisconnectClick(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper);
+
+        void onReceiverSelected(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, int receiver_id);
+
+        void onBroadcastCodeEntered(BluetoothDevice device, int receiver_id, byte[] broadcast_code);
+
+        void onStopSyncReq(BluetoothDevice device, int receiver_id);
+
+        void onRemoveSourceReq(BluetoothDevice device, int receiver_id);
+
+        void onStopObserving();
+    }
+
     public class ViewHolder extends RecyclerView.ViewHolder {
         private final TextView deviceName;
 
@@ -880,6 +982,7 @@
         private Switch bassConnectionSwitch;
         private Spinner bassReceiverIdSpinner;
         private TextView bassReceiverStateText;
+        private ImageButton bassScanButton;
 
         public ViewHolder(@NonNull View itemView) {
             super(itemView);
@@ -888,6 +991,7 @@
             SetupLeAudioView(itemView);
             setupVcView(itemView);
             setupHapView(itemView);
+            setupBassView(itemView);
 
             // Notify viewmodel via parent's click listener
             itemView.setOnClickListener(view -> {
@@ -1606,6 +1710,147 @@
                 }
             });
         }
+
+        private void setupBassView(@NonNull View itemView) {
+            bassConnectionSwitch = itemView.findViewById(R.id.bass_switch);
+            bassConnectionSwitch.setActivated(true);
+            bassReceiverIdSpinner = itemView.findViewById(R.id.num_receiver_spinner);
+            bassReceiverStateText = itemView.findViewById(R.id.receiver_state_text);
+            bassScanButton = itemView.findViewById(R.id.broadcast_button);
+
+            bassConnectionSwitch.setOnCheckedChangeListener((compoundButton, b) -> {
+                if (!compoundButton.isActivated())
+                    return;
+
+                if (bassInteractionListener != null) {
+                    if (b)
+                        bassInteractionListener.onConnectClick(
+                                devices.get(ViewHolder.this.getAdapterPosition()));
+                    else
+                        bassInteractionListener.onDisconnectClick(
+                                devices.get(ViewHolder.this.getAdapterPosition()));
+                }
+            });
+
+            bassReceiverIdSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+                @Override
+                public void onItemSelected(AdapterView<?> adapterView, View view, int position, long l) {
+                    LeAudioDeviceStateWrapper device = devices.get(ViewHolder.this.getAdapterPosition());
+                    ((ViewHolderBassPersistentData) device.bassData.viewsData).selectedReceiverPositionMutable.setValue(position);
+                }
+
+                @Override
+                public void onNothingSelected(AdapterView<?> adapterView) {
+                    // Nothing to do here
+                }
+            });
+
+            bassScanButton.setOnClickListener(view -> {
+                Resources res =  view.getResources();
+
+                // TODO: Do not sync on the string value, but instead sync on the actual state value.
+                if (bassReceiverStateText.getText().equals(res.getString(R.string.broadcast_state_idle))) {
+                    AlertDialog.Builder alert = new AlertDialog.Builder(itemView.getContext());
+                    alert.setTitle("Scan and add a source or remove the currently set one.");
+
+                    BluetoothDevice device = devices.get(ViewHolder.this.getAdapterPosition()).device;
+                    int receiver_id = Integer.parseInt(bassReceiverIdSpinner.getSelectedItem().toString());
+
+                    alert.setPositiveButton("Scan", (dialog, whichButton) -> {
+                        // Scan for new announcements
+                        Intent intent = new Intent(this.itemView.getContext(), BroadcastScanActivity.class);
+                        intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+                        intent.putExtra(EXTRA_BASS_RECEIVER_ID, receiver_id);
+                        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, devices.get(ViewHolder.this.getAdapterPosition()).device);
+                        parent.startActivityForResult(intent, 666);
+                    });
+                    alert.setNeutralButton("Cancel", (dialog, whichButton) -> {
+                        // Do nothing
+                    });
+                    alert.setNegativeButton("Remove", (dialog, whichButton) -> {
+                        bassInteractionListener.onRemoveSourceReq(device, receiver_id);
+                    });
+                    alert.show();
+
+                } else if (bassReceiverStateText.getText().equals(res.getString(R.string.broadcast_state_code_required))) {
+                    AlertDialog.Builder alert = new AlertDialog.Builder(itemView.getContext());
+                    alert.setTitle("Please enter broadcast encryption code...");
+                    EditText pass_input_view = new EditText(itemView.getContext());
+                    pass_input_view.setFilters(new InputFilter[] { new InputFilter.LengthFilter(16) });
+                    alert.setView(pass_input_view);
+
+                    BluetoothDevice device = devices.get(ViewHolder.this.getAdapterPosition()).device;
+                    int receiver_id = Integer.parseInt(bassReceiverIdSpinner.getSelectedItem().toString());
+
+                    alert.setPositiveButton("Set", (dialog, whichButton) -> {
+                        byte[] code = pass_input_view.getText().toString().getBytes();
+                        bassInteractionListener.onBroadcastCodeEntered(device, receiver_id, code);
+                    });
+                    alert.setNegativeButton("Cancel", (dialog, whichButton) -> {
+                        // Do nothing
+                    });
+                    alert.show();
+
+                } else if (bassReceiverStateText.getText().equals(res.getString(R.string.broadcast_state_receiving_broadcast))) {
+                    AlertDialog.Builder alert = new AlertDialog.Builder(itemView.getContext());
+                    alert.setTitle("Stop the synchronization?");
+
+                    BluetoothDevice device = devices.get(ViewHolder.this.getAdapterPosition()).device;
+                    int receiver_id = Integer.parseInt(bassReceiverIdSpinner.getSelectedItem().toString());
+
+                    alert.setPositiveButton("Yes", (dialog, whichButton) -> {
+                        bassInteractionListener.onRemoveSourceReq(device, receiver_id);
+                    });
+                    // FIXME: To modify source we need the valid broadcaster_id context so we should start scan here again
+                    // alert.setNeutralButton("Modify", (dialog, whichButton) -> {
+                    //     // TODO: Open the scan dialog to get the broadcast_id
+                    //     // bassInteractionListener.onStopSyncReq(device, receiver_id, broadcast_id);
+                    // });
+                    alert.setNegativeButton("No", (dialog, whichButton) -> {
+                        // Do nothing
+                    });
+                    alert.show();
+
+                } else if (bassReceiverStateText.getText().equals(res.getString(R.string.broadcast_state_set_source_failed))
+                        || bassReceiverStateText.getText().equals(res.getString(R.string.broadcast_state_sync_pa_failed))) {
+                    AlertDialog.Builder alert = new AlertDialog.Builder(itemView.getContext());
+                    alert.setTitle("Retry broadcast audio announcement scan?");
+
+                    alert.setPositiveButton("Yes", (dialog, whichButton) -> {
+                        // Scan for new announcements
+                        Intent intent = new Intent(view.getContext(), BroadcastScanActivity.class);
+                        int receiver_id = Integer.parseInt(bassReceiverIdSpinner.getSelectedItem().toString());
+                        intent.putExtra(EXTRA_BASS_RECEIVER_ID, receiver_id);
+                        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, devices.get(ViewHolder.this.getAdapterPosition()).device);
+                        parent.startActivityForResult(intent, 666);
+                    });
+                    alert.setNegativeButton("No", (dialog, whichButton) -> {
+                        // Do nothing
+                    });
+                    alert.show();
+
+                } else if (bassReceiverStateText.getText().equals(res.getString(R.string.broadcast_state_syncing))) {
+                    AlertDialog.Builder alert = new AlertDialog.Builder(itemView.getContext());
+                    alert.setTitle("Stop the synchronization?");
+
+                    BluetoothDevice device = devices.get(ViewHolder.this.getAdapterPosition()).device;
+                    int receiver_id = Integer.parseInt(bassReceiverIdSpinner.getSelectedItem().toString());
+
+                    alert.setPositiveButton("Yes", (dialog, whichButton) -> {
+                        bassInteractionListener.onRemoveSourceReq(device, receiver_id);
+                    });
+                    // FIXME: To modify source we need the valid broadcaster_id context so we should start scan here again
+                    // alert.setNeutralButton("Modify", (dialog, whichButton) -> {
+                    //     // TODO: Open the scan dialog to get the broadcast_id
+                    //     // bassInteractionListener.onStopSyncReq(device, receiver_id, broadcast_id);
+                    // });
+                    alert.setNegativeButton("No", (dialog, whichButton) -> {
+                        // Do nothing
+                    });
+                    alert.show();
+                }
+            });
+        }
     }
 
     private class ViewHolderVcPersistentData {
diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioViewModel.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioViewModel.java
index 3d6bf34..ae31538 100644
--- a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioViewModel.java
+++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioViewModel.java
@@ -19,6 +19,7 @@
 
 import android.app.Application;
 import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
 
 import androidx.annotation.NonNull;
 import androidx.lifecycle.AndroidViewModel;
@@ -115,4 +116,37 @@
     public boolean isLeAudioBroadcastSourceSupported() {
         return bluetoothProxy.isLeAudioBroadcastSourceSupported();
     }
+
+    public void connectBass(BluetoothDevice sink, boolean connect) {
+        bluetoothProxy.connectBass(sink, connect);
+    }
+
+    public boolean stopBroadcastObserving() {
+        return bluetoothProxy.stopBroadcastObserving();
+    }
+
+    // TODO: Uncomment this method if necessary
+//    public boolean getBroadcastReceiverState(BluetoothDevice device, int receiver_id) {
+//        return bluetoothProxy.getBroadcastReceiverState(device, receiver_id);
+//    }
+
+    // TODO: Uncomment this method if necessary
+//    public boolean modifyBroadcastSource(BluetoothDevice device, int receiver_id, boolean sync_pa,
+//            List<BluetoothBroadcastAudioScanBaseConfig> configs) {
+//        return bluetoothProxy.modifyBroadcastSource(device, receiver_id, sync_pa, configs);
+//    }
+
+    public boolean removeBroadcastSource(BluetoothDevice sink, int receiver_id) {
+        // TODO: Find source ID from receiver_id. What is receiver_id?
+        int sourceId = 0;
+        return bluetoothProxy.removeBroadcastSource(sink, sourceId);
+    }
+
+    public boolean setBroadcastCode(BluetoothDevice sink, int receiver_id, byte[] bcast_code) {
+        // TODO: Find source ID from receiver_id. What is receiver_id?
+        // TODO: Build BluetoothLeBroadcastMetadata with the new bcast_code.
+        int sourceId = 0;
+        BluetoothLeBroadcastMetadata metadata = null;
+        return bluetoothProxy.modifyBroadcastSource(sink, sourceId, metadata);
+    }
 }
diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/MainActivity.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/MainActivity.java
index 3341007..fa7e8c4 100644
--- a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/MainActivity.java
+++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/MainActivity.java
@@ -136,6 +136,21 @@
         Intent intent = null;
 
         switch (item.getItemId()) {
+            case R.id.action_scan:
+                intent = new Intent(MainActivity.this, BroadcastScanActivity.class);
+                // TODO: Why does this pass no information?
+                //intent.putExtra(BluetoothBroadcastAudioScan.EXTRA_BASS_RECEIVER_ID, 0);
+
+                // TODO: Change BluetoothAdapter.getDefaultAdapter() usages into BluetoothManager#getAdapter().
+                BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter();
+
+                // What does this fake address mean?
+                byte[] address = {(byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff};
+                BluetoothDevice dev = mAdapter.getRemoteDevice(address);
+                intent.putExtra(BluetoothDevice.EXTRA_DEVICE, dev);
+                startActivity(intent);
+                return true;
+
             case R.id.action_broadcast:
                 if (leAudioViewModel.getBluetoothEnabledLive().getValue() == null
                         || !leAudioViewModel.getBluetoothEnabledLive().getValue()) {
@@ -168,6 +183,9 @@
                 Toast.makeText(MainActivity.this, message + "(" + resultCode + ")",
                         Toast.LENGTH_SHORT).show();
             }
+
+            // TODO: Depending on the resultCode we should either stop the sync or try the PAST
+            leAudioViewModel.stopBroadcastObserving();
         }
     }
 
@@ -548,12 +566,72 @@
                                     .show();
                     }
                 });
+
+        recyclerViewAdapter.setOnBassInteractionListener(
+                new LeAudioRecycleViewAdapter.OnBassInteractionListener() {
+                    @Override
+                    public void onConnectClick(
+                            LeAudioDeviceStateWrapper leAudioDeviceStateWrapper) {
+                        Toast.makeText(MainActivity.this,
+                                "Connecting BASS to " + leAudioDeviceStateWrapper.device.toString(),
+                                Toast.LENGTH_SHORT).show();
+                        leAudioViewModel.connectBass(leAudioDeviceStateWrapper.device, true);
+                    }
+
+                    @Override
+                    public void onDisconnectClick(
+                            LeAudioDeviceStateWrapper leAudioDeviceStateWrapper) {
+                        Toast.makeText(MainActivity.this,
+                                "Disconnecting BASS from "
+                                        + leAudioDeviceStateWrapper.device.toString(),
+                                Toast.LENGTH_SHORT).show();
+                        leAudioViewModel.connectBass(leAudioDeviceStateWrapper.device, false);
+                    }
+
+                    @Override
+                    public void onReceiverSelected(
+                            LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, int receiver_id) {
+                        // Do nothing here, the UI is updated elsewhere and we already have the
+                        // latest state value as well
+                    }
+
+                    @Override
+                    public void onBroadcastCodeEntered(BluetoothDevice device, int receiver_id,
+                            byte[] broadcast_code) {
+                        leAudioViewModel.setBroadcastCode(device, receiver_id, broadcast_code);
+                    }
+
+                    @Override
+                    public void onStopSyncReq(BluetoothDevice device, int receiver_id) {
+                        // TODO: When is onStopSyncReq called? and what does below code do?
+
+//                        List<BluetoothBroadcastAudioScanBaseConfig> configs = new ArrayList<>();
+//                        // JT@CC: How come you can call this with null metadata when the
+//                        // constructor has the @Nonull annotation for the param?
+//                        BluetoothBroadcastAudioScanBaseConfig stop_config =
+//                                new BluetoothBroadcastAudioScanBaseConfig(0, new byte[] {});
+//                        configs.add(stop_config);
+//
+//                        leAudioViewModel.modifyBroadcastSource(device, receiver_id, false, configs);
+                    }
+
+                    @Override
+                    public void onRemoveSourceReq(BluetoothDevice device, int receiver_id) {
+                        leAudioViewModel.removeBroadcastSource(device, receiver_id);
+                    }
+
+                    @Override
+                    public void onStopObserving() {
+                        leAudioViewModel.stopBroadcastObserving();
+                    }
+                });
     }
 
     private void cleanupViewsProfileUiEventListeners() {
         recyclerViewAdapter.setOnLeAudioInteractionListener(null);
         recyclerViewAdapter.setOnVolumeControlInteractionListener(null);
         recyclerViewAdapter.setOnHapInteractionListener(null);
+        recyclerViewAdapter.setOnBassInteractionListener(null);
     }
 
     // This sets the initial values and set up the observers
diff --git a/android/leaudio/app/src/main/res/layout/broadcast_scan_activity.xml b/android/leaudio/app/src/main/res/layout/broadcast_scan_activity.xml
new file mode 100644
index 0000000..0d66e0a
--- /dev/null
+++ b/android/leaudio/app/src/main/res/layout/broadcast_scan_activity.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".BroadcastScanActivity">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@+id/scan_status_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="8dp"
+            android:layout_marginTop="8dp"
+            android:text="Scanning for broadcasts..." />
+
+        <ProgressBar
+            android:id="@+id/progressBar"
+            style="?android:attr/progressBarStyleHorizontal"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:indeterminate="true"
+            android:indeterminateBehavior="cycle"
+            android:indeterminateOnly="true" />
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/broadcast_recycler_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            tools:listitem="@layout/broadcast_item" />
+    </LinearLayout>
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/android/leaudio/app/src/main/res/values/strings.xml b/android/leaudio/app/src/main/res/values/donottranslate_strings.xml
similarity index 91%
rename from android/leaudio/app/src/main/res/values/strings.xml
rename to android/leaudio/app/src/main/res/values/donottranslate_strings.xml
index 45d0add..0b7a060 100644
--- a/android/leaudio/app/src/main/res/values/strings.xml
+++ b/android/leaudio/app/src/main/res/values/donottranslate_strings.xml
@@ -79,6 +79,13 @@
     </string-array>
     <string name="group_locked">LOCKED</string>
     <string name="group_unlocked">UNLOCKED</string>
+    <string name="broadcast_state_idle">IDLE</string>
+    <string name="broadcast_state_set_source_failed">SET_SOURCE_FAILED</string>
+    <string name="broadcast_state_syncing">SYNCING</string>
+    <string name="broadcast_state_sync_pa_failed">SYNC_PA_FAILED</string>
+    <string name="broadcast_state_code_required">BROADCAST_CODE_REQUIRED</string>
+    <string name="broadcast_state_code_invalid">BROADCAST_CODE_INVALID</string>
+    <string name="broadcast_state_receiving_broadcast">RECEIVING_BROADCAST</string>
     <string-array name="tbs_call_states">
         <item>Incoming</item>
         <item>Dialing</item>
diff --git a/framework/java/android/bluetooth/BluetoothHapClient.java b/framework/java/android/bluetooth/BluetoothHapClient.java
index 5416d73..1d51670 100644
--- a/framework/java/android/bluetooth/BluetoothHapClient.java
+++ b/framework/java/android/bluetooth/BluetoothHapClient.java
@@ -644,6 +644,7 @@
     public boolean setConnectionPolicy(@NonNull BluetoothDevice device,
             @ConnectionPolicy int connectionPolicy) {
         if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
+        Objects.requireNonNull(device, "BluetoothDevice cannot be null");
         final IBluetoothHapClient service = getService();
         final boolean defaultValue = false;
         if (service == null) {
@@ -673,7 +674,8 @@
      * {@link #CONNECTION_POLICY_UNKNOWN}
      *
      * @param device Bluetooth device
-     * @return connection policy of the device
+     * @return connection policy of the device or {@link #CONNECTION_POLICY_FORBIDDEN} if device is
+     *         null
      * @hide
      */
     @SystemApi
diff --git a/service/java/com/android/server/bluetooth/BluetoothManagerService.java b/service/java/com/android/server/bluetooth/BluetoothManagerService.java
index bd6dfc9..1bae07d 100644
--- a/service/java/com/android/server/bluetooth/BluetoothManagerService.java
+++ b/service/java/com/android/server/bluetooth/BluetoothManagerService.java
@@ -1067,7 +1067,8 @@
     public boolean enableBle(AttributionSource attributionSource, IBinder token)
             throws RemoteException {
         final String packageName = attributionSource.getPackageName();
-        if (!checkBluetoothPermissions(attributionSource, "enableBle", false)) {
+        if (!checkBluetoothPermissions(attributionSource, "enableBle", false)
+                || isAirplaneModeOn()) {
             if (DBG) {
                 Slog.d(TAG, "enableBle(): bluetooth disallowed");
             }
diff --git a/system/bta/dm/bta_dm_act.cc b/system/bta/dm/bta_dm_act.cc
index ba90f8a..35c528a 100644
--- a/system/bta/dm/bta_dm_act.cc
+++ b/system/bta/dm/bta_dm_act.cc
@@ -1793,7 +1793,7 @@
       (tBTA_DM_SDP_RESULT*)osi_malloc(sizeof(tBTA_DM_SDP_RESULT));
 
   p_msg->hdr.event = BTA_DM_SDP_RESULT_EVT;
-  p_msg->sdp_result = static_cast<uint16_t>(sdp_status);
+  p_msg->sdp_result = sdp_status;
 
   bta_sys_sendmsg(p_msg);
 }
diff --git a/system/bta/dm/bta_dm_int.h b/system/bta/dm/bta_dm_int.h
index 637c22a..fc5b9bc 100644
--- a/system/bta/dm/bta_dm_int.h
+++ b/system/bta/dm/bta_dm_int.h
@@ -133,7 +133,7 @@
 /* data type for BTA_DM_SDP_RESULT_EVT */
 typedef struct {
   BT_HDR_RIGID hdr;
-  uint16_t sdp_result;
+  tSDP_RESULT sdp_result;
 } tBTA_DM_SDP_RESULT;
 
 typedef struct {
diff --git a/system/bta/le_audio/client.cc b/system/bta/le_audio/client.cc
index b4bf14d..fe0a4d0 100644
--- a/system/bta/le_audio/client.cc
+++ b/system/bta/le_audio/client.cc
@@ -299,24 +299,32 @@
       return;
     }
 
-    /* Releasement didn't finished in time */
-    if (group->GetTargetState() == AseState::BTA_LE_AUDIO_ASE_STATE_IDLE) {
-      CancelStreamingRequest();
-      LeAudioDevice* leAudioDevice = group->GetFirstActiveDevice();
-      LOG_ASSERT(leAudioDevice)
-          << __func__ << " Shouldn't be called without an active device.";
+    LOG_ERROR(
+        " State not achieved on time for group: group id %d, current state %s, "
+        "target state: %s",
+        group_id, ToString(group->GetState()).c_str(),
+        ToString(group->GetTargetState()).c_str());
+    group->SetTargetState(AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
 
-      do {
-        if (instance) instance->DisconnectDevice(leAudioDevice, true);
-        leAudioDevice = group->GetNextActiveDevice(leAudioDevice);
-      } while (leAudioDevice);
-
-      return;
+    /* There is an issue with a setting up stream or any other operation which
+     * are gatt operations. It means peer is not responsable. Lets close ACL
+     */
+    CancelStreamingRequest();
+    LeAudioDevice* leAudioDevice = group->GetFirstActiveDevice();
+    if (leAudioDevice == nullptr) {
+      LOG_ERROR(" Shouldn't be called without an active device.");
+      leAudioDevice = group->GetFirstDevice();
+      if (leAudioDevice == nullptr) {
+        LOG_ERROR(" Front device is null. Number of devices: %d",
+                  group->Size());
+        return;
+      }
     }
 
-    LOG(ERROR) << __func__ << ", State not achieved on time, releasing ases";
-
-    groupStateMachine_->StopStream(group);
+    do {
+      if (instance) instance->DisconnectDevice(leAudioDevice, true);
+      leAudioDevice = group->GetNextActiveDevice(leAudioDevice);
+    } while (leAudioDevice);
   }
 
   void UpdateContextAndLocations(LeAudioDeviceGroup* group,
@@ -3216,16 +3224,6 @@
         rxUnreceivedPackets, duplicatePackets);
   }
 
-  void CompleteUserConfiguration(LeAudioDeviceGroup* group) {
-    if (audio_sender_state_ == AudioState::RELEASING) {
-      audio_sender_state_ = AudioState::IDLE;
-    }
-
-    if (audio_receiver_state_ == AudioState::RELEASING) {
-      audio_receiver_state_ = AudioState::IDLE;
-    }
-  }
-
   void HandlePendingAvailableContexts(LeAudioDeviceGroup* group) {
     if (!group) return;
 
@@ -3272,28 +3270,20 @@
         SuspendAudio();
         break;
       case GroupStreamStatus::CONFIGURED_BY_USER:
-        CompleteUserConfiguration(group);
+        /* We are done with reconfiguration.
+         * Clean state and if Audio HAL is waiting, cancel the request
+         * so Audio HAL can Resume again.
+         */
+        CancelStreamingRequest();
+        HandlePendingAvailableContexts(group);
         break;
       case GroupStreamStatus::CONFIGURED_AUTONOMOUS:
         /* This state is notified only when
          * groups stays into CONFIGURED state after
-         * STREAMING. Peer device uses cache.
-         * */
-        stream_setup_end_timestamp_ = 0;
-        stream_setup_start_timestamp_ = 0;
-
-        /* Check if stream was stopped for reconfiguration */
-        if (group->IsPendingConfiguration()) {
-          SuspendedForReconfiguration();
-          if (!groupStateMachine_->ConfigureStream(group,
-                                                   current_context_type_)) {
-            // DO SOMETHING
-          }
-          return;
-        }
-        CancelStreamingRequest();
-        HandlePendingAvailableContexts(group);
-        break;
+         * STREAMING. Peer device uses cache. For the moment
+         * it is handled same as IDLE
+         */
+        FALLTHROUGH;
       case GroupStreamStatus::IDLE: {
         stream_setup_end_timestamp_ = 0;
         stream_setup_start_timestamp_ = 0;
diff --git a/system/bta/le_audio/devices.cc b/system/bta/le_audio/devices.cc
index c90db21..255ab80 100644
--- a/system/bta/le_audio/devices.cc
+++ b/system/bta/le_audio/devices.cc
@@ -60,7 +60,13 @@
 
 void LeAudioDeviceGroup::RemoveNode(
     const std::shared_ptr<LeAudioDevice>& leAudioDevice) {
+  /* Group information cleaning in the device. */
   leAudioDevice->group_id_ = bluetooth::groups::kGroupUnknown;
+  for (auto ase : leAudioDevice->ases_) {
+    ase.active = false;
+    ase.cis_conn_hdl = 0;
+  }
+
   leAudioDevices_.erase(
       std::remove_if(
           leAudioDevices_.begin(), leAudioDevices_.end(),
diff --git a/system/bta/le_audio/state_machine.cc b/system/bta/le_audio/state_machine.cc
index b897444..b27866e 100644
--- a/system/bta/le_audio/state_machine.cc
+++ b/system/bta/le_audio/state_machine.cc
@@ -1145,8 +1145,6 @@
       if (cis_id == le_audio::kInvalidCisId) {
         /* Get completive (to be bi-directional CIS) CIS ID for ASE */
         cis_id = leAudioDevice->GetMatchingBidirectionCisId(ase);
-        LOG_INFO(" Configure ase_id %d, cis_id %d, ase state %s", ase->id,
-                 cis_id, ToString(ase->state).c_str());
         if (cis_id == le_audio::kInvalidCisId) {
           /* Get next free CIS ID for group */
           cis_id = group->GetFirstFreeCisId();
@@ -1158,6 +1156,9 @@
         }
       }
 
+      LOG_INFO(" Configure ase_id %d, cis_id %d, ase state %s", ase->id, cis_id,
+               ToString(ase->state).c_str());
+
       ase->cis_id = cis_id;
 
       conf.ase_id = ase->id;
diff --git a/system/gd/rust/linux/client/src/command_handler.rs b/system/gd/rust/linux/client/src/command_handler.rs
index f2221cc..266dd5f 100644
--- a/system/gd/rust/linux/client/src/command_handler.rs
+++ b/system/gd/rust/linux/client/src/command_handler.rs
@@ -437,7 +437,7 @@
             return;
         }
 
-        enforce_arg_len(args, 2, "device <connect|disconnect|info> <address>", || {
+        enforce_arg_len(args, 2, "device <connect|disconnect|info|set-alias> <address>", || {
             match &args[0][0..] {
                 "connect" => {
                     let device = BluetoothDevice {
@@ -519,6 +519,31 @@
                         )
                     );
                 }
+                "set-alias" => {
+                    if args.len() < 3 {
+                        println!("usage: device set-alias <address> <new-alias>");
+                        return;
+                    }
+                    let new_alias = &args[2];
+                    let device =
+                        BluetoothDevice { address: String::from(&args[1]), name: String::from("") };
+                    let old_alias = self
+                        .context
+                        .lock()
+                        .unwrap()
+                        .adapter_dbus
+                        .as_ref()
+                        .unwrap()
+                        .get_remote_alias(device.clone());
+                    println!("Updating alias for {}: {} -> {}", &args[1], old_alias, new_alias);
+                    self.context
+                        .lock()
+                        .unwrap()
+                        .adapter_dbus
+                        .as_mut()
+                        .unwrap()
+                        .set_remote_alias(device.clone(), new_alias.clone());
+                }
                 _ => {
                     println!("Invalid argument '{}'", args[0]);
                 }
diff --git a/system/gd/rust/linux/client/src/dbus_iface.rs b/system/gd/rust/linux/client/src/dbus_iface.rs
index 300316f..a18729e 100644
--- a/system/gd/rust/linux/client/src/dbus_iface.rs
+++ b/system/gd/rust/linux/client/src/dbus_iface.rs
@@ -400,6 +400,11 @@
         dbus_generated!()
     }
 
+    #[dbus_method("SetRemoteAlias")]
+    fn set_remote_alias(&mut self, device: BluetoothDevice, new_alias: String) {
+        dbus_generated!()
+    }
+
     #[dbus_method("GetRemoteClass")]
     fn get_remote_class(&self, device: BluetoothDevice) -> u32 {
         dbus_generated!()
diff --git a/system/gd/rust/linux/client/src/main.rs b/system/gd/rust/linux/client/src/main.rs
index 29790f1..62e5963 100644
--- a/system/gd/rust/linux/client/src/main.rs
+++ b/system/gd/rust/linux/client/src/main.rs
@@ -319,6 +319,8 @@
                     format!("/org/chromium/bluetooth/client/{}/bluetooth_callback", adapter);
                 let conn_cb_objpath: String =
                     format!("/org/chromium/bluetooth/client/{}/bluetooth_conn_callback", adapter);
+                let suspend_cb_objpath: String =
+                    format!("/org/chromium/bluetooth/client/{}/suspend_callback", adapter);
 
                 let dbus_connection = context.lock().unwrap().dbus_connection.clone();
                 let dbus_crossroads = context.lock().unwrap().dbus_crossroads.clone();
@@ -348,7 +350,7 @@
                 // TODO(b/224606285): Implement suspend debug utils in btclient.
                 context.lock().unwrap().suspend_dbus.as_mut().unwrap().register_callback(Box::new(
                     SuspendCallback::new(
-                        cb_objpath,
+                        suspend_cb_objpath,
                         dbus_connection.clone(),
                         dbus_crossroads.clone(),
                     ),
diff --git a/system/gd/rust/linux/service/src/iface_bluetooth.rs b/system/gd/rust/linux/service/src/iface_bluetooth.rs
index 37769b6..10229b2 100644
--- a/system/gd/rust/linux/service/src/iface_bluetooth.rs
+++ b/system/gd/rust/linux/service/src/iface_bluetooth.rs
@@ -248,6 +248,11 @@
         dbus_generated!()
     }
 
+    #[dbus_method("SetRemoteAlias")]
+    fn set_remote_alias(&mut self, _device: BluetoothDevice, new_alias: String) {
+        dbus_generated!()
+    }
+
     #[dbus_method("GetRemoteClass")]
     fn get_remote_class(&self, _device: BluetoothDevice) -> u32 {
         dbus_generated!()
diff --git a/system/gd/rust/linux/service/src/main.rs b/system/gd/rust/linux/service/src/main.rs
index 62b97f2..e749843 100644
--- a/system/gd/rust/linux/service/src/main.rs
+++ b/system/gd/rust/linux/service/src/main.rs
@@ -76,19 +76,6 @@
 
     let adapter_index = get_adapter_index(&args);
 
-    // Hold locks and initialize all interfaces.
-    {
-        intf.lock().unwrap().initialize(get_bt_dispatcher(tx.clone()), args);
-
-        bluetooth_media.lock().unwrap().set_adapter(bluetooth.clone());
-
-        let mut bluetooth = bluetooth.lock().unwrap();
-        bluetooth.init_profiles();
-        bluetooth.enable();
-
-        bluetooth_gatt.lock().unwrap().init_profiles(tx.clone());
-    }
-
     topstack::get_runtime().block_on(async {
         // Connect to D-Bus system bus.
         let (resource, conn) = connection::new_system_sync()?;
@@ -138,7 +125,7 @@
             make_object_name(adapter_index, "gatt"),
             conn.clone(),
             &mut cr,
-            bluetooth_gatt,
+            bluetooth_gatt.clone(),
             disconnect_watcher.clone(),
         );
 
@@ -146,7 +133,7 @@
             make_object_name(adapter_index, "media"),
             conn.clone(),
             &mut cr,
-            bluetooth_media,
+            bluetooth_media.clone(),
             disconnect_watcher.clone(),
         );
 
@@ -158,6 +145,22 @@
             disconnect_watcher.clone(),
         );
 
+        // Hold locks and initialize all interfaces. This must be done AFTER DBus is
+        // initialized so DBus can properly enforce user policies.
+        {
+            intf.lock().unwrap().initialize(get_bt_dispatcher(tx.clone()), args);
+
+            bluetooth_media.lock().unwrap().set_adapter(bluetooth.clone());
+
+            let mut bluetooth = bluetooth.lock().unwrap();
+            bluetooth.init_profiles();
+            bluetooth.enable();
+
+            bluetooth_gatt.lock().unwrap().init_profiles(tx.clone());
+        }
+
+        // Start listening on DBus after exporting interfaces and initializing
+        // all bluetooth objects.
         conn.start_receive(
             MatchRule::new_method_call(),
             Box::new(move |msg, conn| {
diff --git a/system/gd/rust/linux/stack/btif_macros/src/lib.rs b/system/gd/rust/linux/stack/btif_macros/src/lib.rs
index 549c609..4a0f0a3 100644
--- a/system/gd/rust/linux/stack/btif_macros/src/lib.rs
+++ b/system/gd/rust/linux/stack/btif_macros/src/lib.rs
@@ -13,8 +13,20 @@
 
 use crate::proc_macro::TokenStream;
 
+const OUTPUT_DEBUG: bool = false;
+
 fn debug_output_to_file(gen: &proc_macro2::TokenStream, filename: String) {
-    let path = Path::new(filename.as_str());
+    if !OUTPUT_DEBUG {
+        return;
+    }
+
+    let filepath = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap())
+        .join(filename)
+        .to_str()
+        .unwrap()
+        .to_string();
+
+    let path = Path::new(&filepath);
     let mut file = File::create(&path).unwrap();
     file.write_all(gen.to_string().as_bytes()).unwrap();
 }
@@ -120,7 +132,7 @@
     };
 
     // TODO: Have a simple framework to turn on/off macro-generated code debug.
-    debug_output_to_file(&gen, format!("/tmp/out-{}.rs", fn_ident.to_string()));
+    debug_output_to_file(&gen, format!("out-{}.rs", fn_ident.to_string()));
 
     gen.into()
 }
diff --git a/system/gd/rust/linux/stack/src/bluetooth.rs b/system/gd/rust/linux/stack/src/bluetooth.rs
index f2a610d..3728d1a 100644
--- a/system/gd/rust/linux/stack/src/bluetooth.rs
+++ b/system/gd/rust/linux/stack/src/bluetooth.rs
@@ -131,6 +131,9 @@
     /// Gets the alias of the remote device.
     fn get_remote_alias(&self, device: BluetoothDevice) -> String;
 
+    /// Sets the alias of the remote device.
+    fn set_remote_alias(&mut self, device: BluetoothDevice, new_alias: String);
+
     /// Gets the class of the remote device.
     fn get_remote_class(&self, device: BluetoothDevice) -> u32;
 
@@ -399,6 +402,16 @@
         self.bonded_devices.get(&device.address).or_else(|| self.found_devices.get(&device.address))
     }
 
+    fn get_remote_device_if_found_mut(
+        &mut self,
+        device: &BluetoothDevice,
+    ) -> Option<&mut BluetoothDeviceContext> {
+        match self.bonded_devices.get_mut(&device.address) {
+            None => self.found_devices.get_mut(&device.address),
+            some => some,
+        }
+    }
+
     fn get_remote_device_property(
         &self,
         device: &BluetoothDevice,
@@ -407,6 +420,31 @@
         self.get_remote_device_if_found(&device)
             .and_then(|d| d.properties.get(property_type).and_then(|p| Some(p.clone())))
     }
+
+    fn set_remote_device_property(
+        &mut self,
+        device: &BluetoothDevice,
+        property_type: BtPropertyType,
+        property: BluetoothProperty,
+    ) -> Result<(), ()> {
+        let mut remote_device = match self.get_remote_device_if_found_mut(&device) {
+            Some(d) => d,
+            None => {
+                return Err(());
+            }
+        };
+
+        let mut addr = RawAddress::from_string(device.address.clone());
+        if addr.is_none() {
+            return Err(());
+        }
+        let addr = addr.as_mut().unwrap();
+
+        // TODO: Determine why a callback isn't invoked to do this.
+        remote_device.properties.insert(property_type, property.clone());
+        self.intf.lock().unwrap().set_remote_device_property(addr, property);
+        Ok(())
+    }
 }
 
 #[btif_callbacks_dispatcher(Bluetooth, dispatch_base_callbacks, BaseCallbacks)]
@@ -1115,6 +1153,14 @@
         }
     }
 
+    fn set_remote_alias(&mut self, device: BluetoothDevice, new_alias: String) {
+        let _ = self.set_remote_device_property(
+            &device,
+            BtPropertyType::RemoteFriendlyName,
+            BluetoothProperty::RemoteFriendlyName(new_alias),
+        );
+    }
+
     fn get_remote_class(&self, device: BluetoothDevice) -> u32 {
         match self.get_remote_device_property(&device, &BtPropertyType::ClassOfDevice) {
             Some(BluetoothProperty::ClassOfDevice(class)) => return class,
diff --git a/system/gd/rust/topshim/src/btif.rs b/system/gd/rust/topshim/src/btif.rs
index 625f815..452d7a6 100644
--- a/system/gd/rust/topshim/src/btif.rs
+++ b/system/gd/rust/topshim/src/btif.rs
@@ -382,7 +382,9 @@
         let len = self.get_len();
         match &*self {
             BluetoothProperty::BdName(name) => {
-                data.copy_from_slice(&name.as_bytes()[0..len]);
+                let copy_len = len - 1;
+                data[0..copy_len].copy_from_slice(&name.as_bytes()[0..copy_len]);
+                data[copy_len] = 0;
             }
             BluetoothProperty::BdAddr(addr) => {
                 data.copy_from_slice(&addr.val);
@@ -408,11 +410,12 @@
                     unsafe { &mut *(data.as_mut_ptr() as *mut bindings::bt_service_record_t) };
                 record.uuid = sr.uuid;
                 record.channel = sr.channel;
-                let name_len = len - mem::size_of::<BtServiceRecord>();
-                record.name.copy_from_slice(
+                let name_len = len - mem::size_of::<BtServiceRecord>() - 1;
+                record.name[0..name_len].copy_from_slice(
                     &(sr.name.as_bytes().iter().map(|x| *x as c_char).collect::<Vec<c_char>>())
                         [0..name_len],
                 );
+                record.name[name_len] = 0;
             }
             BluetoothProperty::AdapterScanMode(sm) => {
                 data.copy_from_slice(&BtScanMode::to_u32(sm).unwrap_or_default().to_ne_bytes());
@@ -428,7 +431,9 @@
                 data.copy_from_slice(&timeout.to_ne_bytes());
             }
             BluetoothProperty::RemoteFriendlyName(name) => {
-                data.copy_from_slice(&name.as_bytes()[0..len]);
+                let copy_len = len - 1;
+                data[0..copy_len].copy_from_slice(&name.as_bytes()[0..copy_len]);
+                data[copy_len] = 0;
             }
             BluetoothProperty::RemoteRssi(rssi) => {
                 data[0] = *rssi as u8;
@@ -1110,4 +1115,46 @@
         let expected: Vec<i32> = vec![1, 2, 3];
         assert_eq!(expected, vec);
     }
+
+    #[test]
+    fn test_property_with_string_conversions() {
+        {
+            let bdname = BluetoothProperty::BdName("FooBar".into());
+            let prop_pair: (Box<[u8]>, bindings::bt_property_t) = bdname.into();
+            let converted: BluetoothProperty = prop_pair.1.into();
+            assert!(match converted {
+                BluetoothProperty::BdName(name) => "FooBar".to_string() == name,
+                _ => false,
+            });
+        }
+
+        {
+            let orig_record = BtServiceRecord {
+                uuid: Uuid { uu: [0; 16] },
+                channel: 3,
+                name: "FooBar".to_string(),
+            };
+            let service_record = BluetoothProperty::ServiceRecord(orig_record.clone());
+            let prop_pair: (Box<[u8]>, bindings::bt_property_t) = service_record.into();
+            let converted: BluetoothProperty = prop_pair.1.into();
+            assert!(match converted {
+                BluetoothProperty::ServiceRecord(sr) => {
+                    sr.uuid == orig_record.uuid
+                        && sr.channel == orig_record.channel
+                        && sr.name == orig_record.name
+                }
+                _ => false,
+            });
+        }
+
+        {
+            let rfname = BluetoothProperty::RemoteFriendlyName("FooBizz".into());
+            let prop_pair: (Box<[u8]>, bindings::bt_property_t) = rfname.into();
+            let converted: BluetoothProperty = prop_pair.1.into();
+            assert!(match converted {
+                BluetoothProperty::RemoteFriendlyName(name) => "FooBizz".to_string() == name,
+                _ => false,
+            });
+        }
+    }
 }
diff --git a/system/stack/acl/btm_pm.cc b/system/stack/acl/btm_pm.cc
index 0ba4a23..1a6417b 100644
--- a/system/stack/acl/btm_pm.cc
+++ b/system/stack/acl/btm_pm.cc
@@ -238,7 +238,7 @@
          (p_mode->min <= p_cb->interval)) ||
         ((p_mode->mode & BTM_PM_MD_FORCE) == 0 &&
          (p_mode->max >= p_cb->interval))) {
-      LOG_INFO(
+      LOG_DEBUG(
           "Device is already in requested mode %d, interval: %d, max: %d, min: "
           "%d",
           p_mode->mode, p_cb->interval, p_mode->max, p_mode->min);
diff --git a/system/stack/btm/btm_ble.cc b/system/stack/btm/btm_ble.cc
index 5e1ab22..dfb136d 100644
--- a/system/stack/btm/btm_ble.cc
+++ b/system/stack/btm/btm_ble.cc
@@ -89,7 +89,11 @@
   memset(p_dev_rec->sec_bd_name, 0, sizeof(tBTM_BD_NAME));
 
   p_dev_rec->device_type |= dev_type;
-  p_dev_rec->ble.SetAddressType(addr_type);
+  if (is_ble_addr_type_known(addr_type))
+    p_dev_rec->ble.SetAddressType(addr_type);
+  else
+    LOG_WARN(
+        "Please do not update device record from anonymous le advertisement");
 
   /* sync up with the Inq Data base*/
   tBTM_INQ_INFO* p_info = BTM_InqDbRead(bd_addr);
@@ -481,7 +485,12 @@
     /* new inquiry result, overwrite device type in security device record */
     if (p_inq_info) {
       p_dev_rec->device_type = p_inq_info->results.device_type;
-      p_dev_rec->ble.SetAddressType(p_inq_info->results.ble_addr_type);
+      if (is_ble_addr_type_known(p_inq_info->results.ble_addr_type))
+        p_dev_rec->ble.SetAddressType(p_inq_info->results.ble_addr_type);
+      else
+        LOG_WARN(
+            "Please do not update device record from anonymous le "
+            "advertisement");
     }
 
     if (p_dev_rec->bd_addr == remote_bda &&
@@ -1713,7 +1722,12 @@
     p_dev_rec->timestamp = btm_cb.dev_rec_count++;
   }
 
-  p_dev_rec->ble.SetAddressType(addr_type);
+  if (is_ble_addr_type_known(addr_type))
+    p_dev_rec->ble.SetAddressType(addr_type);
+  else
+    LOG_WARN(
+        "Please do not update device record from anonymous le advertisement");
+
   p_dev_rec->ble.pseudo_addr = bda;
   p_dev_rec->ble_hci_handle = handle;
   p_dev_rec->device_type |= BT_DEVICE_TYPE_BLE;
diff --git a/system/stack/btm/btm_dev.cc b/system/stack/btm/btm_dev.cc
index d887e93..eeb37be 100644
--- a/system/stack/btm/btm_dev.cc
+++ b/system/stack/btm/btm_dev.cc
@@ -237,7 +237,12 @@
     memcpy(p_dev_rec->dev_class, p_inq_info->results.dev_class, DEV_CLASS_LEN);
 
     p_dev_rec->device_type = p_inq_info->results.device_type;
-    p_dev_rec->ble.SetAddressType(p_inq_info->results.ble_addr_type);
+    if (is_ble_addr_type_known(p_inq_info->results.ble_addr_type))
+      p_dev_rec->ble.SetAddressType(p_inq_info->results.ble_addr_type);
+    else
+      LOG_WARN(
+          "Please do not update device record from anonymous le advertisement");
+
   } else if (bd_addr == btm_cb.connecting_bda)
     memcpy(p_dev_rec->dev_class, btm_cb.connecting_dc, DEV_CLASS_LEN);
 
diff --git a/system/stack/btm/btm_int_types.h b/system/stack/btm/btm_int_types.h
index 2f8a7e5..360db83 100644
--- a/system/stack/btm/btm_int_types.h
+++ b/system/stack/btm/btm_int_types.h
@@ -39,6 +39,8 @@
 #define BTM_MAX_SCN_ 31  // PORT_MAX_RFC_PORTS packages/modules/Bluetooth/system/stack/include/rfcdefs.h
 
 constexpr size_t kMaxLogSize = 255;
+constexpr size_t kBtmLogHistoryBufferSize = 100;
+
 class TimestampedStringCircularBuffer
     : public bluetooth::common::TimestampedCircularBuffer<std::string> {
  public:
@@ -347,7 +349,8 @@
     sco_cb.Init();       /* SCO Database and Structures (If included) */
     devcb.Init();
 
-    history_ = std::make_shared<TimestampedStringCircularBuffer>(40);
+    history_ = std::make_shared<TimestampedStringCircularBuffer>(
+        kBtmLogHistoryBufferSize);
     CHECK(history_ != nullptr);
     history_->Push(std::string("Initialized btm history"));
   }
diff --git a/system/test/rootcanal/bluetooth_hci.cc b/system/test/rootcanal/bluetooth_hci.cc
index 0df9bd8..dcc38dc 100644
--- a/system/test/rootcanal/bluetooth_hci.cc
+++ b/system/test/rootcanal/bluetooth_hci.cc
@@ -36,6 +36,7 @@
 namespace sim {
 
 using android::hardware::hidl_vec;
+using ::bluetooth::hci::Address;
 using rootcanal::AsyncTaskId;
 using rootcanal::DualModeController;
 using rootcanal::HciDevice;
@@ -109,7 +110,12 @@
   char mac_property[PROPERTY_VALUE_MAX] = "";
   property_get("vendor.bt.rootcanal_mac_address", mac_property,
                "3C:5A:B4:01:02:03");
-  controller_->Initialize({"dmc", std::string(mac_property)});
+  auto addr = Address::FromString(std::string(mac_property));
+  if (addr) {
+    controller_->SetAddress(*addr);
+  } else {
+    LOG_ALWAYS_FATAL("Invalid address: %s", mac_property);
+  }
 
   controller_->RegisterEventChannel(
       [this, cb](std::shared_ptr<std::vector<uint8_t>> packet) {
diff --git a/system/tools/scripts/btsnooz.py b/system/tools/scripts/btsnooz.py
index 622824a..5613bd8 100755
--- a/system/tools/scripts/btsnooz.py
+++ b/system/tools/scripts/btsnooz.py
@@ -26,6 +26,7 @@
 import struct
 import sys
 import zlib
+import subprocess
 
 # Enumeration of the values the 'type' field can take in a btsnooz
 # header. These values come from the Bluetooth stack's internal
@@ -56,15 +57,15 @@
   Returns the HCI type of a packet given its btsnooz type.
   """
     if type == TYPE_OUT_CMD:
-        return '\x01'
+        return b'\x01'
     if type == TYPE_IN_ACL or type == TYPE_OUT_ACL:
-        return '\x02'
+        return b'\x02'
     if type == TYPE_IN_SCO or type == TYPE_OUT_SCO:
-        return '\x03'
+        return b'\x03'
     if type == TYPE_IN_EVT:
-        return '\x04'
+        return b'\x04'
     if type == TYPE_IN_ISO or type == TYPE_OUT_ISO:
-        return '\x05'
+        return b'\x05'
     raise RuntimeError("type_to_hci: unknown type (0x{:02x})".format(type))
 
 
@@ -81,7 +82,7 @@
     # Oddly, the file header (9 bytes) is not compressed, but the rest is.
     decompressed = zlib.decompress(snooz[9:])
 
-    sys.stdout.write('btsnoop\x00\x00\x00\x00\x01\x00\x00\x03\xea')
+    sys.stdout.buffer.write(b'btsnoop\x00\x00\x00\x00\x01\x00\x00\x03\xea')
 
     if version == 1:
         decode_snooz_v1(decompressed, last_timestamp_ms)
@@ -108,11 +109,11 @@
         length, delta_time_ms, type = struct.unpack_from('=HIb', decompressed, offset)
         first_timestamp_ms += delta_time_ms
         offset += 7
-        sys.stdout.write(struct.pack('>II', length, length))
-        sys.stdout.write(struct.pack('>II', type_to_direction(type), 0))
-        sys.stdout.write(struct.pack('>II', (first_timestamp_ms >> 32), (first_timestamp_ms & 0xFFFFFFFF)))
-        sys.stdout.write(type_to_hci(type))
-        sys.stdout.write(decompressed[offset:offset + length - 1])
+        sys.stdout.buffer.write(struct.pack('>II', length, length))
+        sys.stdout.buffer.write(struct.pack('>II', type_to_direction(type), 0))
+        sys.stdout.buffer.write(struct.pack('>II', (first_timestamp_ms >> 32), (first_timestamp_ms & 0xFFFFFFFF)))
+        sys.stdout.buffer.write(type_to_hci(type))
+        sys.stdout.buffer.write(decompressed[offset:offset + length - 1])
         offset += length - 1
 
 
@@ -135,31 +136,58 @@
         length, packet_length, delta_time_ms, snooz_type = struct.unpack_from('=HHIb', decompressed, offset)
         first_timestamp_ms += delta_time_ms
         offset += 9
-        sys.stdout.write(struct.pack('>II', packet_length, length))
-        sys.stdout.write(struct.pack('>II', type_to_direction(snooz_type), 0))
-        sys.stdout.write(struct.pack('>II', (first_timestamp_ms >> 32), (first_timestamp_ms & 0xFFFFFFFF)))
-        sys.stdout.write(type_to_hci(snooz_type))
-        sys.stdout.write(decompressed[offset:offset + length - 1])
+        sys.stdout.buffer.write(struct.pack('>II', packet_length, length))
+        sys.stdout.buffer.write(struct.pack('>II', type_to_direction(snooz_type), 0))
+        sys.stdout.buffer.write(struct.pack('>II', (first_timestamp_ms >> 32), (first_timestamp_ms & 0xFFFFFFFF)))
+        sys.stdout.buffer.write(type_to_hci(snooz_type))
+        sys.stdout.buffer.write(decompressed[offset:offset + length - 1])
         offset += length - 1
 
 
 def main():
     if len(sys.argv) > 2:
         sys.stderr.write('Usage: %s [bugreport]\n' % sys.argv[0])
-        exit(1)
+        sys.exit(1)
+
+    ## Assume the uudecoded data is being piped in
+    if not sys.stdin.isatty():
+        base64_string = ""
+        try:
+            for line in sys.stdin.readlines():
+                base64_string += line.strip()
+            decode_snooz(base64.standard_b64decode(base64_string))
+            sys.exit(0)
+        except Exception as e:
+            sys.stderr.write('Failed uudecoding...ensure input is a valid uuencoded stream.\n')
+            sys.stderr.write(e)
+            sys.exit(1)
 
     iterator = fileinput.input()
+
     found = False
     base64_string = ""
-    for line in iterator:
-        if found:
-            if line.find('--- END:BTSNOOP_LOG_SUMMARY') != -1:
-                decode_snooz(base64.standard_b64decode(base64_string))
-                sys.exit(0)
-            base64_string += line.strip()
+    try:
+        for line in iterator:
+            if found:
+                if line.find('--- END:BTSNOOP_LOG_SUMMARY') != -1:
+                    decode_snooz(base64.standard_b64decode(base64_string))
+                    sys.exit(0)
+                base64_string += line.strip()
 
-        if line.find('--- BEGIN:BTSNOOP_LOG_SUMMARY') != -1:
-            found = True
+            if line.find('--- BEGIN:BTSNOOP_LOG_SUMMARY') != -1:
+                found = True
+
+    except UnicodeDecodeError:
+        ## Check if there is a BTSNOOP log uuencoded in the bugreport
+        p = subprocess.Popen(["egrep", "-a", "BTSNOOP_LOG_SUMMARY", sys.argv[1]], stdout=subprocess.PIPE)
+        p.wait()
+
+        if (p.returncode == 0):
+            sys.stderr.write('Failed to parse uuencoded btsnooz data from bugreport.\n')
+            sys.stderr.write(' Try:\n')
+            sys.stderr.write('LC_CTYPE=C sed -n "/BEGIN:BTSNOOP_LOG_SUMMARY/,/END:BTSNOOP_LOG_SUMMARY/p " ' +
+                             sys.argv[1] + ' | egrep -av "BTSNOOP_LOG_SUMMARY" | ' + sys.argv[0] + ' > hci.log\n')
+            sys.exit(1)
 
     if not found:
         sys.stderr.write('No btsnooz section found in bugreport.\n')
diff --git a/tools/rootcanal/model/controller/dual_mode_controller.cc b/tools/rootcanal/model/controller/dual_mode_controller.cc
index 3770fcf..32a8fdb 100644
--- a/tools/rootcanal/model/controller/dual_mode_controller.cc
+++ b/tools/rootcanal/model/controller/dual_mode_controller.cc
@@ -38,17 +38,6 @@
 constexpr uint16_t kLeMaximumDataTime = 0x148;
 
 // Device methods.
-void DualModeController::Initialize(const std::vector<std::string>& args) {
-  if (args.size() < 2) return;
-
-  Address addr{};
-  if (Address::FromString(args[1], addr)) {
-    properties_.SetAddress(addr);
-  } else {
-    LOG_ALWAYS_FATAL("Invalid address: %s", args[1].c_str());
-  }
-};
-
 std::string DualModeController::GetTypeString() const {
   return "Simulated Bluetooth Controller";
 }
diff --git a/tools/rootcanal/model/controller/dual_mode_controller.h b/tools/rootcanal/model/controller/dual_mode_controller.h
index f9a4d28..8ef3cf3 100644
--- a/tools/rootcanal/model/controller/dual_mode_controller.h
+++ b/tools/rootcanal/model/controller/dual_mode_controller.h
@@ -62,8 +62,6 @@
   ~DualModeController() = default;
 
   // Device methods.
-  virtual void Initialize(const std::vector<std::string>& args) override;
-
   virtual std::string GetTypeString() const override;
 
   virtual void IncomingPacket(
diff --git a/tools/rootcanal/model/devices/beacon.cc b/tools/rootcanal/model/devices/beacon.cc
index e18a091..00a6983 100644
--- a/tools/rootcanal/model/devices/beacon.cc
+++ b/tools/rootcanal/model/devices/beacon.cc
@@ -40,6 +40,17 @@
                                  'c'});
 }
 
+Beacon::Beacon(const vector<std::string>& args) : Beacon() {
+  if (args.size() >= 2) {
+    Address addr{};
+    if (Address::FromString(args[1], addr)) properties_.SetLeAddress(addr);
+  }
+
+  if (args.size() >= 3) {
+    SetAdvertisementInterval(std::chrono::milliseconds(std::stoi(args[2])));
+  }
+}
+
 std::string Beacon::GetTypeString() const { return "beacon"; }
 
 std::string Beacon::ToString() const {
@@ -49,17 +60,6 @@
   return dev;
 }
 
-void Beacon::Initialize(const vector<std::string>& args) {
-  if (args.size() < 2) return;
-
-  Address addr{};
-  if (Address::FromString(args[1], addr)) properties_.SetLeAddress(addr);
-
-  if (args.size() < 3) return;
-
-  SetAdvertisementInterval(std::chrono::milliseconds(std::stoi(args[2])));
-}
-
 void Beacon::TimerTick() {
   if (IsAdvertisementAvailable()) {
     last_advertisement_ = std::chrono::steady_clock::now();
diff --git a/tools/rootcanal/model/devices/beacon.h b/tools/rootcanal/model/devices/beacon.h
index c0fc4ed..fc6af5c 100644
--- a/tools/rootcanal/model/devices/beacon.h
+++ b/tools/rootcanal/model/devices/beacon.h
@@ -27,9 +27,12 @@
 class Beacon : public Device {
  public:
   Beacon();
+  Beacon(const std::vector<std::string>& args);
   virtual ~Beacon() = default;
 
-  static std::shared_ptr<Device> Create() { return std::make_shared<Beacon>(); }
+  static std::shared_ptr<Device> Create(const std::vector<std::string>& args) {
+    return std::make_shared<Beacon>(args);
+  }
 
   // Return a string representation of the type of device.
   virtual std::string GetTypeString() const override;
@@ -37,9 +40,6 @@
   // Return a string representation of the device.
   virtual std::string ToString() const override;
 
-  // Set the address and advertising interval from string args.
-  virtual void Initialize(const std::vector<std::string>& args) override;
-
   virtual void IncomingPacket(
       model::packets::LinkLayerPacketView packet) override;
 
diff --git a/tools/rootcanal/model/devices/beacon_swarm.cc b/tools/rootcanal/model/devices/beacon_swarm.cc
index ac2cea6..f47df40 100644
--- a/tools/rootcanal/model/devices/beacon_swarm.cc
+++ b/tools/rootcanal/model/devices/beacon_swarm.cc
@@ -24,7 +24,7 @@
 bool BeaconSwarm::registered_ =
     DeviceBoutique::Register("beacon_swarm", &BeaconSwarm::Create);
 
-BeaconSwarm::BeaconSwarm() {
+BeaconSwarm::BeaconSwarm(const vector<std::string>& args) : Beacon(args) {
   advertising_interval_ms_ = std::chrono::milliseconds(1280);
   properties_.SetLeAdvertisementType(0x03 /* NON_CONNECT */);
   properties_.SetLeAdvertisement({
@@ -60,17 +60,6 @@
                                  'c'});
 }
 
-void BeaconSwarm::Initialize(const vector<std::string>& args) {
-  if (args.size() < 2) return;
-
-  Address addr{};
-  if (Address::FromString(args[1], addr)) properties_.SetLeAddress(addr);
-
-  if (args.size() < 3) return;
-
-  SetAdvertisementInterval(std::chrono::milliseconds(std::stoi(args[2])));
-}
-
 void BeaconSwarm::TimerTick() {
   Address beacon_addr = properties_.GetLeAddress();
   uint8_t* low_order_byte = (uint8_t*)(&beacon_addr);
diff --git a/tools/rootcanal/model/devices/beacon_swarm.h b/tools/rootcanal/model/devices/beacon_swarm.h
index 118e047..775aa7c 100644
--- a/tools/rootcanal/model/devices/beacon_swarm.h
+++ b/tools/rootcanal/model/devices/beacon_swarm.h
@@ -26,19 +26,16 @@
 // Pretend to be a lot of beacons by changing the advertising address.
 class BeaconSwarm : public Beacon {
  public:
-  BeaconSwarm();
+  BeaconSwarm(const std::vector<std::string>& args);
   virtual ~BeaconSwarm() = default;
 
-  static std::shared_ptr<Device> Create() {
-    return std::make_shared<BeaconSwarm>();
+  static std::shared_ptr<Device> Create(const std::vector<std::string>& args) {
+    return std::make_shared<BeaconSwarm>(args);
   }
 
   // Return a string representation of the type of device.
   virtual std::string GetTypeString() const override { return "beacon_swarm"; }
 
-  // Set the address and advertising interval from string args.
-  virtual void Initialize(const std::vector<std::string>& args) override;
-
   virtual void TimerTick() override;
 
  private:
diff --git a/tools/rootcanal/model/devices/broken_adv.cc b/tools/rootcanal/model/devices/broken_adv.cc
index 2de2017..5190f99 100644
--- a/tools/rootcanal/model/devices/broken_adv.cc
+++ b/tools/rootcanal/model/devices/broken_adv.cc
@@ -51,15 +51,15 @@
   page_scan_delay_ms_ = std::chrono::milliseconds(600);
 }
 
-void BrokenAdv::Initialize(const vector<std::string>& args) {
-  if (args.size() < 2) return;
+BrokenAdv::BrokenAdv(const vector<std::string>& args) : BrokenAdv() {
+  if (args.size() >= 2) {
+    Address addr{};
+    if (Address::FromString(args[1], addr)) properties_.SetLeAddress(addr);
+  }
 
-  Address addr{};
-  if (Address::FromString(args[1], addr)) properties_.SetLeAddress(addr);
-
-  if (args.size() < 3) return;
-
-  SetAdvertisementInterval(std::chrono::milliseconds(std::stoi(args[2])));
+  if (args.size() >= 3) {
+    SetAdvertisementInterval(std::chrono::milliseconds(std::stoi(args[2])));
+  }
 }
 
 // Mostly return the correct length
diff --git a/tools/rootcanal/model/devices/broken_adv.h b/tools/rootcanal/model/devices/broken_adv.h
index 34c7369..e551ea2 100644
--- a/tools/rootcanal/model/devices/broken_adv.h
+++ b/tools/rootcanal/model/devices/broken_adv.h
@@ -26,15 +26,13 @@
 class BrokenAdv : public Device {
  public:
   BrokenAdv();
+  BrokenAdv(const std::vector<std::string>& args);
   ~BrokenAdv() = default;
 
-  static std::shared_ptr<Device> Create() {
-    return std::make_shared<BrokenAdv>();
+  static std::shared_ptr<Device> Create(const std::vector<std::string>& args) {
+    return std::make_shared<BrokenAdv>(args);
   }
 
-  // Initialize the device based on the values of |args|.
-  virtual void Initialize(const std::vector<std::string>& args) override;
-
   // Return a string representation of the type of device.
   virtual std::string GetTypeString() const override { return "broken_adv"; }
 
diff --git a/tools/rootcanal/model/devices/car_kit.cc b/tools/rootcanal/model/devices/car_kit.cc
index ba2c43e..ef92980 100644
--- a/tools/rootcanal/model/devices/car_kit.cc
+++ b/tools/rootcanal/model/devices/car_kit.cc
@@ -81,16 +81,16 @@
   });
 }
 
-void CarKit::Initialize(const vector<std::string>& args) {
-  if (args.size() < 2) return;
+CarKit::CarKit(const vector<std::string>& args) : CarKit() {
+  if (args.size() >= 2) {
+    Address addr{};
+    if (Address::FromString(args[1], addr)) properties_.SetAddress(addr);
+    LOG_INFO("%s SetAddress %s", ToString().c_str(), addr.ToString().c_str());
+  }
 
-  Address addr{};
-  if (Address::FromString(args[1], addr)) properties_.SetAddress(addr);
-  LOG_INFO("%s SetAddress %s", ToString().c_str(), addr.ToString().c_str());
-
-  if (args.size() < 3) return;
-
-  properties_.SetClockOffset(std::stoi(args[2]));
+  if (args.size() >= 3) {
+    properties_.SetClockOffset(std::stoi(args[2]));
+  }
 }
 
 void CarKit::TimerTick() { link_layer_controller_.TimerTick(); }
diff --git a/tools/rootcanal/model/devices/car_kit.h b/tools/rootcanal/model/devices/car_kit.h
index 8b4295d..f793e49 100644
--- a/tools/rootcanal/model/devices/car_kit.h
+++ b/tools/rootcanal/model/devices/car_kit.h
@@ -28,12 +28,12 @@
 class CarKit : public Device {
  public:
   CarKit();
+  CarKit(const std::vector<std::string>& args);
   ~CarKit() = default;
 
-  static std::shared_ptr<CarKit> Create() { return std::make_shared<CarKit>(); }
-
-  // Initialize the device based on the values of |args|.
-  virtual void Initialize(const std::vector<std::string>& args) override;
+  static std::shared_ptr<CarKit> Create(const std::vector<std::string>& args) {
+    return std::make_shared<CarKit>(args);
+  }
 
   // Return a string representation of the type of device.
   virtual std::string GetTypeString() const override { return "car_kit"; }
diff --git a/tools/rootcanal/model/devices/classic.cc b/tools/rootcanal/model/devices/classic.cc
index 732a826..60866c9 100644
--- a/tools/rootcanal/model/devices/classic.cc
+++ b/tools/rootcanal/model/devices/classic.cc
@@ -40,15 +40,15 @@
   page_scan_delay_ms_ = std::chrono::milliseconds(600);
 }
 
-void Classic::Initialize(const vector<std::string>& args) {
-  if (args.size() < 2) return;
+Classic::Classic(const vector<std::string>& args) : Classic() {
+  if (args.size() >= 2) {
+    Address addr{};
+    if (Address::FromString(args[1], addr)) properties_.SetAddress(addr);
+  }
 
-  Address addr{};
-  if (Address::FromString(args[1], addr)) properties_.SetAddress(addr);
-
-  if (args.size() < 3) return;
-
-  properties_.SetClockOffset(std::stoi(args[2]));
+  if (args.size() >= 3) {
+    properties_.SetClockOffset(std::stoi(args[2]));
+  }
 }
 
 }  // namespace rootcanal
diff --git a/tools/rootcanal/model/devices/classic.h b/tools/rootcanal/model/devices/classic.h
index 861c7cf..b3f6b65 100644
--- a/tools/rootcanal/model/devices/classic.h
+++ b/tools/rootcanal/model/devices/classic.h
@@ -26,15 +26,13 @@
 class Classic : public Device {
  public:
   Classic();
+  Classic(const std::vector<std::string>& args);
   ~Classic() = default;
 
-  static std::shared_ptr<Device> Create() {
-    return std::make_shared<Classic>();
+  static std::shared_ptr<Device> Create(const std::vector<std::string>& args) {
+    return std::make_shared<Classic>(args);
   }
 
-  // Initialize the device based on the values of |args|.
-  virtual void Initialize(const std::vector<std::string>& args) override;
-
   // Return a string representation of the type of device.
   virtual std::string GetTypeString() const override { return "classic"; }
 
diff --git a/tools/rootcanal/model/devices/device.h b/tools/rootcanal/model/devices/device.h
index f57f11f..25d1024 100644
--- a/tools/rootcanal/model/devices/device.h
+++ b/tools/rootcanal/model/devices/device.h
@@ -40,9 +40,6 @@
         properties_(properties_filename) {}
   virtual ~Device() = default;
 
-  // Initialize the device based on the values of |args|.
-  virtual void Initialize(const std::vector<std::string>& args) = 0;
-
   // Return a string representation of the type of device.
   virtual std::string GetTypeString() const = 0;
 
diff --git a/tools/rootcanal/model/devices/keyboard.cc b/tools/rootcanal/model/devices/keyboard.cc
index 5c818a0..9d219de 100644
--- a/tools/rootcanal/model/devices/keyboard.cc
+++ b/tools/rootcanal/model/devices/keyboard.cc
@@ -24,7 +24,7 @@
 bool Keyboard::registered_ =
     DeviceBoutique::Register("keyboard", &Keyboard::Create);
 
-Keyboard::Keyboard() {
+Keyboard::Keyboard(const vector<std::string>& args) : Beacon(args) {
   properties_.SetLeAdvertisementType(0x00 /* CONNECTABLE */);
   properties_.SetLeAdvertisement(
       {0x11,  // Length
@@ -63,17 +63,6 @@
 
 std::string Keyboard::GetTypeString() const { return "keyboard"; }
 
-void Keyboard::Initialize(const vector<std::string>& args) {
-  if (args.size() < 2) return;
-
-  Address addr{};
-  if (Address::FromString(args[1], addr)) properties_.SetLeAddress(addr);
-
-  if (args.size() < 3) return;
-
-  SetAdvertisementInterval(std::chrono::milliseconds(std::stoi(args[2])));
-}
-
 void Keyboard::TimerTick() {
   if (!connected_) {
     Beacon::TimerTick();
diff --git a/tools/rootcanal/model/devices/keyboard.h b/tools/rootcanal/model/devices/keyboard.h
index a97e3f9..65620f3 100644
--- a/tools/rootcanal/model/devices/keyboard.h
+++ b/tools/rootcanal/model/devices/keyboard.h
@@ -26,19 +26,16 @@
 
 class Keyboard : public Beacon {
  public:
-  Keyboard();
+  Keyboard(const std::vector<std::string>& args);
   virtual ~Keyboard() = default;
 
-  static std::shared_ptr<Device> Create() {
-    return std::make_shared<Keyboard>();
+  static std::shared_ptr<Device> Create(const std::vector<std::string>& args) {
+    return std::make_shared<Keyboard>(args);
   }
 
   // Return a string representation of the type of device.
   virtual std::string GetTypeString() const override;
 
-  // Initialize the device based on the values of |args|.
-  virtual void Initialize(const std::vector<std::string>& args) override;
-
   virtual void IncomingPacket(
       model::packets::LinkLayerPacketView packet) override;
 
diff --git a/tools/rootcanal/model/devices/link_layer_socket_device.h b/tools/rootcanal/model/devices/link_layer_socket_device.h
index 8ad89bd..d887760 100644
--- a/tools/rootcanal/model/devices/link_layer_socket_device.h
+++ b/tools/rootcanal/model/devices/link_layer_socket_device.h
@@ -48,8 +48,6 @@
     return "link_layer_socket_device";
   }
 
-  virtual void Initialize(const std::vector<std::string>&) override {}
-
   virtual void IncomingPacket(
       model::packets::LinkLayerPacketView packet) override;
 
diff --git a/tools/rootcanal/model/devices/loopback.cc b/tools/rootcanal/model/devices/loopback.cc
index bd4ecdf..59c0959 100644
--- a/tools/rootcanal/model/devices/loopback.cc
+++ b/tools/rootcanal/model/devices/loopback.cc
@@ -44,6 +44,17 @@
                                  'l', 'o', 'o', 'p'});
 }
 
+Loopback::Loopback(const vector<std::string>& args) : Loopback() {
+  if (args.size() >= 2) {
+    Address addr{};
+    if (Address::FromString(args[1], addr)) properties_.SetLeAddress(addr);
+  }
+
+  if (args.size() >= 3) {
+    SetAdvertisementInterval(std::chrono::milliseconds(std::stoi(args[2])));
+  }
+}
+
 std::string Loopback::GetTypeString() const { return "loopback"; }
 
 std::string Loopback::ToString() const {
@@ -53,17 +64,6 @@
   return dev;
 }
 
-void Loopback::Initialize(const vector<std::string>& args) {
-  if (args.size() < 2) return;
-
-  Address addr{};
-  if (Address::FromString(args[1], addr)) properties_.SetLeAddress(addr);
-
-  if (args.size() < 3) return;
-
-  SetAdvertisementInterval(std::chrono::milliseconds(std::stoi(args[2])));
-}
-
 void Loopback::TimerTick() {}
 
 void Loopback::IncomingPacket(model::packets::LinkLayerPacketView packet) {
diff --git a/tools/rootcanal/model/devices/loopback.h b/tools/rootcanal/model/devices/loopback.h
index 866a81d..d6af78d 100644
--- a/tools/rootcanal/model/devices/loopback.h
+++ b/tools/rootcanal/model/devices/loopback.h
@@ -27,10 +27,11 @@
 class Loopback : public Device {
  public:
   Loopback();
+  Loopback(const std::vector<std::string>& args);
   virtual ~Loopback() = default;
 
-  static std::shared_ptr<Device> Create() {
-    return std::make_shared<Loopback>();
+  static std::shared_ptr<Device> Create(const std::vector<std::string>& args) {
+    return std::make_shared<Loopback>(args);
   }
 
   // Return a string representation of the type of device.
@@ -39,9 +40,6 @@
   // Return a string representation of the device.
   virtual std::string ToString() const override;
 
-  // Set the address and advertising interval from string args.
-  virtual void Initialize(const std::vector<std::string>& args) override;
-
   virtual void IncomingPacket(
       model::packets::LinkLayerPacketView packet) override;
 
diff --git a/tools/rootcanal/model/devices/remote_loopback_device.cc b/tools/rootcanal/model/devices/remote_loopback_device.cc
index 3d5ceff..afe2754 100644
--- a/tools/rootcanal/model/devices/remote_loopback_device.cc
+++ b/tools/rootcanal/model/devices/remote_loopback_device.cc
@@ -35,13 +35,6 @@
   return GetTypeString() + " (no address)";
 }
 
-void RemoteLoopbackDevice::Initialize(const std::vector<std::string>& args) {
-  if (args.size() < 2) return;
-
-  Address addr{};
-  if (Address::FromString(args[1], addr)) properties_.SetAddress(addr);
-}
-
 void RemoteLoopbackDevice::IncomingPacket(
     model::packets::LinkLayerPacketView packet) {
   // TODO: Check sender?
diff --git a/tools/rootcanal/model/devices/remote_loopback_device.h b/tools/rootcanal/model/devices/remote_loopback_device.h
index 78c1ffb..496093b 100644
--- a/tools/rootcanal/model/devices/remote_loopback_device.h
+++ b/tools/rootcanal/model/devices/remote_loopback_device.h
@@ -28,7 +28,7 @@
   RemoteLoopbackDevice();
   virtual ~RemoteLoopbackDevice() = default;
 
-  static std::shared_ptr<Device> Create() {
+  static std::shared_ptr<Device> Create(const std::vector<std::string>&) {
     return std::make_shared<RemoteLoopbackDevice>();
   }
 
@@ -38,8 +38,6 @@
 
   virtual std::string ToString() const override;
 
-  virtual void Initialize(const std::vector<std::string>& args) override;
-
   virtual void IncomingPacket(
       model::packets::LinkLayerPacketView packet) override;
 
diff --git a/tools/rootcanal/model/devices/scripted_beacon.cc b/tools/rootcanal/model/devices/scripted_beacon.cc
index b13c569..9c04be7 100644
--- a/tools/rootcanal/model/devices/scripted_beacon.cc
+++ b/tools/rootcanal/model/devices/scripted_beacon.cc
@@ -37,7 +37,8 @@
 namespace rootcanal {
 bool ScriptedBeacon::registered_ =
     DeviceBoutique::Register("scripted_beacon", &ScriptedBeacon::Create);
-ScriptedBeacon::ScriptedBeacon() {
+
+ScriptedBeacon::ScriptedBeacon(const vector<std::string>& args) : Beacon(args) {
   advertising_interval_ms_ = std::chrono::milliseconds(1280);
   properties_.SetLeAdvertisementType(0x02 /* SCANNABLE */);
   properties_.SetLeAdvertisement({
@@ -75,31 +76,20 @@
                                  0x08,  // TYPE_NAME_SHORT
                                  'g', 'b', 'e', 'a'});
   LOG_INFO("Scripted_beacon registered %s", registered_ ? "true" : "false");
-}
 
-bool has_time_elapsed(steady_clock::time_point time_point) {
-  return steady_clock::now() > time_point;
-}
-
-void ScriptedBeacon::Initialize(const vector<std::string>& args) {
-  if (args.size() < 2) {
-    LOG_ERROR(
-        "Initialization failed, need mac address, playback and playback events "
-        "file arguments");
-    return;
-  }
-
-  Address addr{};
-  if (Address::FromString(args[1], addr)) properties_.SetLeAddress(addr);
-
-  if (args.size() < 4) {
+  if (args.size() >= 4) {
+    config_file_ = args[2];
+    events_file_ = args[3];
+    set_state(PlaybackEvent::INITIALIZED);
+  } else {
     LOG_ERROR(
         "Initialization failed, need playback and playback events file "
         "arguments");
   }
-  config_file_ = args[2];
-  events_file_ = args[3];
-  set_state(PlaybackEvent::INITIALIZED);
+}
+
+bool has_time_elapsed(steady_clock::time_point time_point) {
+  return steady_clock::now() > time_point;
 }
 
 void ScriptedBeacon::populate_event(PlaybackEvent* event,
diff --git a/tools/rootcanal/model/devices/scripted_beacon.h b/tools/rootcanal/model/devices/scripted_beacon.h
index 333480a..a5ea22a 100644
--- a/tools/rootcanal/model/devices/scripted_beacon.h
+++ b/tools/rootcanal/model/devices/scripted_beacon.h
@@ -30,11 +30,11 @@
 // Pretend to be a lot of beacons by advertising from a file.
 class ScriptedBeacon : public Beacon {
  public:
-  ScriptedBeacon();
+  ScriptedBeacon(const std::vector<std::string>& args);
   virtual ~ScriptedBeacon() = default;
 
-  static std::shared_ptr<Device> Create() {
-    return std::make_shared<ScriptedBeacon>();
+  static std::shared_ptr<Device> Create(const std::vector<std::string>& args) {
+    return std::make_shared<ScriptedBeacon>(args);
   }
 
   // Return a string representation of the type of device.
@@ -46,9 +46,6 @@
     return "scripted_beacon " + config_file_;
   }
 
-  // Set the address and advertising interval from string args.
-  void Initialize(const std::vector<std::string>& args) override;
-
   void TimerTick() override;
 
   void IncomingPacket(model::packets::LinkLayerPacketView packet_view) override;
diff --git a/tools/rootcanal/model/devices/sniffer.cc b/tools/rootcanal/model/devices/sniffer.cc
index b57acd1..a38607b 100644
--- a/tools/rootcanal/model/devices/sniffer.cc
+++ b/tools/rootcanal/model/devices/sniffer.cc
@@ -26,16 +26,12 @@
 bool Sniffer::registered_ =
     DeviceBoutique::Register("sniffer", &Sniffer::Create);
 
-Sniffer::Sniffer() {}
-
-void Sniffer::Initialize(const vector<std::string>& args) {
-  if (args.size() < 2) return;
-
-  if (Address::FromString(args[1], device_to_sniff_)) {
-    properties_.SetAddress(device_to_sniff_);
+Sniffer::Sniffer(const vector<std::string>& args) {
+  if (args.size() >= 2) {
+    if (Address::FromString(args[1], device_to_sniff_)) {
+      properties_.SetAddress(device_to_sniff_);
+    }
   }
-
-  if (args.size() < 3) return;
 }
 
 void Sniffer::TimerTick() {}
diff --git a/tools/rootcanal/model/devices/sniffer.h b/tools/rootcanal/model/devices/sniffer.h
index f72b4f7..2688655 100644
--- a/tools/rootcanal/model/devices/sniffer.h
+++ b/tools/rootcanal/model/devices/sniffer.h
@@ -29,16 +29,13 @@
 
 class Sniffer : public Device {
  public:
-  Sniffer();
+  Sniffer(const std::vector<std::string>& args);
   ~Sniffer() = default;
 
-  static std::shared_ptr<Sniffer> Create() {
-    return std::make_shared<Sniffer>();
+  static std::shared_ptr<Sniffer> Create(const std::vector<std::string>& args) {
+    return std::make_shared<Sniffer>(args);
   }
 
-  // Initialize the device based on the values of |args|.
-  virtual void Initialize(const std::vector<std::string>& args) override;
-
   // Return a string representation of the type of device.
   virtual std::string GetTypeString() const override { return "sniffer"; }
 
diff --git a/tools/rootcanal/model/setup/device_boutique.cc b/tools/rootcanal/model/setup/device_boutique.cc
index d95fe9d..3acb5b5 100644
--- a/tools/rootcanal/model/setup/device_boutique.cc
+++ b/tools/rootcanal/model/setup/device_boutique.cc
@@ -22,10 +22,11 @@
 
 namespace rootcanal {
 
-std::unordered_map<std::string, std::function<std::shared_ptr<Device>()>>&
+std::unordered_map<std::string, std::function<std::shared_ptr<Device>(
+                                    const vector<std::string>&)>>&
 DeviceBoutique::GetMap() {
-  static std::unordered_map<std::string,
-                            std::function<std::shared_ptr<Device>()>>
+  static std::unordered_map<std::string, std::function<std::shared_ptr<Device>(
+                                             const vector<std::string>&)>>
       impl;
   return impl;
 }
@@ -33,9 +34,10 @@
 // Register a constructor for a device type.
 bool DeviceBoutique::Register(
     const std::string& device_type,
-    const std::function<std::shared_ptr<Device>()> device_constructor) {
+    const std::function<std::shared_ptr<Device>(const vector<std::string>&)>
+        method) {
   LOG_INFO("Registering %s", device_type.c_str());
-  GetMap()[device_type] = device_constructor;
+  GetMap()[device_type] = method;
   return true;
 }
 
@@ -43,15 +45,14 @@
     const vector<std::string>& args) {
   ASSERT(!args.empty());
 
-  if (GetMap().find(args[0]) == GetMap().end()) {
+  auto device = GetMap().find(args[0]);
+
+  if (device == GetMap().end()) {
     LOG_WARN("No constructor registered for %s", args[0].c_str());
     return std::shared_ptr<Device>(nullptr);
   }
 
-  std::shared_ptr<Device> new_device = GetMap()[args[0]]();
-  if (new_device != nullptr) new_device->Initialize(args);
-
-  return new_device;
+  return device->second(args);
 }
 
 }  // namespace rootcanal
diff --git a/tools/rootcanal/model/setup/device_boutique.h b/tools/rootcanal/model/setup/device_boutique.h
index a07b48c..562003c 100644
--- a/tools/rootcanal/model/setup/device_boutique.h
+++ b/tools/rootcanal/model/setup/device_boutique.h
@@ -34,14 +34,16 @@
 
   // Register a constructor for a device type.
   static bool Register(const std::string& device_type,
-                       const std::function<std::shared_ptr<Device>()> method);
+                       const std::function<std::shared_ptr<Device>(
+                           const std::vector<std::string>&)>
+                           method);
 
-  // Call the constructor that matches arg[0], then call dev->Initialize(args).
+  // Call the function that matches arg[0] with args
   static std::shared_ptr<Device> Create(const std::vector<std::string>& args);
 
  private:
-  static std::unordered_map<std::string,
-                            std::function<std::shared_ptr<Device>()>>&
+  static std::unordered_map<std::string, std::function<std::shared_ptr<Device>(
+                                             const std::vector<std::string>&)>>&
   GetMap();
 };
 
diff --git a/tools/rootcanal/model/setup/test_model.cc b/tools/rootcanal/model/setup/test_model.cc
index c2ad281..2746180 100644
--- a/tools/rootcanal/model/setup/test_model.cc
+++ b/tools/rootcanal/model/setup/test_model.cc
@@ -173,13 +173,12 @@
 
 void TestModel::AddHciConnection(std::shared_ptr<HciDevice> dev) {
   size_t index = Add(std::static_pointer_cast<Device>(dev));
-  std::string addr = "da:4c:10:de:17:";  // Da HCI dev
-  std::stringstream stream;
-  stream << std::setfill('0') << std::setw(2) << std::hex << (index % 256);
-  addr += stream.str();
 
-  dev->Initialize({"IgnoredTypeName", addr});
-  LOG_INFO("initialized %s", addr.c_str());
+  uint8_t raw[] = {0xda, 0x4c, 0x10, 0xde, 0x17, uint8_t(index)};  // Da HCI dev
+  auto addr = Address{{raw[5], raw[4], raw[3], raw[2], raw[1], raw[0]}};
+  dev->SetAddress(addr);
+
+  LOG_INFO("initialized %s", addr.ToString().c_str());
   for (size_t i = 0; i < phys_.size(); i++) {
     AddDeviceToPhy(index, i);
   }