Merge "Factor out duplicated warning options"
diff --git a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapMethodProxy.java b/android/app/src/com/android/bluetooth/BluetoothMethodProxy.java
similarity index 83%
rename from android/app/src/com/android/bluetooth/pbap/BluetoothPbapMethodProxy.java
rename to android/app/src/com/android/bluetooth/BluetoothMethodProxy.java
index 8846e60..5710451 100644
--- a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapMethodProxy.java
+++ b/android/app/src/com/android/bluetooth/BluetoothMethodProxy.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.bluetooth.pbap;
+package com.android.bluetooth;
 
 import android.content.ContentResolver;
 import android.content.Context;
@@ -22,7 +22,6 @@
 import android.net.Uri;
 import android.util.Log;
 
-import com.android.bluetooth.Utils;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.obex.HeaderSet;
 
@@ -31,22 +30,22 @@
 /**
  * Proxy class for method calls to help with unit testing
  */
-public class BluetoothPbapMethodProxy {
-    private static final String TAG = BluetoothPbapMethodProxy.class.getSimpleName();
-    private static BluetoothPbapMethodProxy sInstance;
+public class BluetoothMethodProxy {
+    private static final String TAG = BluetoothMethodProxy.class.getSimpleName();
+    private static BluetoothMethodProxy sInstance;
     private static final Object INSTANCE_LOCK = new Object();
 
-    private BluetoothPbapMethodProxy() {}
+    private BluetoothMethodProxy() {}
 
     /**
      * Get the singleton instance of proxy
      *
      * @return the singleton instance, guaranteed not null
      */
-    public static BluetoothPbapMethodProxy getInstance() {
+    public static BluetoothMethodProxy getInstance() {
         synchronized (INSTANCE_LOCK) {
             if (sInstance == null) {
-                sInstance = new BluetoothPbapMethodProxy();
+                sInstance = new BluetoothMethodProxy();
             }
         }
         return sInstance;
@@ -58,7 +57,7 @@
      * @param proxy a test instance of the BluetoothPbapMethodCallProxy
      */
     @VisibleForTesting
-    public static void setInstanceForTesting(BluetoothPbapMethodProxy proxy) {
+    public static void setInstanceForTesting(BluetoothMethodProxy proxy) {
         Utils.enforceInstrumentationTestMode();
         synchronized (INSTANCE_LOCK) {
             Log.d(TAG, "setInstanceForTesting(), set to " + proxy);
diff --git a/android/app/src/com/android/bluetooth/btservice/AdapterService.java b/android/app/src/com/android/bluetooth/btservice/AdapterService.java
index c3f04a1..fda23c9 100644
--- a/android/app/src/com/android/bluetooth/btservice/AdapterService.java
+++ b/android/app/src/com/android/bluetooth/btservice/AdapterService.java
@@ -5247,6 +5247,7 @@
     }
 
     // Boolean flags
+    private static final String SDP_SERIALIZATION_FLAG = "INIT_sdp_serialization";
     private static final String GD_CORE_FLAG = "INIT_gd_core";
     private static final String GD_ADVERTISING_FLAG = "INIT_gd_advertising";
     private static final String GD_SCANNING_FLAG = "INIT_gd_scanning";
@@ -5285,6 +5286,10 @@
         if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH, GD_CORE_FLAG, false)) {
             initFlags.add(String.format("%s=%s", GD_CORE_FLAG, "true"));
         }
+        if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH,
+                    SDP_SERIALIZATION_FLAG, true)) {
+            initFlags.add(String.format("%s=%s", SDP_SERIALIZATION_FLAG, "true"));
+        }
         if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH, GD_ADVERTISING_FLAG, false)) {
             initFlags.add(String.format("%s=%s", GD_ADVERTISING_FLAG, "true"));
         }
diff --git a/android/app/src/com/android/bluetooth/csip/CsipSetCoordinatorService.java b/android/app/src/com/android/bluetooth/csip/CsipSetCoordinatorService.java
index f85dbbc..172383e 100644
--- a/android/app/src/com/android/bluetooth/csip/CsipSetCoordinatorService.java
+++ b/android/app/src/com/android/bluetooth/csip/CsipSetCoordinatorService.java
@@ -598,6 +598,24 @@
     }
 
     /**
+     * Get group ID for a given device and UUID
+     * @param device potential group member
+     * @param uuid profile context UUID
+     * @return group ID
+     */
+    public Integer getGroupId(BluetoothDevice device, ParcelUuid uuid) {
+        Map<Integer, Integer> device_groups =
+                mDeviceGroupIdRankMap.getOrDefault(device, new HashMap<>());
+        return mGroupIdToUuidMap.entrySet()
+                .stream()
+                .filter(e -> (device_groups.containsKey(e.getKey())
+                        && e.getValue().equals(uuid)))
+                .map(Map.Entry::getKey)
+                .findFirst()
+                .orElse(IBluetoothCsipSetCoordinator.CSIS_GROUP_ID_INVALID);
+    }
+
+    /**
      * Get device's groups/
      * @param device group member device
      * @return map of group id and related uuids.
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 c6d3b0f..729aaf1 100644
--- a/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java
+++ b/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java
@@ -93,6 +93,11 @@
     private static LeAudioService sLeAudioService;
 
     /**
+     * Indicates group audio support for none direction
+     */
+    private static final int AUDIO_DIRECTION_NONE = 0x00;
+
+    /**
      * Indicates group audio support for input direction
      */
     private static final int AUDIO_DIRECTION_INPUT_BIT = 0x01;
@@ -102,11 +107,6 @@
      */
     private static final int AUDIO_DIRECTION_OUTPUT_BIT = 0x02;
 
-    /*
-     * Indicates no active contexts
-     */
-    private static final int ACTIVE_CONTEXTS_NONE = 0;
-
     private AdapterService mAdapterService;
     private DatabaseManager mDatabaseManager;
     private HandlerThread mStateMachinesThread;
@@ -137,14 +137,14 @@
         LeAudioGroupDescriptor() {
             mIsConnected = false;
             mIsActive = false;
-            mActiveContexts = ACTIVE_CONTEXTS_NONE;
+            mDirection = AUDIO_DIRECTION_NONE;
             mCodecStatus = null;
             mLostLeadDeviceWhileStreaming = null;
         }
 
         public Boolean mIsConnected;
         public Boolean mIsActive;
-        public Integer mActiveContexts;
+        public Integer mDirection;
         public BluetoothLeAudioCodecStatus mCodecStatus;
         /* This can be non empty only for the streaming time */
         BluetoothDevice mLostLeadDeviceWhileStreaming;
@@ -161,21 +161,6 @@
     private final Map<BluetoothDevice, Integer> mDeviceGroupIdMap = new ConcurrentHashMap<>();
     private final Map<BluetoothDevice, Integer> mDeviceAudioLocationMap = new ConcurrentHashMap<>();
 
-    private final int mContextSupportingInputAudio = BluetoothLeAudio.CONTEXT_TYPE_CONVERSATIONAL
-            | BluetoothLeAudio.CONTEXT_TYPE_VOICE_ASSISTANTS;
-
-    private final int mContextSupportingOutputAudio = BluetoothLeAudio.CONTEXT_TYPE_CONVERSATIONAL
-            | BluetoothLeAudio.CONTEXT_TYPE_MEDIA
-            | BluetoothLeAudio.CONTEXT_TYPE_GAME
-            | BluetoothLeAudio.CONTEXT_TYPE_INSTRUCTIONAL
-            | BluetoothLeAudio.CONTEXT_TYPE_VOICE_ASSISTANTS
-            | BluetoothLeAudio.CONTEXT_TYPE_LIVE
-            | BluetoothLeAudio.CONTEXT_TYPE_SOUND_EFFECTS
-            | BluetoothLeAudio.CONTEXT_TYPE_NOTIFICATIONS
-            | BluetoothLeAudio.CONTEXT_TYPE_RINGTONE
-            | BluetoothLeAudio.CONTEXT_TYPE_ALERTS
-            | BluetoothLeAudio.CONTEXT_TYPE_EMERGENCY_ALARM;
-
     private BroadcastReceiver mBondStateChangedReceiver;
     private BroadcastReceiver mConnectionStateChangedReceiver;
     private BroadcastReceiver mMuteStateChangedReceiver;
@@ -320,8 +305,8 @@
                 Integer group_id = entry.getKey();
                 if (descriptor.mIsActive) {
                     descriptor.mIsActive = false;
-                    updateActiveDevices(group_id, descriptor.mActiveContexts,
-                            ACTIVE_CONTEXTS_NONE, descriptor.mIsActive);
+                    updateActiveDevices(group_id, descriptor.mDirection, AUDIO_DIRECTION_NONE,
+                            descriptor.mIsActive);
                     break;
                 }
             }
@@ -624,32 +609,6 @@
         return result;
     }
 
-    /**
-     * Get supported group audio direction from available context.
-     *
-     * @param activeContexts bitset of active context to be matched with possible audio direction
-     * support.
-     * @return matched possible audio direction support masked bitset
-     * {@link #AUDIO_DIRECTION_INPUT_BIT} if input audio is supported
-     * {@link #AUDIO_DIRECTION_OUTPUT_BIT} if output audio is supported
-     */
-    private Integer getAudioDirectionsFromActiveContextsMap(Integer activeContexts) {
-        Integer supportedAudioDirections = 0;
-
-        if (((activeContexts & mContextSupportingInputAudio) != 0)
-                || (Utils.isPtsTestMode()
-                && (activeContexts
-                & (BluetoothLeAudio.CONTEXT_TYPE_RINGTONE
-                | BluetoothLeAudio.CONTEXT_TYPE_MEDIA)) != 0)) {
-            supportedAudioDirections |= AUDIO_DIRECTION_INPUT_BIT;
-        }
-        if ((activeContexts & mContextSupportingOutputAudio) != 0) {
-            supportedAudioDirections |= AUDIO_DIRECTION_OUTPUT_BIT;
-        }
-
-        return supportedAudioDirections;
-    }
-
     private Integer getActiveGroupId() {
         synchronized (mGroupLock) {
             for (Map.Entry<Integer, LeAudioGroupDescriptor> entry : mGroupDescriptors.entrySet()) {
@@ -801,12 +760,7 @@
     }
 
     private boolean updateActiveInDevice(BluetoothDevice device, Integer groupId,
-            Integer oldActiveContexts, Integer newActiveContexts) {
-        Integer oldSupportedAudioDirections =
-                getAudioDirectionsFromActiveContextsMap(oldActiveContexts);
-        Integer newSupportedAudioDirections =
-                getAudioDirectionsFromActiveContextsMap(newActiveContexts);
-
+            Integer oldSupportedAudioDirections, Integer newSupportedAudioDirections) {
         boolean oldSupportedByDeviceInput = (oldSupportedAudioDirections
                 & AUDIO_DIRECTION_INPUT_BIT) != 0;
         boolean newSupportedByDeviceInput = (newSupportedAudioDirections
@@ -866,12 +820,7 @@
     }
 
     private boolean updateActiveOutDevice(BluetoothDevice device, Integer groupId,
-            Integer oldActiveContexts, Integer newActiveContexts) {
-        Integer oldSupportedAudioDirections =
-                getAudioDirectionsFromActiveContextsMap(oldActiveContexts);
-        Integer newSupportedAudioDirections =
-                getAudioDirectionsFromActiveContextsMap(newActiveContexts);
-
+            Integer oldSupportedAudioDirections, Integer newSupportedAudioDirections) {
         boolean oldSupportedByDeviceOutput = (oldSupportedAudioDirections
                 & AUDIO_DIRECTION_OUTPUT_BIT) != 0;
         boolean newSupportedByDeviceOutput = (newSupportedAudioDirections
@@ -941,13 +890,13 @@
     /**
      * Report the active devices change to the active device manager and the media framework.
      * @param groupId id of group which devices should be updated
-     * @param newActiveContexts new active contexts for group of devices
-     * @param oldActiveContexts old active contexts for group of devices
+     * @param newSupportedAudioDirections new supported audio directions for group of devices
+     * @param oldSupportedAudioDirections old supported audio directions for group of devices
      * @param isActive if there is new active group
      * @return true if group is active after change false otherwise.
      */
-    private boolean updateActiveDevices(Integer groupId, Integer oldActiveContexts,
-            Integer newActiveContexts, boolean isActive) {
+    private boolean updateActiveDevices(Integer groupId, Integer oldSupportedAudioDirections,
+            Integer newSupportedAudioDirections, boolean isActive) {
         BluetoothDevice device = null;
 
         if (isActive) {
@@ -955,9 +904,11 @@
         }
 
         boolean outReplaced =
-                updateActiveOutDevice(device, groupId, oldActiveContexts, newActiveContexts);
+                updateActiveOutDevice(device, groupId, oldSupportedAudioDirections,
+                        newSupportedAudioDirections);
         boolean inReplaced =
-                updateActiveInDevice(device, groupId, oldActiveContexts, newActiveContexts);
+                updateActiveInDevice(device, groupId, oldSupportedAudioDirections,
+                        newSupportedAudioDirections);
 
         if (outReplaced || inReplaced) {
             Intent intent = new Intent(BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED);
@@ -1149,8 +1100,8 @@
                 return;
             }
 
-            descriptor.mIsActive = updateActiveDevices(groupId,
-                            ACTIVE_CONTEXTS_NONE, descriptor.mActiveContexts, true);
+            descriptor.mIsActive = updateActiveDevices(groupId, AUDIO_DIRECTION_NONE,
+                    descriptor.mDirection, true);
 
             if (descriptor.mIsActive) {
                 notifyGroupStatusChanged(groupId, LeAudioStackEvent.GROUP_STATUS_ACTIVE);
@@ -1167,8 +1118,8 @@
             }
 
             descriptor.mIsActive = false;
-            updateActiveDevices(groupId, descriptor.mActiveContexts,
-                    ACTIVE_CONTEXTS_NONE, descriptor.mIsActive);
+            updateActiveDevices(groupId, descriptor.mDirection, AUDIO_DIRECTION_NONE,
+                    descriptor.mIsActive);
             /* Clear lost devices */
             if (DBG) Log.d(TAG, "Clear for group: " + groupId);
             clearLostDevicesWhileStreaming(descriptor);
@@ -1342,13 +1293,13 @@
             if (descriptor != null) {
                 if (descriptor.mIsActive) {
                     descriptor.mIsActive =
-                            updateActiveDevices(groupId, descriptor.mActiveContexts,
-                                    available_contexts, descriptor.mIsActive);
+                            updateActiveDevices(groupId, descriptor.mDirection, direction,
+                            descriptor.mIsActive);
                     if (!descriptor.mIsActive) {
                         notifyGroupStatusChanged(groupId, BluetoothLeAudio.GROUP_STATUS_INACTIVE);
                     }
                 }
-                descriptor.mActiveContexts = available_contexts;
+                descriptor.mDirection = direction;
             } else {
                 Log.e(TAG, "no descriptors for group: " + groupId);
             }
@@ -1669,8 +1620,8 @@
                     descriptor.mIsActive = false;
                     /* Update audio framework */
                     updateActiveDevices(myGroupId,
-                            descriptor.mActiveContexts,
-                            descriptor.mActiveContexts,
+                            descriptor.mDirection,
+                            descriptor.mDirection,
                             descriptor.mIsActive);
                     return;
                 }
@@ -1678,8 +1629,8 @@
 
             if (descriptor.mIsActive) {
                 updateActiveDevices(myGroupId,
-                        descriptor.mActiveContexts,
-                        descriptor.mActiveContexts,
+                        descriptor.mDirection,
+                        descriptor.mDirection,
                         descriptor.mIsActive);
             }
         }
@@ -1933,6 +1884,13 @@
     }
 
     private void notifyGroupNodeAdded(BluetoothDevice device, int groupId) {
+        if (mVolumeControlService == null) {
+            mVolumeControlService = mServiceFactory.getVolumeControlService();
+        }
+        if (mVolumeControlService != null) {
+            mVolumeControlService.handleGroupNodeAdded(groupId, device);
+        }
+
         if (mLeAudioCallbacks != null) {
             int n = mLeAudioCallbacks.beginBroadcast();
             for (int i = 0; i < n; i++) {
@@ -2796,7 +2754,7 @@
             ProfileService.println(sb, "  Group: " + groupId);
             ProfileService.println(sb, "    isActive: " + descriptor.mIsActive);
             ProfileService.println(sb, "    isConnected: " + descriptor.mIsConnected);
-            ProfileService.println(sb, "    mActiveContexts: " + descriptor.mActiveContexts);
+            ProfileService.println(sb, "    mDirection: " + descriptor.mDirection);
             ProfileService.println(sb, "    group lead: " + getConnectedGroupLeadDevice(groupId));
             ProfileService.println(sb, "    first device: " + getFirstDeviceFromGroup(groupId));
             ProfileService.println(sb, "    lost lead device: "
diff --git a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposer.java b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposer.java
index 3f37aac..123f145 100644
--- a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposer.java
+++ b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposer.java
@@ -24,6 +24,7 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import com.android.bluetooth.BluetoothMethodProxy;
 import com.android.bluetooth.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.vcard.VCardBuilder;
@@ -107,7 +108,7 @@
             return false;
         }
 
-        mCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(
+        mCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(
                 mContext.getContentResolver(), contentUri, projection, selection, selectionArgs,
                 sortOrder);
 
diff --git a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java
index 1353152..e64772c 100644
--- a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java
+++ b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java
@@ -43,6 +43,7 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import com.android.bluetooth.BluetoothMethodProxy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.obex.ApplicationParameter;
 import com.android.obex.HeaderSet;
@@ -242,7 +243,7 @@
 
     private PbapStateMachine mStateMachine;
 
-    private BluetoothPbapMethodProxy mPbapMethodProxy;
+    private BluetoothMethodProxy mPbapMethodProxy;
 
     private enum ContactsType {
         TYPE_PHONEBOOK , TYPE_SIM ;
@@ -272,7 +273,7 @@
         mVcardManager = new BluetoothPbapVcardManager(mContext);
         mVcardSimManager = new BluetoothPbapSimVcardManager(mContext);
         mStateMachine = stateMachine;
-        mPbapMethodProxy = BluetoothPbapMethodProxy.getInstance();
+        mPbapMethodProxy = BluetoothMethodProxy.getInstance();
     }
 
     @Override
diff --git a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManager.java b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManager.java
index 5019169..22eac64 100644
--- a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManager.java
+++ b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManager.java
@@ -15,41 +15,35 @@
 */
 package com.android.bluetooth.pbap;
 
-import com.android.bluetooth.R;
-
 import android.content.ContentResolver;
+import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteException;
 import android.net.Uri;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.vcard.VCardBuilder;
-import com.android.vcard.VCardConfig;
-import com.android.vcard.VCardConstants;
-import com.android.vcard.VCardUtils;
-
-import android.content.ContentValues;
-import android.provider.CallLog;
-import android.provider.CallLog.Calls;
-import android.text.TextUtils;
-import android.util.Log;
-import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.CommonDataKinds;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Contacts;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.bluetooth.BluetoothMethodProxy;
+import com.android.bluetooth.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.obex.Operation;
+import com.android.obex.ResponseCodes;
+import com.android.obex.ServerOperation;
+import com.android.vcard.VCardBuilder;
+import com.android.vcard.VCardConfig;
+import com.android.vcard.VCardUtils;
 
 import java.io.IOException;
 import java.io.OutputStream;
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
 import java.util.Collections;
 import java.util.Comparator;
-
-import com.android.obex.Operation;
-import com.android.obex.ResponseCodes;
-import com.android.obex.ServerOperation;
+import java.util.List;
 
 /**
  * VCard composer especially for Call Log used in Bluetooth.
@@ -119,7 +113,7 @@
         }
 
         //checkpoint Figure out if we can apply selection, projection and sort order.
-        mCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mContentResolver,
+        mCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mContentResolver,
                 contentUri, SIM_PROJECTION, null, null, sortOrder);
 
         if (mCursor == null) {
@@ -273,7 +267,7 @@
         int size = 0;
         Cursor contactCursor = null;
         try {
-            contactCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(
+            contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(
                     mContentResolver, SIM_URI, SIM_PROJECTION, null,null, null);
             if (contactCursor != null) {
                 size = contactCursor.getCount();
@@ -293,7 +287,7 @@
         ArrayList<String> allnames = new ArrayList<String>();
         Cursor contactCursor = null;
         try {
-            contactCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(
+            contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(
                     mContentResolver, SIM_URI, SIM_PROJECTION, null,null,null);
             if (contactCursor != null) {
                 for (contactCursor.moveToFirst(); !contactCursor.isAfterLast(); contactCursor
@@ -334,7 +328,7 @@
         Cursor contactCursor = null;
 
         try {
-            contactCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(
+            contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(
                     mContentResolver, SIM_URI, SIM_PROJECTION, null, null, null);
 
             if (contactCursor != null) {
diff --git a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java
index 14e2647..658b82a 100644
--- a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java
+++ b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java
@@ -51,6 +51,7 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import com.android.bluetooth.BluetoothMethodProxy;
 import com.android.bluetooth.R;
 import com.android.bluetooth.util.DevicePolicyUtils;
 import com.android.internal.annotations.VisibleForTesting;
@@ -182,7 +183,7 @@
             selectionClause = Phone.STARRED + " = 1";
         }
         try {
-            contactCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver,
+            contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver,
                     myUri, new String[]{Phone.CONTACT_ID}, selectionClause,
                     null, Phone.CONTACT_ID);
             if (contactCursor == null) {
@@ -209,7 +210,7 @@
         int size = 0;
         Cursor callCursor = null;
         try {
-            callCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver,
+            callCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver,
                     myUri, null, selection, null, CallLog.Calls.DEFAULT_SORT_ORDER);
             if (callCursor != null) {
                 size = callCursor.getCount();
@@ -243,7 +244,7 @@
         Cursor callCursor = null;
         ArrayList<String> list = new ArrayList<String>();
         try {
-            callCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver,
+            callCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver,
                     myUri, projection, selection, null, CALLLOG_SORT_ORDER);
             if (callCursor != null) {
                 for (callCursor.moveToFirst(); !callCursor.isAfterLast(); callCursor.moveToNext()) {
@@ -295,7 +296,7 @@
             if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) {
                 orderBy = Phone.DISPLAY_NAME;
             }
-            contactCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver,
+            contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver,
                     myUri, PHONES_CONTACTS_PROJECTION, null, null, orderBy);
             if (contactCursor != null) {
                 appendDistinctNameIdList(nameList, mContext.getString(android.R.string.unknownName),
@@ -354,7 +355,7 @@
         final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext);
         Cursor contactCursor = null;
         try {
-            contactCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver,
+            contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver,
                     myUri, PHONES_CONTACTS_PROJECTION, null, null,
                     Phone.CONTACT_ID);
 
@@ -443,7 +444,7 @@
         }
 
         try {
-            contactCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver,
+            contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver,
                     uri, projection, null, null, Phone.CONTACT_ID);
 
             if (contactCursor != null) {
@@ -478,7 +479,7 @@
         long primaryVcMsb = 0;
         ArrayList<String> list = new ArrayList<String>();
         try {
-            callCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver,
+            callCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver,
                     myUri, null, selection, null, null);
             while (callCursor != null && callCursor.moveToNext()) {
                 count = count + 1;
@@ -522,7 +523,7 @@
         long endPointId = 0;
         try {
             // Need test to see if order by _ID is ok here, or by date?
-            callsCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver,
+            callsCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver,
                     myUri, CALLLOG_PROJECTION, typeSelection, null,
                     CALLLOG_SORT_ORDER);
             if (callsCursor != null) {
@@ -596,7 +597,7 @@
         }
 
         try {
-            contactCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver,
+            contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver,
                     myUri, PHONES_CONTACTS_PROJECTION, selectionClause,
                     null, Phone.CONTACT_ID);
             if (contactCursor != null) {
@@ -640,7 +641,7 @@
             if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) {
                 orderBy = Phone.DISPLAY_NAME;
             }
-            contactCursor = BluetoothPbapMethodProxy.getInstance().contentResolverQuery(mResolver,
+            contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mResolver,
                     myUri, PHONES_CONTACTS_PROJECTION, null, null, orderBy);
         } catch (CursorWindowAllocationException e) {
             Log.e(TAG, "CursorWindowAllocationException while composing phonebook one vcard");
diff --git a/android/app/src/com/android/bluetooth/vc/VolumeControlService.java b/android/app/src/com/android/bluetooth/vc/VolumeControlService.java
index e5cd625..d1e5fd0 100644
--- a/android/app/src/com/android/bluetooth/vc/VolumeControlService.java
+++ b/android/app/src/com/android/bluetooth/vc/VolumeControlService.java
@@ -26,6 +26,7 @@
 import android.bluetooth.BluetoothProfile;
 import android.bluetooth.BluetoothUuid;
 import android.bluetooth.BluetoothVolumeControl;
+import android.bluetooth.IBluetoothCsipSetCoordinator;
 import android.bluetooth.IBluetoothLeAudio;
 import android.bluetooth.IBluetoothVolumeControl;
 import android.bluetooth.IBluetoothVolumeControlCallback;
@@ -47,6 +48,7 @@
 import com.android.bluetooth.btservice.ProfileService;
 import com.android.bluetooth.btservice.ServiceFactory;
 import com.android.bluetooth.btservice.storage.DatabaseManager;
+import com.android.bluetooth.csip.CsipSetCoordinatorService;
 import com.android.bluetooth.le_audio.LeAudioService;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.modules.utils.SynchronousResultReceiver;
@@ -193,7 +195,8 @@
     private BroadcastReceiver mBondStateChangedReceiver;
     private BroadcastReceiver mConnectionStateChangedReceiver;
 
-    private final ServiceFactory mFactory = new ServiceFactory();
+    @VisibleForTesting
+    ServiceFactory mFactory = new ServiceFactory();
 
     public static boolean isEnabled() {
         return BluetoothProperties.isProfileVcpControllerEnabled().orElse(false);
@@ -632,6 +635,29 @@
         mVolumeControlNativeInterface.unmuteGroup(groupId);
     }
 
+    /**
+     * {@hide}
+     */
+    public void handleGroupNodeAdded(int groupId, BluetoothDevice device) {
+        // Ignore disconnected device, its volume will be set once it connects
+        synchronized (mStateMachines) {
+            VolumeControlStateMachine sm = mStateMachines.get(device);
+            if (sm == null) {
+                return;
+            }
+            if (sm.getConnectionState() != BluetoothProfile.STATE_CONNECTED) {
+                return;
+            }
+        }
+
+        // If group volume has already changed, the new group member should set it
+        Integer groupVolume = mGroupVolumeCache.getOrDefault(groupId,
+                IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME);
+        if (groupVolume != IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME) {
+            mVolumeControlNativeInterface.setVolume(device, groupVolume);
+        }
+    }
+
     void handleVolumeControlChanged(BluetoothDevice device, int groupId,
                                     int volume, boolean mute, boolean isAutonomous) {
 
@@ -974,6 +1000,17 @@
                 }
                 removeStateMachine(device);
             }
+        } else if (toState == BluetoothProfile.STATE_CONNECTED) {
+            // Restore the group volume if it was changed while the device was not yet connected.
+            CsipSetCoordinatorService csipClient = mFactory.getCsipSetCoordinatorService();
+            Integer groupId = csipClient.getGroupId(device, BluetoothUuid.CAP);
+            if (groupId != IBluetoothCsipSetCoordinator.CSIS_GROUP_ID_INVALID) {
+                Integer groupVolume = mGroupVolumeCache.getOrDefault(groupId,
+                        IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME);
+                if (groupVolume != IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME) {
+                    mVolumeControlNativeInterface.setVolume(device, groupVolume);
+                }
+            }
         }
     }
 
diff --git a/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapConvoListingElementTest.java b/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapConvoListingElementTest.java
new file mode 100644
index 0000000..84b0d7f
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapConvoListingElementTest.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.map;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.SignedLongLong;
+import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
+import com.android.internal.util.FastXmlSerializer;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class BluetoothMapConvoListingElementTest {
+    private static final long TEST_ID = 1111;
+    private static final String TEST_NAME = "test_name";
+    private static final long TEST_LAST_ACTIVITY = 0;
+    private static final boolean TEST_READ = true;
+    private static final boolean TEST_REPORT_READ = true;
+    private static final long TEST_VERSION_COUNTER = 0;
+    private static final int TEST_CURSOR_INDEX = 1;
+    private static final TYPE TEST_TYPE = TYPE.EMAIL;
+    private static final String TEST_SUMMARY = "test_summary";
+    private static final String TEST_SMS_MMS_CONTACTS = "test_sms_mms_contacts";
+
+    private final BluetoothMapConvoContactElement TEST_CONTACT_ELEMENT_ONE =
+            new BluetoothMapConvoContactElement("test_uci_one", "test_name_one",
+                    "test_display_name_one", "test_presence_status_one", 2, TEST_LAST_ACTIVITY, 2,
+                    1, "1111");
+
+    private final BluetoothMapConvoContactElement TEST_CONTACT_ELEMENT_TWO =
+            new BluetoothMapConvoContactElement("test_uci_two", "test_name_two",
+                    "test_display_name_two", "test_presence_status_two", 1, TEST_LAST_ACTIVITY, 1,
+                    2, "1112");
+
+    private final List<BluetoothMapConvoContactElement> TEST_CONTACTS = new ArrayList<>(
+            Arrays.asList(TEST_CONTACT_ELEMENT_ONE, TEST_CONTACT_ELEMENT_TWO));
+
+    private final SignedLongLong signedLongLong = new SignedLongLong(TEST_ID, 0);
+
+    private BluetoothMapConvoListingElement mListingElement;
+
+    @Before
+    public void setUp() throws Exception {
+        mListingElement = new BluetoothMapConvoListingElement();
+
+        mListingElement.setCursorIndex(TEST_CURSOR_INDEX);
+        mListingElement.setVersionCounter(TEST_VERSION_COUNTER);
+        mListingElement.setName(TEST_NAME);
+        mListingElement.setType(TEST_TYPE);
+        mListingElement.setContacts(TEST_CONTACTS);
+        mListingElement.setLastActivity(TEST_LAST_ACTIVITY);
+        mListingElement.setRead(TEST_READ, TEST_REPORT_READ);
+        mListingElement.setConvoId(0, TEST_ID);
+        mListingElement.setSummary(TEST_SUMMARY);
+        mListingElement.setSmsMmsContacts(TEST_SMS_MMS_CONTACTS);
+    }
+
+    @Test
+    public void getters() throws Exception {
+        assertThat(mListingElement.getCursorIndex()).isEqualTo(TEST_CURSOR_INDEX);
+        assertThat(mListingElement.getVersionCounter()).isEqualTo(TEST_VERSION_COUNTER);
+        assertThat(mListingElement.getName()).isEqualTo(TEST_NAME);
+        assertThat(mListingElement.getType()).isEqualTo(TEST_TYPE);
+        assertThat(mListingElement.getContacts()).isEqualTo(TEST_CONTACTS);
+        assertThat(mListingElement.getLastActivity()).isEqualTo(TEST_LAST_ACTIVITY);
+        assertThat(mListingElement.getRead()).isEqualTo("READ");
+        assertThat(mListingElement.getReadBool()).isEqualTo(TEST_READ);
+        assertThat(mListingElement.getConvoId()).isEqualTo(signedLongLong.toHexString());
+        assertThat(mListingElement.getCpConvoId()).isEqualTo(
+                signedLongLong.getLeastSignificantBits());
+        assertThat(mListingElement.getFullSummary()).isEqualTo(TEST_SUMMARY);
+        assertThat(mListingElement.getSmsMmsContacts()).isEqualTo(TEST_SMS_MMS_CONTACTS);
+    }
+
+    @Test
+    public void incrementVersionCounter() {
+        mListingElement.incrementVersionCounter();
+        assertThat(mListingElement.getVersionCounter()).isEqualTo(TEST_VERSION_COUNTER + 1);
+    }
+
+    @Test
+    public void removeContactWithObject() {
+        mListingElement.removeContact(TEST_CONTACT_ELEMENT_TWO);
+        assertThat(mListingElement.getContacts().size()).isEqualTo(1);
+    }
+
+    @Test
+    public void removeContactWithIndex() {
+        mListingElement.removeContact(1);
+        assertThat(mListingElement.getContacts().size()).isEqualTo(1);
+    }
+
+    @Test
+    public void encodeToXml_thenDecodeToInstance_returnsCorrectly() throws Exception {
+        final XmlSerializer serializer = new FastXmlSerializer();
+        final StringWriter writer = new StringWriter();
+
+        serializer.setOutput(writer);
+        serializer.startDocument("UTF-8", true);
+        mListingElement.encode(serializer);
+        serializer.endDocument();
+
+        final XmlPullParserFactory parserFactory = XmlPullParserFactory.newInstance();
+        parserFactory.setNamespaceAware(true);
+        final XmlPullParser parser;
+        parser = parserFactory.newPullParser();
+
+        parser.setInput(new StringReader(writer.toString()));
+        parser.next();
+
+        BluetoothMapConvoListingElement listingElementFromXml =
+                BluetoothMapConvoListingElement.createFromXml(parser);
+
+        assertThat(listingElementFromXml.getVersionCounter()).isEqualTo(0);
+        assertThat(listingElementFromXml.getName()).isEqualTo(TEST_NAME);
+        assertThat(listingElementFromXml.getContacts()).isEqualTo(TEST_CONTACTS);
+        assertThat(listingElementFromXml.getLastActivity()).isEqualTo(TEST_LAST_ACTIVITY);
+        assertThat(listingElementFromXml.getRead()).isEqualTo("UNREAD");
+        assertThat(listingElementFromXml.getConvoId()).isEqualTo(signedLongLong.toHexString());
+        assertThat(listingElementFromXml.getFullSummary().trim()).isEqualTo(TEST_SUMMARY);
+    }
+
+    @Test
+    public void equalsWithSameValues_returnsTrue() {
+        BluetoothMapConvoListingElement listingElement = new BluetoothMapConvoListingElement();
+        listingElement.setName(TEST_NAME);
+        listingElement.setContacts(TEST_CONTACTS);
+        listingElement.setLastActivity(TEST_LAST_ACTIVITY);
+        listingElement.setRead(TEST_READ, TEST_REPORT_READ);
+
+        BluetoothMapConvoListingElement listingElementEqual = new BluetoothMapConvoListingElement();
+        listingElementEqual.setName(TEST_NAME);
+        listingElementEqual.setContacts(TEST_CONTACTS);
+        listingElementEqual.setLastActivity(TEST_LAST_ACTIVITY);
+        listingElementEqual.setRead(TEST_READ, TEST_REPORT_READ);
+
+        assertThat(listingElement).isEqualTo(listingElementEqual);
+    }
+
+    @Test
+    public void equalsWithDifferentRead_returnsFalse() {
+        BluetoothMapConvoListingElement
+                listingElement = new BluetoothMapConvoListingElement();
+
+        BluetoothMapConvoListingElement listingElementWithDifferentRead =
+                new BluetoothMapConvoListingElement();
+        listingElementWithDifferentRead.setRead(TEST_READ, TEST_REPORT_READ);
+
+        assertThat(listingElement).isNotEqualTo(listingElementWithDifferentRead);
+    }
+
+    @Test
+    public void compareToWithSameValues_returnsZero() {
+        BluetoothMapConvoListingElement
+                listingElement = new BluetoothMapConvoListingElement();
+        listingElement.setLastActivity(TEST_LAST_ACTIVITY);
+
+        BluetoothMapConvoListingElement listingElementSameLastActivity =
+                new BluetoothMapConvoListingElement();
+        listingElementSameLastActivity.setLastActivity(TEST_LAST_ACTIVITY);
+
+        assertThat(listingElement.compareTo(listingElementSameLastActivity)).isEqualTo(0);
+    }
+}
\ No newline at end of file
diff --git a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposerTest.java b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposerTest.java
index d8e2b62..33089f4 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposerTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposerTest.java
@@ -38,6 +38,8 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.bluetooth.BluetoothMethodProxy;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -61,7 +63,7 @@
     private BluetoothPbapCallLogComposer mComposer;
 
     @Spy
-    BluetoothPbapMethodProxy mPbapCallProxy = BluetoothPbapMethodProxy.getInstance();
+    BluetoothMethodProxy mPbapCallProxy = BluetoothMethodProxy.getInstance();
 
     @Mock
     Cursor mMockCursor;
@@ -69,7 +71,7 @@
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        BluetoothPbapMethodProxy.setInstanceForTesting(mPbapCallProxy);
+        BluetoothMethodProxy.setInstanceForTesting(mPbapCallProxy);
 
         doReturn(mMockCursor).when(mPbapCallProxy)
                 .contentResolverQuery(any(), any(), any(), any(), any(), any());
@@ -82,7 +84,7 @@
 
     @After
     public void tearDown() throws Exception {
-        BluetoothPbapMethodProxy.setInstanceForTesting(null);
+        BluetoothMethodProxy.setInstanceForTesting(null);
     }
 
     @Test
diff --git a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapObexServerTest.java b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapObexServerTest.java
index b888930..1ad0918 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapObexServerTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapObexServerTest.java
@@ -27,15 +27,14 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.content.Context;
 import android.os.Handler;
 import android.os.UserManager;
-import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.bluetooth.BluetoothMethodProxy;
 import com.android.obex.HeaderSet;
 import com.android.obex.Operation;
 import com.android.obex.ResponseCodes;
@@ -63,7 +62,7 @@
     @Mock PbapStateMachine mMockStateMachine;
 
     @Spy
-    BluetoothPbapMethodProxy mPbapMethodProxy = BluetoothPbapMethodProxy.getInstance();
+    BluetoothMethodProxy mPbapMethodProxy = BluetoothMethodProxy.getInstance();
 
     BluetoothPbapObexServer mServer;
 
@@ -85,14 +84,14 @@
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        BluetoothPbapMethodProxy.setInstanceForTesting(mPbapMethodProxy);
+        BluetoothMethodProxy.setInstanceForTesting(mPbapMethodProxy);
         mServer = new BluetoothPbapObexServer(
                 mMockHandler, InstrumentationRegistry.getTargetContext(), mMockStateMachine);
     }
 
     @After
     public void tearDown() throws Exception {
-        BluetoothPbapMethodProxy.setInstanceForTesting(null);
+        BluetoothMethodProxy.setInstanceForTesting(null);
     }
 
     @Test
diff --git a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManagerTest.java b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManagerTest.java
index 5492e1c..0230275 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManagerTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManagerTest.java
@@ -35,6 +35,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.bluetooth.BluetoothMethodProxy;
 import com.android.obex.Operation;
 import com.android.obex.ResponseCodes;
 
@@ -60,7 +61,7 @@
     private static final String TAG = BluetoothPbapSimVcardManagerTest.class.getSimpleName();
 
     @Spy
-    BluetoothPbapMethodProxy mPbapMethodProxy = BluetoothPbapMethodProxy.getInstance();
+    BluetoothMethodProxy mPbapMethodProxy = BluetoothMethodProxy.getInstance();
 
     Context mContext;
     BluetoothPbapSimVcardManager mManager;
@@ -70,14 +71,14 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        BluetoothPbapMethodProxy.setInstanceForTesting(mPbapMethodProxy);
+        BluetoothMethodProxy.setInstanceForTesting(mPbapMethodProxy);
         mContext =  InstrumentationRegistry.getTargetContext();
         mManager = new BluetoothPbapSimVcardManager(mContext);
     }
 
     @After
     public void tearDown() {
-        BluetoothPbapMethodProxy.setInstanceForTesting(null);
+        BluetoothMethodProxy.setInstanceForTesting(null);
     }
 
     @Test
diff --git a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapVcardManagerTest.java b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapVcardManagerTest.java
index b6cceb5..fa8bb5a 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapVcardManagerTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapVcardManagerTest.java
@@ -27,7 +27,6 @@
 
 import android.content.Context;
 import android.database.Cursor;
-import android.net.Uri;
 import android.provider.CallLog;
 import android.provider.ContactsContract;
 
@@ -35,6 +34,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.bluetooth.BluetoothMethodProxy;
 import com.android.bluetooth.R;
 
 import org.junit.After;
@@ -56,7 +56,7 @@
     private static final String TAG = BluetoothPbapVcardManagerTest.class.getSimpleName();
 
     @Spy
-    BluetoothPbapMethodProxy mPbapMethodProxy = BluetoothPbapMethodProxy.getInstance();
+    BluetoothMethodProxy mPbapMethodProxy = BluetoothMethodProxy.getInstance();
 
     Context mContext;
     BluetoothPbapVcardManager mManager;
@@ -64,14 +64,14 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        BluetoothPbapMethodProxy.setInstanceForTesting(mPbapMethodProxy);
+        BluetoothMethodProxy.setInstanceForTesting(mPbapMethodProxy);
         mContext = InstrumentationRegistry.getTargetContext();
         mManager = new BluetoothPbapVcardManager(mContext);
     }
 
     @After
     public void tearDown() {
-        BluetoothPbapMethodProxy.setInstanceForTesting(null);
+        BluetoothMethodProxy.setInstanceForTesting(null);
     }
 
     @Test
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 7ca7623..bdd8169 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
@@ -41,7 +41,9 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.bluetooth.btservice.AdapterService;
+import com.android.bluetooth.btservice.ServiceFactory;
 import com.android.bluetooth.btservice.storage.DatabaseManager;
+import com.android.bluetooth.csip.CsipSetCoordinatorService;
 import com.android.bluetooth.R;
 import com.android.bluetooth.TestUtils;
 import com.android.bluetooth.x.com.android.modules.utils.SynchronousResultReceiver;
@@ -73,6 +75,7 @@
     private VolumeControlService mService;
     private VolumeControlService.BluetoothVolumeControlBinder mServiceBinder;
     private BluetoothDevice mDevice;
+    private BluetoothDevice mDeviceTwo;
     private HashMap<BluetoothDevice, LinkedBlockingQueue<Intent>> mDeviceQueueMap;
     private static final int TIMEOUT_MS = 1000;
     private static final int BT_LE_AUDIO_MAX_VOL = 255;
@@ -87,6 +90,8 @@
     @Mock private DatabaseManager mDatabaseManager;
     @Mock private VolumeControlNativeInterface mNativeInterface;
     @Mock private AudioManager mAudioManager;
+    @Mock private ServiceFactory mServiceFactory;
+    @Mock private CsipSetCoordinatorService mCsipService;
 
     @Rule public final ServiceTestRule mServiceRule = new ServiceTestRule();
 
@@ -119,9 +124,12 @@
         startService();
         mService.mVolumeControlNativeInterface = mNativeInterface;
         mService.mAudioManager = mAudioManager;
+        mService.mFactory = mServiceFactory;
         mServiceBinder = (VolumeControlService.BluetoothVolumeControlBinder) mService.initBinder();
         mServiceBinder.mIsTesting = true;
 
+        doReturn(mCsipService).when(mServiceFactory).getCsipSetCoordinatorService();
+
         // Override the timeout value to speed up the test
         VolumeControlStateMachine.sConnectTimeoutMs = TIMEOUT_MS;    // 1s
 
@@ -134,8 +142,10 @@
 
         // Get a device for testing
         mDevice = TestUtils.getTestDevice(mAdapter, 0);
+        mDeviceTwo = TestUtils.getTestDevice(mAdapter, 1);
         mDeviceQueueMap = new HashMap<>();
         mDeviceQueueMap.put(mDevice, new LinkedBlockingQueue<>());
+        mDeviceQueueMap.put(mDeviceTwo, new LinkedBlockingQueue<>());
         doReturn(BluetoothDevice.BOND_BONDED).when(mAdapterService)
                 .getBondState(any(BluetoothDevice.class));
         doReturn(new ParcelUuid[]{BluetoothUuid.VOLUME_CONTROL}).when(mAdapterService)
@@ -631,6 +641,92 @@
         Assert.assertEquals(volume, mService.getGroupVolume(groupId));
     }
 
+    /**
+     * Test setting volume for a group member who connects after the volume level
+     * for a group was already changed and cached.
+     */
+    @Test
+    public void testLateConnectingDevice() throws Exception {
+        int groupId = 1;
+        int groupVolume = 56;
+
+        // Both devices are in the same group
+        when(mCsipService.getGroupId(mDevice, BluetoothUuid.CAP)).thenReturn(groupId);
+        when(mCsipService.getGroupId(mDeviceTwo, BluetoothUuid.CAP)).thenReturn(groupId);
+
+        // Update the device policy so okToConnect() returns true
+        when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+        when(mDatabaseManager
+                .getProfileConnectionPolicy(any(BluetoothDevice.class),
+                        eq(BluetoothProfile.VOLUME_CONTROL)))
+                .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+        doReturn(true).when(mNativeInterface).connectVolumeControl(any(BluetoothDevice.class));
+        doReturn(true).when(mNativeInterface).disconnectVolumeControl(any(BluetoothDevice.class));
+
+        generateConnectionMessageFromNative(mDevice, BluetoothProfile.STATE_CONNECTED,
+                BluetoothProfile.STATE_DISCONNECTED);
+        Assert.assertEquals(BluetoothProfile.STATE_CONNECTED,
+                mService.getConnectionState(mDevice));
+        Assert.assertTrue(mService.getDevices().contains(mDevice));
+
+        mService.setGroupVolume(groupId, groupVolume);
+        verify(mNativeInterface, times(1)).setGroupVolume(eq(groupId), eq(groupVolume));
+        verify(mNativeInterface, times(0)).setVolume(eq(mDeviceTwo), eq(groupVolume));
+
+        // Verify that second device gets the proper group volume level when connected
+        generateConnectionMessageFromNative(mDeviceTwo, BluetoothProfile.STATE_CONNECTED,
+                BluetoothProfile.STATE_DISCONNECTED);
+        Assert.assertEquals(BluetoothProfile.STATE_CONNECTED,
+                mService.getConnectionState(mDeviceTwo));
+        Assert.assertTrue(mService.getDevices().contains(mDeviceTwo));
+        verify(mNativeInterface, times(1)).setVolume(eq(mDeviceTwo), eq(groupVolume));
+    }
+
+    /**
+     * Test setting volume for a new group member who is discovered after the volume level
+     * for a group was already changed and cached.
+     */
+    @Test
+    public void testLateDiscoveredGroupMember() throws Exception {
+        int groupId = 1;
+        int groupVolume = 56;
+
+        // For now only one device is in the group
+        when(mCsipService.getGroupId(mDevice, BluetoothUuid.CAP)).thenReturn(groupId);
+        when(mCsipService.getGroupId(mDeviceTwo, BluetoothUuid.CAP)).thenReturn(-1);
+
+        // Update the device policy so okToConnect() returns true
+        when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+        when(mDatabaseManager
+                .getProfileConnectionPolicy(any(BluetoothDevice.class),
+                        eq(BluetoothProfile.VOLUME_CONTROL)))
+                .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+        doReturn(true).when(mNativeInterface).connectVolumeControl(any(BluetoothDevice.class));
+        doReturn(true).when(mNativeInterface).disconnectVolumeControl(any(BluetoothDevice.class));
+
+        generateConnectionMessageFromNative(mDevice, BluetoothProfile.STATE_CONNECTED,
+                BluetoothProfile.STATE_DISCONNECTED);
+        Assert.assertEquals(BluetoothProfile.STATE_CONNECTED,
+                mService.getConnectionState(mDevice));
+        Assert.assertTrue(mService.getDevices().contains(mDevice));
+
+        // Set the group volume
+        mService.setGroupVolume(groupId, groupVolume);
+
+        // Verify that second device will not get the group volume level if it is not a group member
+        generateConnectionMessageFromNative(mDeviceTwo, BluetoothProfile.STATE_CONNECTED,
+                BluetoothProfile.STATE_DISCONNECTED);
+        Assert.assertEquals(BluetoothProfile.STATE_CONNECTED,
+                mService.getConnectionState(mDeviceTwo));
+        Assert.assertTrue(mService.getDevices().contains(mDeviceTwo));
+        verify(mNativeInterface, times(0)).setVolume(eq(mDeviceTwo), eq(groupVolume));
+
+        // But gets the volume when it becomes the group member
+        when(mCsipService.getGroupId(mDeviceTwo, BluetoothUuid.CAP)).thenReturn(groupId);
+        mService.handleGroupNodeAdded(groupId, mDeviceTwo);
+        verify(mNativeInterface, times(1)).setVolume(eq(mDeviceTwo), eq(groupVolume));
+    }
+
     @Test
     public void testServiceBinderGetDevicesMatchingConnectionStates() throws Exception {
         final SynchronousResultReceiver<List<BluetoothDevice>> recv =
diff --git a/android/pandora/.gitignore b/android/pandora/.gitignore
new file mode 100644
index 0000000..cdb0870
--- /dev/null
+++ b/android/pandora/.gitignore
@@ -0,0 +1,3 @@
+trace*
+log*
+out*
diff --git a/android/pandora/gen_cov.py b/android/pandora/gen_cov.py
new file mode 100755
index 0000000..e37c704
--- /dev/null
+++ b/android/pandora/gen_cov.py
@@ -0,0 +1,320 @@
+#!/usr/bin/env python
+
+import argparse
+import os
+from pathlib import Path
+import shutil
+import subprocess
+import sys
+import xml.etree.ElementTree as ET
+
+
+def run_pts_bot():
+  run_pts_bot_cmd = [
+      # atest command with verbose mode.
+      'atest',
+      '-d',
+      '-v',
+      'pts-bot',
+      # Coverage tool chains and specify that coverage should be flush to the
+      # disk between each tests.
+      '--',
+      '--coverage',
+      '--coverage-toolchain JACOCO',
+      '--coverage-toolchain CLANG',
+      '--coverage-flush',
+  ]
+  subprocess.run(run_pts_bot_cmd).returncode
+
+
+def run_unit_tests():
+
+  # Output logs directory
+  logs_out = Path('logs_bt_tests')
+  logs_out.mkdir(exist_ok=True)
+
+  mts_tests = []
+  android_build_top = os.getenv('ANDROID_BUILD_TOP')
+  mts_xml = ET.parse(
+      f'{android_build_top}/test/mts/tools/mts-tradefed/res/config/mts-bluetooth-tests-list.xml'
+  )
+
+  for child in mts_xml.getroot():
+    value = child.attrib['value']
+    if 'enable:true' in value:
+      test = value.replace(':enable:true', '')
+      mts_tests.append(test)
+
+  for test in mts_tests:
+    print(f'Test started: {test}')
+
+    # Env variables necessary for native unit tests.
+    env = os.environ.copy()
+    env['CLANG_COVERAGE_CONTINUOUS_MODE'] = 'true'
+    env['CLANG_COVERAGE'] = 'true'
+    env['NATIVE_COVERAGE_PATHS'] = 'packages/modules/Bluetooth'
+    run_test_cmd = [
+        # atest command with verbose mode.
+        'atest',
+        '-d',
+        '-v',
+        test,
+        # Coverage tool chains and specify that coverage should be flush to the
+        # disk between each tests.
+        '--',
+        '--coverage',
+        '--coverage-toolchain JACOCO',
+        '--coverage-toolchain CLANG',
+        '--coverage-flush',
+        # Allows tests to use hidden APIs.
+        '--test-arg '
+        'com.android.compatibility.testtype.LibcoreTest:hidden-api-checks:false',
+        '--test-arg '
+        'com.android.tradefed.testtype.AndroidJUnitTest:hidden-api-checks:false',
+        '--test-arg '
+        'com.android.tradefed.testtype.InstrumentationTest:hidden-api-checks:false',
+        '--skip-system-status-check '
+        'com.android.tradefed.suite.checker.ShellStatusChecker',
+    ]
+    with open(f'{logs_out}/{test}.txt', 'w') as f:
+      returncode = subprocess.run(
+          run_test_cmd, env=env, stdout=f, stderr=subprocess.STDOUT).returncode
+      print(
+          f'Test ended [{"Success" if returncode == 0 else "Failed"}]: {test}')
+
+
+def generate_java_coverage(bt_apex_name, trace_path, coverage_out):
+
+  out = os.getenv('OUT')
+  android_host_out = os.getenv('ANDROID_HOST_OUT')
+
+  java_coverage_out = Path(f'{coverage_out}/java')
+  temp_path = Path(f'{coverage_out}/temp')
+  if temp_path.exists():
+    shutil.rmtree(temp_path, ignore_errors=True)
+  temp_path.mkdir()
+
+  framework_jar_path = Path(
+      f'{out}/obj/PACKAGING/jacoco_intermediates/JAVA_LIBRARIES/framework-bluetooth.{bt_apex_name}_intermediates'
+  )
+  service_jar_path = Path(
+      f'{out}/obj/PACKAGING/jacoco_intermediates/JAVA_LIBRARIES/service-bluetooth.{bt_apex_name}_intermediates'
+  )
+  app_jar_path = Path(
+      f'{out}/obj/PACKAGING/jacoco_intermediates/ETC/Bluetooth{"Google" if "com.google" in bt_apex_name else ""}.{bt_apex_name}_intermediates'
+  )
+
+  # From google3/configs/wireless/android/testing/atp/prod/mainline-engprod/templates/modules/bluetooth.gcl.
+  framework_exclude_classes = [
+      '**/com/android/bluetooth/x/**/*.class',
+      '**/*Test$*.class',
+      '**/android/bluetooth/I*$Default.class',
+      '**/android/bluetooth/**/I*$Default.class',
+      '**/android/bluetooth/I*$Stub.class',
+      '**/android/bluetooth/**/I*$Stub.class',
+      '**/android/bluetooth/I*$Stub$Proxy.class',
+      '**/android/bluetooth/**/I*$Stub$Proxy.class',
+      '**/com/android/internal/util/**/*.class',
+      '**/android/net/**/*.class',
+  ]
+  service_exclude_classes = [
+      '**/com/android/bluetooth/x/**/*.class',
+      '**/androidx/**/*.class',
+      '**/android/net/**/*.class',
+      '**/android/support/**/*.class',
+      '**/kotlin/**/*.class',
+      '**/*Test$*.class',
+      '**/com/android/internal/annotations/**/*.class',
+      '**/android/annotation/**/*.class',
+      '**/android/net/**/*.class',
+  ]
+  app_exclude_classes = [
+      '**/*Test$*.class',
+      '**/com/android/bluetooth/x/**/*.class',
+      '**/com/android/internal/annotations/**/*.class',
+      '**/com/android/internal/util/**/*.class',
+      '**/android/annotation/**/*.class',
+      '**/android/net/**/*.class',
+      '**/android/support/v4/**/*.class',
+      '**/androidx/**/*.class',
+      '**/kotlin/**/*.class',
+      '**/com/google/**/*.class',
+      '**/javax/**/*.class',
+      '**/android/hardware/**/*.class',  # Added
+      '**/android/hidl/**/*.class',  # Added
+      '**/com/android/bluetooth/**/BluetoothMetrics*.class',  # Added
+  ]
+
+  # Merged ec files.
+  merged_ec_path = Path(f'{temp_path}/merged.ec')
+  subprocess.run((
+      f'java -jar {android_host_out}/framework/jacoco-cli.jar merge {trace_path.absolute()}/*.ec '
+      f'--destfile {merged_ec_path.absolute()}'),
+                 shell=True)
+
+  # Copy and extract jar files.
+  framework_temp_path = Path(f'{temp_path}/{framework_jar_path.name}')
+  service_temp_path = Path(f'{temp_path}/{service_jar_path.name}')
+  app_temp_path = Path(f'{temp_path}/{app_jar_path.name}')
+
+  shutil.copytree(framework_jar_path, framework_temp_path)
+  shutil.copytree(service_jar_path, service_temp_path)
+  shutil.copytree(app_jar_path, app_temp_path)
+
+  current_dir_path = Path.cwd()
+  for p in [framework_temp_path, service_temp_path, app_temp_path]:
+    os.chdir(p.absolute())
+    os.system('jar xf jacoco-report-classes.jar')
+    os.chdir(current_dir_path)
+
+  os.remove(f'{framework_temp_path}/jacoco-report-classes.jar')
+  os.remove(f'{service_temp_path}/jacoco-report-classes.jar')
+  os.remove(f'{app_temp_path}/jacoco-report-classes.jar')
+
+  # Generate coverage report.
+  exclude_classes = []
+  for glob in framework_exclude_classes:
+    exclude_classes.extend(list(framework_temp_path.glob(glob)))
+  for glob in service_exclude_classes:
+    exclude_classes.extend(list(service_temp_path.glob(glob)))
+  for glob in app_exclude_classes:
+    exclude_classes.extend(list(app_temp_path.glob(glob)))
+
+  for c in exclude_classes:
+    if c.exists():
+      os.remove(c.absolute())
+
+  gen_java_cov_report_cmd = [
+      f'java',
+      f'-jar',
+      f'{android_host_out}/framework/jacoco-cli.jar',
+      f'report',
+      f'{merged_ec_path.absolute()}',
+      f'--classfiles',
+      f'{temp_path.absolute()}',
+      f'--html',
+      f'{java_coverage_out.absolute()}',
+      f'--name',
+      f'{java_coverage_out.absolute()}.html',
+  ]
+  subprocess.run(gen_java_cov_report_cmd)
+
+  # Cleanup.
+  shutil.rmtree(temp_path, ignore_errors=True)
+
+
+def generate_native_coverage(bt_apex_name, trace_path, coverage_out):
+
+  out = os.getenv('OUT')
+  android_build_top = os.getenv('ANDROID_BUILD_TOP')
+
+  native_coverage_out = Path(f'{coverage_out}/native')
+  temp_path = Path(f'{coverage_out}/temp')
+  if temp_path.exists():
+    shutil.rmtree(temp_path, ignore_errors=True)
+  temp_path.mkdir()
+
+  # From google3/configs/wireless/android/testing/atp/prod/mainline-engprod/templates/modules/bluetooth.gcl.
+  exclude_files = {
+      'system/.*_aidl.*',
+      'system/.*_test.*',
+      'system/.*_mock.*',
+      'system/.*_unittest.*',
+      'system/binder/',
+      'system/blueberry/',
+      'system/build/',
+      'system/conf/',
+      'system/doc/',
+      'system/test/',
+      'system/gd/l2cap/',
+      'system/gd/security/',
+      'system/gd/neighbor/',
+      # 'android/', # Should not be excluded
+  }
+
+  # Merge profdata files.
+  profdata_path = Path(f'{temp_path}/coverage.profdata')
+  subprocess.run(
+      f'llvm-profdata merge --sparse -o {profdata_path.absolute()} {trace_path.absolute()}/*.profraw',
+      shell=True)
+
+  gen_native_cov_report_cmd = [
+      f'llvm-cov',
+      f'show',
+      f'-format=html',
+      f'-output-dir={native_coverage_out.absolute()}',
+      f'-instr-profile={profdata_path.absolute()}',
+      f'{out}/symbols/apex/{bt_apex_name}/lib64/libbluetooth_jni.so',
+      f'-path-equivalence=/proc/self/cwd,{android_build_top}',
+      f'/proc/self/cwd/packages/modules/Bluetooth',
+  ]
+  for f in exclude_files:
+    gen_native_cov_report_cmd.append(f'-ignore-filename-regex={f}')
+  subprocess.run(gen_native_cov_report_cmd, cwd=android_build_top)
+
+  # Cleanup.
+  shutil.rmtree(temp_path, ignore_errors=True)
+
+
+if __name__ == '__main__':
+
+  parser = argparse.ArgumentParser()
+  parser.add_argument(
+      '--apex-name',
+      default='com.android.btservices',
+      help='bluetooth apex name. Default: com.android.btservices')
+  parser.add_argument(
+      '--java', action='store_true', help='generate Java coverage')
+  parser.add_argument(
+      '--native', action='store_true', help='generate native coverage')
+  parser.add_argument(
+      '--out',
+      type=str,
+      default='out_coverage',
+      help='out directory for coverage reports. Default: ./out_coverage')
+  parser.add_argument(
+      '--trace',
+      type=str,
+      default='trace',
+      help='trace directory with .ec and .profraw files. Default: ./trace')
+  parser.add_argument(
+      '--full-report',
+      action='store_true',
+      help='run all tests and compute coverage report')
+  args = parser.parse_args()
+
+  coverage_out = Path(args.out)
+  shutil.rmtree(coverage_out, ignore_errors=True)
+  coverage_out.mkdir()
+
+  if not args.full_report:
+    trace_path = Path(args.trace)
+    if (not trace_path.exists() or not trace_path.is_dir()):
+      sys.exit('Trace directory does not exist')
+
+    if (args.java):
+      generate_java_coverage(args.bt_apex_name, trace_path, coverage_out)
+    if (args.native):
+      generate_native_coverage(args.bt_apex_name, trace_path, coverage_out)
+
+  else:
+    # Compute Pandora coverage.
+    run_pts_bot()
+    coverage_out_pandora = Path(f'{coverage_out}/pandora')
+    coverage_out_pandora.mkdir()
+    trace_pandora = Path('trace_pandora')
+    subprocess.run(['adb', 'pull', '/data/misc/trace', trace_pandora])
+    generate_java_coverage(args.bt_apex_name, trace_pandora,
+                           coverage_out_pandora)
+    generate_native_coverage(args.bt_apex_name, trace_pandora,
+                             coverage_out_pandora)
+
+    # Compute all coverage.
+    run_unit_tests()
+    coverage_out_mainline = Path(f'{coverage_out}/mainline')
+    coverage_out_pandora.mkdir()
+    trace_all = Path('trace_all')
+    subprocess.run(['adb', 'pull', '/data/misc/trace', trace_all])
+    generate_java_coverage(args.bt_apex_name, trace_all, coverage_out_mainline)
+    generate_native_coverage(args.bt_apex_name, trace_all,
+                             coverage_out_mainline)
diff --git a/android/pandora/mmi2grpc/mmi2grpc/l2cap.py b/android/pandora/mmi2grpc/mmi2grpc/l2cap.py
index 2baefcd..760758d 100644
--- a/android/pandora/mmi2grpc/mmi2grpc/l2cap.py
+++ b/android/pandora/mmi2grpc/mmi2grpc/l2cap.py
@@ -1,13 +1,15 @@
 import time
+import sys
 
 from mmi2grpc._helpers import assert_description
 from mmi2grpc._helpers import match_description
 from mmi2grpc._proxy import ProfileProxy
+
 from pandora_experimental.host_grpc import Host
-from pandora_experimental.host_pb2 import Connection
+from pandora_experimental.host_pb2 import Connection, ConnectabilityMode, AddressType
 from pandora_experimental.l2cap_grpc import L2CAP
+
 from typing import Optional
-import sys
 
 
 class L2CAPProxy(ProfileProxy):
@@ -88,7 +90,10 @@
         """
         Place the IUT into LE connectable mode.
         """
-        self.host.SetLEConnectable()
+        self.host.StartAdvertising(
+            connectability_mode=ConnectabilityMode.CONECTABILITY_CONNECTABLE,
+            own_address_type=AddressType.PUBLIC,
+        )
         # not strictly necessary, but can save time on waiting connection
         tests_to_open_bluetooth_server_socket = [
             "L2CAP/LE/CFC/BV-03-C",
diff --git a/android/pandora/mmi2grpc/mmi2grpc/sm.py b/android/pandora/mmi2grpc/mmi2grpc/sm.py
index 19928d9..7991770 100644
--- a/android/pandora/mmi2grpc/mmi2grpc/sm.py
+++ b/android/pandora/mmi2grpc/mmi2grpc/sm.py
@@ -23,6 +23,7 @@
 
 from pandora_experimental.security_grpc import Security
 from pandora_experimental.host_grpc import Host
+from pandora_experimental.host_pb2 import ConnectabilityMode, AddressType
 
 
 def debug(*args, **kwargs):
@@ -89,7 +90,10 @@
         """
         Action: Place the IUT in connectable mode
         """
-        self.host.SetLEConnectable()
+        self.host.StartAdvertising(
+            connectability_mode=ConnectabilityMode.CONECTABILITY_CONNECTABLE,
+            own_address_type=AddressType.PUBLIC,
+        )
         return "OK"
 
     @assert_description
@@ -140,12 +144,31 @@
 
         return "OK"
 
+    @assert_description
+    def MMI_IUT_ACCEPT_CONNECTION_BR_EDR(self, **kwargs):
+        """
+        Please prepare IUT into a connectable mode in BR/EDR.
+
+        Description:
+        Verify that the Implementation Under Test (IUT) can accept a connect
+        request from PTS.
+        """
+
+        return "OK"
+
+    @assert_description
+    def _mmi_2001(self, **kwargs):
+        """
+        Please verify the passKey is correct: 000000
+        """
+        return "OK"
+
     def _handle_pairing_requests(self):
 
         def task():
             pairing_events = self.security.OnPairing()
             for event in pairing_events:
-                if event.just_works:
+                if event.just_works or event.numeric_comparison:
                     pairing_events.send(event=event, confirm=True)
                 if event.passkey_entry_request:
                     try:
diff --git a/android/pandora/server/configs/PtsBotTest.xml b/android/pandora/server/configs/PtsBotTest.xml
index e5850b7..42f3ec9 100644
--- a/android/pandora/server/configs/PtsBotTest.xml
+++ b/android/pandora/server/configs/PtsBotTest.xml
@@ -62,6 +62,7 @@
         <option name="profile" value="SM/PER/KDU" />
         <option name="profile" value="SM/PER/SCJW" />
         <option name="profile" value="SM/PER/SCPK" />
+        <option name="profile" value="SM/PER/SCCT" />
         <option name="profile" value="HOGP/RH" />
     </test>
 </configuration>
diff --git a/android/pandora/server/configs/pts_bot_tests_config.json b/android/pandora/server/configs/pts_bot_tests_config.json
index 759895e..2af2074 100644
--- a/android/pandora/server/configs/pts_bot_tests_config.json
+++ b/android/pandora/server/configs/pts_bot_tests_config.json
@@ -413,7 +413,11 @@
     "SM/CEN/SCPK/BV-04-C",
     "SM/PER/SCJW/BV-02-C",
     "SM/PER/SCPK/BI-04-C",
-    "SM/PER/SCPK/BV-03-C"
+    "SM/PER/SCPK/BV-03-C",
+    "SM/PER/SCCT/BV-04-C",
+    "SM/PER/SCCT/BV-06-C",
+    "SM/PER/SCCT/BV-08-C",
+    "SM/PER/SCCT/BV-10-C"
   ],
   "ics": {
     "TSPC_4.0HCI_1a_2": true,
diff --git a/android/pandora/server/proto/pandora_experimental/host.proto b/android/pandora/server/proto/pandora_experimental/host.proto
index 3a2fa57..d0cad8d 100644
--- a/android/pandora/server/proto/pandora_experimental/host.proto
+++ b/android/pandora/server/proto/pandora_experimental/host.proto
@@ -41,12 +41,23 @@
   rpc GetLEConnection(GetLEConnectionRequest) returns (GetLEConnectionResponse);
   // Disconnect ongoing LE connection.
   rpc DisconnectLE(DisconnectLERequest) returns (google.protobuf.Empty);
-  // Start LE advertisement
-  rpc SetLEConnectable(google.protobuf.Empty) returns (google.protobuf.Empty);
+  // Create and enable an advertising set using legacy or extended advertising,
+  // except periodic advertising.
+  rpc StartAdvertising(StartAdvertisingRequest) returns (StartAdvertisingResponse);
+  // Create and enable a periodic advertising set.
+  rpc StartPeriodicAdvertising(StartPeriodicAdvertisingRequest) returns (StartPeriodicAdvertisingResponse);
+  // Remove an advertising set.
+  rpc StopAdvertising(StopAdvertisingRequest) returns (StopAdvertisingResponse);
   // Run BR/EDR inquiry and returns each device found
   rpc RunInquiry(RunInquiryRequest) returns (stream RunInquiryResponse);
   // Run LE discovery (scanning) and return each device found
   rpc RunDiscovery(RunDiscoveryRequest) returns (stream RunDiscoveryResponse);
+  // Set BREDR connectability mode
+  rpc SetConnectabilityMode(SetConnectabilityModeRequest) returns (SetConnectabilityModeResponse);
+  // Set BREDR discoverable mode
+  rpc SetDiscoverabilityMode(SetDiscoverabilityModeRequest) returns (SetDiscoverabilityModeResponse);
+  // Get device name from connection
+  rpc GetDeviceName(GetDeviceNameRequest) returns (GetDeviceNameResponse);
 }
 
 // Response of the `ReadLocalAddress` method.
@@ -58,15 +69,33 @@
 // A Token representing an ACL connection.
 // It's acquired via a Connect on the Host service.
 message Connection {
-  // Opaque value filled by the gRPC server, must not
-  // be modified nor crafted.
+  // Opaque value filled by the gRPC server, must not be modified nor crafted
+  // Android specific: it's secretly an encoded InternelConnectionRef created using newConnection
   bytes cookie = 1;
 }
 
+// Internal representation of a Connection - not exposed to clients, included here
+// just for code-generation convenience
+message InternalConnectionRef {
+  bytes address = 1;
+  Transport transport = 2;
+}
+
+// WARNING: Leaving this enum empty will default to BREDR, so make sure that this is a
+// valid default whenever used, and that we always populate this value.
+enum Transport {
+  TRANSPORT_BREDR = 0;
+  TRANSPORT_LE = 1;
+}
+
 // Request of the `Connect` method.
 message ConnectRequest {
   // Peer Bluetooth Device Address as array of 6 bytes.
   bytes address = 1;
+  // Whether we want to initiate pairing as part of the connection
+  bool skip_pairing = 2;
+  // Whether confirmation prompts should be auto-accepted or handled manually
+  bool manually_confirm = 3;
 }
 
 // Response of the `Connect` method.
@@ -146,6 +175,74 @@
   Connection connection = 1;
 }
 
+message AdvertisingHandle {
+  bytes cookie = 1;
+}
+
+enum AddressType {
+  PUBLIC = 0x00;
+  RANDOM = 0x01;
+}
+
+// Advertising Data including one or multiple AD types.
+// Since the Flags AD type is mandatory, it must be automatically set by the
+// IUT.
+// include_<AD type> fields are used for AD type which are generally not
+// exposed and that must be set by the IUT when specified.
+// See Core Supplement, Part A, Data Types for details
+message AdvertisingData {
+  repeated string service_uuids = 1;
+  bool include_local_name = 2;
+  bytes manufacturer_specific_data = 3;
+  bool include_tx_power_level = 4;
+  bool include_peripheral_connection_interval_range = 5;
+  repeated string service_solicitation = 6;
+  map<string, bytes> service_data = 7;
+  // Must be on 16 bits.
+  uint32 appearance = 8;
+  repeated bytes public_target_addresses = 9;
+  repeated bytes random_target_addresses = 10;
+  bool include_advertising_interval = 11;
+  bool include_le_address = 12;
+  bool include_le_role = 13;
+  string uri = 14;
+}
+
+message StartAdvertisingRequest {
+  bool legacy = 1;
+  DiscoverabilityMode discovery_mode = 2;
+  ConnectabilityMode connectability_mode = 3;
+  AddressType own_address_type = 4;
+  // If none, undirected.
+  bytes peer_address = 5;
+  AdvertisingData advertising_data = 6;
+  // If none, not scannable.
+  AdvertisingData scan_response_data = 7;
+}
+
+message StartAdvertisingResponse {
+  AdvertisingHandle handle = 1;
+}
+
+message StartPeriodicAdvertisingRequest {
+  AddressType own_address_type = 1;
+  // If none, undirected.
+  bytes peer_address = 2;
+  uint32 interval_min = 3;
+  uint32 interval_max = 4;
+  AdvertisingData advertising_data = 5;
+}
+
+message StartPeriodicAdvertisingResponse {
+  AdvertisingHandle handle = 1;
+}
+
+message StopAdvertisingRequest {
+  AdvertisingHandle handle = 1;
+}
+
+message StopAdvertisingResponse {}
+
 message RunInquiryRequest {
 }
 
@@ -165,3 +262,38 @@
   string name = 1;
   bytes address = 2;
 }
+
+// 5.3 Vol 3C 4.1 Discoverability Modes
+enum DiscoverabilityMode {
+  DISCOVERABILITY_UNSPECIFIED = 0;
+  DISCOVERABILITY_NONE = 1;
+  DISCOVERABILITY_LIMITED = 2;
+  DISCOVERABILITY_GENERAL = 3;
+}
+
+// 5.3 Vol 3C 4.2 Connectability Modes
+enum ConnectabilityMode {
+  CONNECTABILITY_UNSPECIFIED = 0;
+  CONNECTABILITY_NOT_CONNECTABLE = 1;
+  CONECTABILITY_CONNECTABLE = 2;
+}
+
+message SetConnectabilityModeRequest {
+  ConnectabilityMode connectability = 1;
+}
+
+message SetConnectabilityModeResponse {}
+
+message SetDiscoverabilityModeRequest {
+  DiscoverabilityMode discoverability = 1;
+}
+
+message SetDiscoverabilityModeResponse {}
+
+message GetDeviceNameRequest {
+  Connection connection = 1;
+}
+
+message GetDeviceNameResponse {
+  string name = 1;
+}
diff --git a/android/pandora/server/src/com/android/pandora/Gatt.kt b/android/pandora/server/src/com/android/pandora/Gatt.kt
index 6db84b8..122c634 100644
--- a/android/pandora/server/src/com/android/pandora/Gatt.kt
+++ b/android/pandora/server/src/com/android/pandora/Gatt.kt
@@ -74,7 +74,7 @@
     grpcUnary<ExchangeMTUResponse>(mScope, responseObserver) {
       val mtu = request.mtu
       Log.i(TAG, "exchangeMTU MTU=$mtu")
-      if (!GattInstance.get(request.connection.cookie).mGatt.requestMtu(mtu)) {
+      if (!GattInstance.get(request.connection.address).mGatt.requestMtu(mtu)) {
         Log.e(TAG, "Error on requesting MTU $mtu")
         throw Status.UNKNOWN.asException()
       }
@@ -88,7 +88,7 @@
   ) {
     grpcUnary<WriteResponse>(mScope, responseObserver) {
       Log.i(TAG, "writeAttFromHandle handle=${request.handle}")
-      val gattInstance = GattInstance.get(request.connection.cookie)
+      val gattInstance = GattInstance.get(request.connection.address)
       var characteristic: BluetoothGattCharacteristic? =
           getCharacteristicWithHandle(request.handle, gattInstance)
       if (characteristic == null) {
@@ -113,7 +113,7 @@
       responseObserver: StreamObserver<DiscoverServicesResponse>
   ) {
     grpcUnary<DiscoverServicesResponse>(mScope, responseObserver) {
-      val gattInstance = GattInstance.get(request.connection.cookie)
+      val gattInstance = GattInstance.get(request.connection.address)
       Log.i(TAG, "discoverServiceByUuid uuid=${request.uuid}")
       // In some cases, GATT starts a discovery immediately after being connected, so
       // we need to wait until the service discovery is finished to be able to discover again.
@@ -133,7 +133,7 @@
   ) {
     grpcUnary<DiscoverServicesResponse>(mScope, responseObserver) {
       Log.i(TAG, "discoverServices")
-      val gattInstance = GattInstance.get(request.connection.cookie)
+      val gattInstance = GattInstance.get(request.connection.address)
       check(gattInstance.mGatt.discoverServices())
       gattInstance.waitForDiscoveryEnd()
       DiscoverServicesResponse.newBuilder()
@@ -168,7 +168,7 @@
   ) {
     grpcUnary<ClearCacheResponse>(mScope, responseObserver) {
       Log.i(TAG, "clearCache")
-      val gattInstance = GattInstance.get(request.connection.cookie)
+      val gattInstance = GattInstance.get(request.connection.address)
       check(gattInstance.mGatt.refresh())
       ClearCacheResponse.newBuilder().build()
     }
@@ -180,7 +180,7 @@
   ) {
     grpcUnary<ReadCharacteristicResponse>(mScope, responseObserver) {
       Log.i(TAG, "readCharacteristicFromHandle handle=${request.handle}")
-      val gattInstance = GattInstance.get(request.connection.cookie)
+      val gattInstance = GattInstance.get(request.connection.address)
       val characteristic: BluetoothGattCharacteristic? =
           getCharacteristicWithHandle(request.handle, gattInstance)
       checkNotNull(characteristic) { "Characteristic handle ${request.handle} not found." }
@@ -198,7 +198,7 @@
   ) {
     grpcUnary<ReadCharacteristicsFromUuidResponse>(mScope, responseObserver) {
       Log.i(TAG, "readCharacteristicsFromUuid uuid=${request.uuid}")
-      val gattInstance = GattInstance.get(request.connection.cookie)
+      val gattInstance = GattInstance.get(request.connection.address)
       tryDiscoverServices(gattInstance)
       val readValues =
           gattInstance.readCharacteristicUuidBlocking(
@@ -215,7 +215,7 @@
   ) {
     grpcUnary<ReadCharacteristicDescriptorResponse>(mScope, responseObserver) {
       Log.i(TAG, "readCharacteristicDescriptorFromHandle handle=${request.handle}")
-      val gattInstance = GattInstance.get(request.connection.cookie)
+      val gattInstance = GattInstance.get(request.connection.address)
       val descriptor: BluetoothGattDescriptor? =
           getDescriptorWithHandle(request.handle, gattInstance)
       checkNotNull(descriptor) { "Descriptor handle ${request.handle} not found." }
diff --git a/android/pandora/server/src/com/android/pandora/Host.kt b/android/pandora/server/src/com/android/pandora/Host.kt
index 95f6708..ad64c12 100644
--- a/android/pandora/server/src/com/android/pandora/Host.kt
+++ b/android/pandora/server/src/com/android/pandora/Host.kt
@@ -17,29 +17,34 @@
 package com.android.pandora
 
 import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothAssignedNumbers
 import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothDevice.ADDRESS_TYPE_PUBLIC
+import android.bluetooth.BluetoothDevice.ADDRESS_TYPE_RANDOM
 import android.bluetooth.BluetoothDevice.BOND_BONDED
+import android.bluetooth.BluetoothDevice.TRANSPORT_AUTO
+import android.bluetooth.BluetoothDevice.TRANSPORT_BREDR
 import android.bluetooth.BluetoothDevice.TRANSPORT_LE
 import android.bluetooth.BluetoothManager
 import android.bluetooth.BluetoothProfile
 import android.bluetooth.le.AdvertiseCallback
 import android.bluetooth.le.AdvertiseData
 import android.bluetooth.le.AdvertiseSettings
-import android.bluetooth.le.AdvertisingSetParameters
 import android.bluetooth.le.ScanCallback
 import android.bluetooth.le.ScanResult
 import android.content.Context
 import android.content.Intent
 import android.content.IntentFilter
 import android.net.MacAddress
+import android.os.ParcelUuid
 import android.util.Log
 import com.google.protobuf.ByteString
 import com.google.protobuf.Empty
 import io.grpc.Status
 import io.grpc.stub.StreamObserver
-import kotlin.Result.Companion.failure
-import kotlin.Result.Companion.success
-import kotlin.coroutines.suspendCoroutine
+import java.io.IOException
+import java.time.Duration
+import java.util.UUID
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.awaitCancellation
@@ -57,19 +62,25 @@
 import kotlinx.coroutines.flow.shareIn
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
 import pandora.HostGrpc.HostImplBase
 import pandora.HostProto.*
 
 @kotlinx.coroutines.ExperimentalCoroutinesApi
 class Host(private val context: Context, private val server: Server) : HostImplBase() {
   private val TAG = "PandoraHost"
-  private val ADVERTISEMENT_DURATION_MILLIS: Int = 10000
+
   private val scope: CoroutineScope
   private val flow: Flow<Intent>
 
   private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)!!
   private val bluetoothAdapter = bluetoothManager.adapter
 
+  private var connectability = ConnectabilityMode.CONNECTABILITY_UNSPECIFIED
+  private var discoverability = DiscoverabilityMode.DISCOVERABILITY_UNSPECIFIED
+
+  private val advertisers = mutableMapOf<UUID, AdvertiseCallback>()
+
   init {
     scope = CoroutineScope(Dispatchers.Default)
 
@@ -79,6 +90,8 @@
     intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
     intentFilter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)
     intentFilter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST)
+    intentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
+    intentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
     intentFilter.addAction(BluetoothDevice.ACTION_FOUND)
 
     // Creates a shared flow of intents that can be used in all methods in the coroutine scope.
@@ -219,81 +232,59 @@
       acceptPairingAndAwaitBonded(bluetoothDevice)
 
       WaitConnectionResponse.newBuilder()
-        .setConnection(
-          Connection.newBuilder()
-            .setCookie(ByteString.copyFromUtf8(bluetoothDevice.address))
-            .build()
-        )
+        .setConnection(newConnection(bluetoothDevice, Transport.TRANSPORT_BREDR))
         .build()
     }
   }
 
-  /**
-   * Set the device in advertisement mode for #ADVERTISEMENT_DURATION_MILLIS milliseconds.
-   * @param request Request sent by the client.
-   * @param responseObserver Response to build and set back to the client.
-   */
-  override fun setLEConnectable(
-    request: Empty,
-    responseObserver: StreamObserver<Empty>,
-  ) {
-    // Creates a gRPC coroutine in a given coroutine scope which executes a given suspended function
-    // returning a gRPC response and sends it on a given gRPC stream observer.
-    grpcUnary<Empty>(scope, responseObserver) {
-      Log.i(TAG, "setLEConnectable")
-      val advertiser = bluetoothAdapter.getBluetoothLeAdvertiser()
-      val advertiseSettings =
-        AdvertiseSettings.Builder()
-          .setConnectable(true)
-          .setOwnAddressType(AdvertisingSetParameters.ADDRESS_TYPE_PUBLIC)
-          .setTimeout(ADVERTISEMENT_DURATION_MILLIS)
-          .build()
-      val advertiseData = AdvertiseData.Builder().build()
-      suspendCoroutine<Boolean> { continuation ->
-        val advertiseCallback =
-          object : AdvertiseCallback() {
-            override fun onStartFailure(errorCode: Int) {
-              Log.i(TAG, "Advertising failed: $errorCode")
-              continuation.resumeWith(failure(Exception("Advertising failed: $errorCode")))
-            }
-
-            override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
-              Log.i(TAG, "Advertising success")
-              continuation.resumeWith(success(true))
-            }
-          }
-        advertiser.startAdvertising(advertiseSettings, advertiseData, advertiseCallback)
-      }
-
-      // Response sent to client
-      Empty.getDefaultInstance()
-    }
-  }
-
   override fun connect(request: ConnectRequest, responseObserver: StreamObserver<ConnectResponse>) {
     grpcUnary(scope, responseObserver) {
       val bluetoothDevice = request.address.toBluetoothDevice(bluetoothAdapter)
 
       Log.i(TAG, "connect: address=$bluetoothDevice")
 
+      bluetoothAdapter.cancelDiscovery()
+
       if (!bluetoothDevice.isConnected()) {
-        if (bluetoothDevice.bondState == BOND_BONDED) {
-          // already bonded, just reconnect
-          bluetoothDevice.connect()
-          waitConnectionIntent(bluetoothDevice)
+        if (request.skipPairing) {
+          // do an SDP request to trigger a temporary BREDR connection
+          try {
+            withTimeout(1500) { bluetoothDevice.createRfcommSocket(3).connect() }
+          } catch (e: IOException) {
+            // ignore
+          }
         } else {
-          // need to bond
-          bluetoothDevice.createBond()
-          acceptPairingAndAwaitBonded(bluetoothDevice)
+          if (bluetoothDevice.bondState == BOND_BONDED) {
+            // already bonded, just reconnect
+            bluetoothDevice.connect()
+            waitConnectionIntent(bluetoothDevice)
+          } else {
+            // need to bond
+            bluetoothDevice.createBond()
+            if (!request.manuallyConfirm) {
+              acceptPairingAndAwaitBonded(bluetoothDevice)
+            }
+          }
         }
       }
 
       ConnectResponse.newBuilder()
-        .setConnection(
-          Connection.newBuilder()
-            .setCookie(ByteString.copyFromUtf8(bluetoothDevice.address))
-            .build()
-        )
+        .setConnection(newConnection(bluetoothDevice, Transport.TRANSPORT_BREDR))
+        .build()
+    }
+  }
+
+  override fun getConnection(
+    request: GetConnectionRequest,
+    responseObserver: StreamObserver<GetConnectionResponse>
+  ) {
+    grpcUnary(scope, responseObserver) {
+      val device = bluetoothAdapter.getRemoteDevice(request.address.toByteArray())
+      check(
+        device.isConnected() && device.type != BluetoothDevice.DEVICE_TYPE_LE
+      ) // either classic or dual
+      GetConnectionResponse.newBuilder()
+        .setConnection(newConnection(device, Transport.TRANSPORT_BREDR))
         .build()
     }
   }
@@ -319,6 +310,7 @@
 
       bluetoothDevice.disconnect()
       connectionStateChangedFlow.filter { it == BluetoothAdapter.STATE_DISCONNECTED }.first()
+     
       DisconnectResponse.getDefaultInstance()
     }
   }
@@ -329,13 +321,17 @@
   ) {
     grpcUnary<ConnectLEResponse>(scope, responseObserver) {
       val address = request.address.decodeAsMacAddressToString()
-      Log.i(TAG, "connectLE: $address")
-      val device = scanLeDevice(address)
-      GattInstance(device!!, TRANSPORT_LE, context).waitForState(BluetoothProfile.STATE_CONNECTED)
+      Log.i(TAG, "connect LE: $address")
+      val device = scanLeDevice(address)!!
+      GattInstance(device, TRANSPORT_LE, context)
+
+      flow
+        .filter { it.action == BluetoothDevice.ACTION_ACL_CONNECTED }
+        .filter { it.getBluetoothDeviceExtra() == device }
+        .first()
+
       ConnectLEResponse.newBuilder()
-        .setConnection(
-          Connection.newBuilder().setCookie(ByteString.copyFromUtf8(device.address)).build()
-        )
+        .setConnection(newConnection(device, Transport.TRANSPORT_LE))
         .build()
     }
   }
@@ -347,13 +343,10 @@
     grpcUnary<GetLEConnectionResponse>(scope, responseObserver) {
       val address = request.address.decodeAsMacAddressToString()
       Log.i(TAG, "getLEConnection: $address")
-      val device =
-        bluetoothAdapter.getRemoteLeDevice(address, BluetoothDevice.ADDRESS_TYPE_PUBLIC)
+      val device = bluetoothAdapter.getRemoteLeDevice(address, BluetoothDevice.ADDRESS_TYPE_PUBLIC)
       if (device.isConnected) {
         GetLEConnectionResponse.newBuilder()
-          .setConnection(
-            Connection.newBuilder().setCookie(ByteString.copyFromUtf8(device.address)).build()
-          )
+          .setConnection(newConnection(device, Transport.TRANSPORT_LE))
           .build()
       } else {
         Log.e(TAG, "Device: $device is not connected")
@@ -364,7 +357,7 @@
 
   override fun disconnectLE(request: DisconnectLERequest, responseObserver: StreamObserver<Empty>) {
     grpcUnary<Empty>(scope, responseObserver) {
-      val address = request.connection.cookie.toByteArray().decodeToString()
+      val address = request.connection.address
       Log.i(TAG, "disconnectLE: $address")
       val gattInstance = GattInstance.get(address)
 
@@ -409,6 +402,72 @@
     return bluetoothDevice
   }
 
+  override fun startAdvertising(
+    request: StartAdvertisingRequest,
+    responseObserver: StreamObserver<StartAdvertisingResponse>
+  ) {
+    Log.d(TAG, "startAdvertising")
+    grpcUnary(scope, responseObserver) {
+      val handle = UUID.randomUUID()
+
+      callbackFlow {
+          val callback =
+            object : AdvertiseCallback() {
+              override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
+                sendBlocking(
+                  StartAdvertisingResponse.newBuilder()
+                    .setHandle(
+                      AdvertisingHandle.newBuilder()
+                        .setCookie(ByteString.copyFromUtf8(handle.toString()))
+                    )
+                    .build()
+                )
+              }
+              override fun onStartFailure(errorCode: Int) {
+                error("failed to start advertising")
+              }
+            }
+
+          advertisers[handle] = callback
+
+          val advertisingDataBuilder = AdvertiseData.Builder()
+
+          for (service_uuid in request.advertisingData.serviceUuidsList) {
+            advertisingDataBuilder.addServiceUuid(ParcelUuid.fromString(service_uuid))
+          }
+
+          advertisingDataBuilder
+            .setIncludeDeviceName(request.advertisingData.includeLocalName)
+            .setIncludeTxPowerLevel(request.advertisingData.includeTxPowerLevel)
+            .addManufacturerData(
+              BluetoothAssignedNumbers.GOOGLE,
+              request.advertisingData.manufacturerSpecificData.toByteArray()
+            )
+
+          bluetoothAdapter.bluetoothLeAdvertiser.startAdvertising(
+            AdvertiseSettings.Builder()
+              .setConnectable(
+                request.connectabilityMode == ConnectabilityMode.CONECTABILITY_CONNECTABLE
+              )
+              .setOwnAddressType(
+                when (request.ownAddressType!!) {
+                  AddressType.PUBLIC -> ADDRESS_TYPE_PUBLIC
+                  AddressType.RANDOM -> ADDRESS_TYPE_RANDOM
+                  AddressType.UNRECOGNIZED ->
+                    error("unrecognized address type ${request.ownAddressType}")
+                }
+              )
+              .build(),
+            advertisingDataBuilder.build(),
+            callback,
+          )
+
+          awaitClose { /* no-op */}
+        }
+        .first()
+    }
+  }
+
   override fun runInquiry(
     request: RunInquiryRequest,
     responseObserver: StreamObserver<RunInquiryResponse>
@@ -432,15 +491,83 @@
             .addDevice(
               Device.newBuilder()
                 .setName(device.name)
-                .setAddress(
-                  ByteString.copyFrom(MacAddress.fromString(device.address).toByteArray())
-                )
+                .setAddress(device.toByteString())
             )
             .build()
         }
     }
   }
 
+  override fun setConnectabilityMode(
+    request: SetConnectabilityModeRequest,
+    responseObserver: StreamObserver<SetConnectabilityModeResponse>
+  ) {
+    grpcUnary(scope, responseObserver) {
+      Log.d(TAG, "setConnectabilityMode")
+      connectability = request.connectability!!
+
+      val scanMode =
+        when (connectability) {
+          ConnectabilityMode.CONNECTABILITY_UNSPECIFIED,
+          ConnectabilityMode.UNRECOGNIZED -> null
+          ConnectabilityMode.CONNECTABILITY_NOT_CONNECTABLE -> {
+            BluetoothAdapter.SCAN_MODE_NONE
+          }
+          ConnectabilityMode.CONECTABILITY_CONNECTABLE -> {
+            if (
+              discoverability == DiscoverabilityMode.DISCOVERABILITY_LIMITED ||
+                discoverability == DiscoverabilityMode.DISCOVERABILITY_GENERAL
+            ) {
+              BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE
+            } else {
+              BluetoothAdapter.SCAN_MODE_CONNECTABLE
+            }
+          }
+        }
+
+      if (scanMode != null) {
+        bluetoothAdapter.setScanMode(scanMode)
+      }
+      SetConnectabilityModeResponse.getDefaultInstance()
+    }
+  }
+
+  override fun setDiscoverabilityMode(
+    request: SetDiscoverabilityModeRequest,
+    responseObserver: StreamObserver<SetDiscoverabilityModeResponse>
+  ) {
+    Log.d(TAG, "setDiscoverabilityMode")
+    grpcUnary(scope, responseObserver) {
+      discoverability = request.discoverability!!
+
+      val scanMode =
+        when (discoverability) {
+          DiscoverabilityMode.DISCOVERABILITY_UNSPECIFIED,
+          DiscoverabilityMode.UNRECOGNIZED -> null
+          DiscoverabilityMode.DISCOVERABILITY_NONE ->
+            if (connectability == ConnectabilityMode.CONECTABILITY_CONNECTABLE) {
+              BluetoothAdapter.SCAN_MODE_CONNECTABLE
+            } else {
+              BluetoothAdapter.SCAN_MODE_NONE
+            }
+          DiscoverabilityMode.DISCOVERABILITY_LIMITED,
+          DiscoverabilityMode.DISCOVERABILITY_GENERAL ->
+            BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE
+        }
+
+      if (scanMode != null) {
+        bluetoothAdapter.setScanMode(scanMode)
+      }
+
+      if (request.discoverability == DiscoverabilityMode.DISCOVERABILITY_LIMITED) {
+        bluetoothAdapter.setDiscoverableTimeout(
+          Duration.ofSeconds(120)
+        ) // limited discoverability needs a timeout, 120s is Android default
+      }
+      SetDiscoverabilityModeResponse.getDefaultInstance()
+    }
+  }
+
   override fun runDiscovery(
     request: RunDiscoveryRequest,
     responseObserver: StreamObserver<RunDiscoveryResponse>
@@ -477,4 +604,14 @@
       }
     }
   }
+
+  override fun getDeviceName(
+    request: GetDeviceNameRequest,
+    responseObserver: StreamObserver<GetDeviceNameResponse>
+  ) {
+    grpcUnary(scope, responseObserver) {
+      val device = request.connection.toBluetoothDevice(bluetoothAdapter)
+      GetDeviceNameResponse.newBuilder().setName(device.name).build()
+    }
+  }
 }
diff --git a/android/pandora/server/src/com/android/pandora/Security.kt b/android/pandora/server/src/com/android/pandora/Security.kt
index e7ded29..01247f0 100644
--- a/android/pandora/server/src/com/android/pandora/Security.kt
+++ b/android/pandora/server/src/com/android/pandora/Security.kt
@@ -18,9 +18,15 @@
 
 import android.bluetooth.BluetoothAdapter
 import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothDevice.ACTION_PAIRING_REQUEST
+import android.bluetooth.BluetoothDevice.BOND_BONDED
 import android.bluetooth.BluetoothDevice.DEVICE_TYPE_CLASSIC
+import android.bluetooth.BluetoothDevice.DEVICE_TYPE_DUAL
 import android.bluetooth.BluetoothDevice.DEVICE_TYPE_LE
 import android.bluetooth.BluetoothDevice.EXTRA_PAIRING_VARIANT
+import android.bluetooth.BluetoothDevice.TRANSPORT_AUTO
+import android.bluetooth.BluetoothDevice.TRANSPORT_BREDR
+import android.bluetooth.BluetoothDevice.TRANSPORT_LE
 import android.bluetooth.BluetoothManager
 import android.content.Context
 import android.content.Intent
@@ -31,6 +37,7 @@
 import io.grpc.stub.StreamObserver
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
 import kotlinx.coroutines.cancel
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
@@ -57,6 +64,7 @@
   init {
     val intentFilter = IntentFilter()
     intentFilter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST)
+    intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
 
     flow = intentFlow(context, intentFilter).shareIn(globalScope, SharingStarted.Eagerly)
   }
@@ -68,8 +76,17 @@
   override fun pair(request: PairRequest, responseObserver: StreamObserver<Empty>) {
     grpcUnary(globalScope, responseObserver) {
       val bluetoothDevice = request.connection.toBluetoothDevice(bluetoothAdapter)
-      Log.i(TAG, "pair: ${bluetoothDevice.address}")
-      bluetoothDevice.createBond()
+      Log.i(
+        TAG,
+        "pair: ${bluetoothDevice.address} (current bond state: ${bluetoothDevice.bondState})"
+      )
+      bluetoothDevice.createBond(
+        when (request.connection.transport!!) {
+          Transport.TRANSPORT_LE -> TRANSPORT_LE
+          Transport.TRANSPORT_BREDR -> TRANSPORT_BREDR
+          Transport.UNRECOGNIZED -> TRANSPORT_AUTO
+        }
+      )
       Empty.getDefaultInstance()
     }
   }
@@ -78,26 +95,28 @@
     request: DeletePairingRequest,
     responseObserver: StreamObserver<DeletePairingResponse>
   ) {
-    grpcUnary<DeletePairingResponse>(globalScope, responseObserver) {
+    grpcUnary(globalScope, responseObserver) {
       val bluetoothDevice = request.address.toBluetoothDevice(bluetoothAdapter)
       Log.i(TAG, "DeletePairing: device=$bluetoothDevice")
 
+      val unbonded =
+        globalScope.async {
+          flow
+            .filter { it.action == BluetoothDevice.ACTION_BOND_STATE_CHANGED }
+            .filter { it.getBluetoothDeviceExtra() == bluetoothDevice }
+            .filter {
+              it.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothAdapter.ERROR) ==
+                BluetoothDevice.BOND_NONE
+            }
+            .first()
+        }
+
       if (bluetoothDevice.removeBond()) {
         Log.i(TAG, "DeletePairing: device=$bluetoothDevice - wait BOND_NONE intent")
-        flow
-          .filter { it.getAction() == BluetoothDevice.ACTION_BOND_STATE_CHANGED }
-          .filter { it.getBluetoothDeviceExtra() == bluetoothDevice }
-          .filter {
-            it.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothAdapter.ERROR) ==
-              BluetoothDevice.BOND_NONE
-          }
-          .filter {
-            it.getIntExtra(BluetoothDevice.EXTRA_REASON, BluetoothAdapter.ERROR) ==
-              BluetoothDevice.BOND_SUCCESS
-          }
-          .first()
+        unbonded.await()
       } else {
         Log.i(TAG, "DeletePairing: device=$bluetoothDevice - Already unpaired")
+        unbonded.cancel()
       }
       DeletePairingResponse.getDefaultInstance()
     }
@@ -125,63 +144,62 @@
         }
         .launchIn(this)
 
-      flow.map { intent ->
-        val device = intent.getBluetoothDeviceExtra()
-        val variant = intent.getIntExtra(EXTRA_PAIRING_VARIANT, BluetoothDevice.ERROR)
-        Log.i(
-          TAG,
-          "OnPairing: Handling PairingEvent ${variant} for device ${device.address}"
-        )
-        val eventBuilder =
-          PairingEvent.newBuilder().setAddress(ByteString.copyFrom(device.toByteArray()))
-        when (variant) {
-          // SSP / LE Just Works
-          BluetoothDevice.PAIRING_VARIANT_CONSENT ->
-            eventBuilder.justWorks = Empty.getDefaultInstance()
+      flow
+        .filter { intent -> intent.action == ACTION_PAIRING_REQUEST }
+        .map { intent ->
+          val device = intent.getBluetoothDeviceExtra()
+          val variant = intent.getIntExtra(EXTRA_PAIRING_VARIANT, BluetoothDevice.ERROR)
+          Log.i(TAG, "OnPairing: Handling PairingEvent ${variant} for device ${device.address}")
+          val eventBuilder =
+            PairingEvent.newBuilder().setAddress(device.toByteString())
+          when (variant) {
+            // SSP / LE Just Works
+            BluetoothDevice.PAIRING_VARIANT_CONSENT ->
+              eventBuilder.justWorks = Empty.getDefaultInstance()
 
-          // SSP / LE Numeric Comparison
-          BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION ->
-            eventBuilder.numericComparison =
-              intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR)
-          BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY -> {
-            val passkey =
-              intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR)
-            eventBuilder.passkeyEntryNotification = passkey
-          }
-
-          // Out-Of-Band not currently supported
-          BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT ->
-            error("Received OOB pairing confirmation (UNSUPPORTED)")
-
-          // Legacy PIN entry, or LE legacy passkey entry, depending on transport
-          BluetoothDevice.PAIRING_VARIANT_PIN ->
-            when (device.type) {
-              DEVICE_TYPE_CLASSIC -> eventBuilder.pinCodeRequest = Empty.getDefaultInstance()
-              DEVICE_TYPE_LE ->
-                eventBuilder.passkeyEntryRequest = Empty.getDefaultInstance()
-              else -> error("cannot determine pairing variant, since transport is unknown")
+            // SSP / LE Numeric Comparison
+            BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION ->
+              eventBuilder.numericComparison =
+                intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR)
+            BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY -> {
+              val passkey =
+                intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR)
+              eventBuilder.passkeyEntryNotification = passkey
             }
-          BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS ->
-            eventBuilder.pinCodeRequest = Empty.getDefaultInstance()
 
-          // Legacy PIN entry or LE legacy passkey entry, except we just generate the PIN in the
-          // stack and display it to the user for convenience
-          BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN -> {
-            val passkey =
-              intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR)
-            when (device.type) {
-              DEVICE_TYPE_CLASSIC ->
-                eventBuilder.pinCodeNotification =
-                  ByteString.copyFrom(passkey.toString().toByteArray())
-              DEVICE_TYPE_LE -> eventBuilder.passkeyEntryNotification = passkey
-              else -> error("cannot determine pairing variant, since transport is unknown")
+            // Out-Of-Band not currently supported
+            BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT ->
+              error("Received OOB pairing confirmation (UNSUPPORTED)")
+
+            // Legacy PIN entry, or LE legacy passkey entry, depending on transport
+            BluetoothDevice.PAIRING_VARIANT_PIN ->
+              when (device.type) {
+                DEVICE_TYPE_CLASSIC -> eventBuilder.pinCodeRequest = Empty.getDefaultInstance()
+                DEVICE_TYPE_LE -> eventBuilder.passkeyEntryRequest = Empty.getDefaultInstance()
+                else -> error("cannot determine pairing variant, since transport is unknown")
+              }
+            BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS ->
+              eventBuilder.pinCodeRequest = Empty.getDefaultInstance()
+
+            // Legacy PIN entry or LE legacy passkey entry, except we just generate the PIN in
+            // the
+            // stack and display it to the user for convenience
+            BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN -> {
+              val passkey =
+                intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR)
+              when (device.type) {
+                DEVICE_TYPE_CLASSIC ->
+                  eventBuilder.pinCodeNotification =
+                    ByteString.copyFrom(passkey.toString().toByteArray())
+                DEVICE_TYPE_LE -> eventBuilder.passkeyEntryNotification = passkey
+                else -> error("cannot determine pairing variant, since transport is unknown")
+              }
+            }
+            else -> {
+              error("Received unknown pairing variant $variant")
             }
           }
-          else -> {
-            error("Received unknown pairing variant $variant")
-          }
+          eventBuilder.build()
         }
-        eventBuilder.build()
-      }
     }
 }
diff --git a/android/pandora/server/src/com/android/pandora/Utils.kt b/android/pandora/server/src/com/android/pandora/Utils.kt
index d2d27db..a59deb0 100644
--- a/android/pandora/server/src/com/android/pandora/Utils.kt
+++ b/android/pandora/server/src/com/android/pandora/Utils.kt
@@ -53,6 +53,8 @@
 import kotlinx.coroutines.withTimeout
 import kotlinx.coroutines.withTimeoutOrNull
 import pandora.HostProto.Connection
+import pandora.HostProto.InternalConnectionRef
+import pandora.HostProto.Transport
 
 fun shell(cmd: String): String {
   val fd = InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(cmd)
@@ -284,7 +286,7 @@
 }
 
 fun Intent.getBluetoothDeviceExtra(): BluetoothDevice =
-  this.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
+  this.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)!!
 
 fun ByteString.decodeAsMacAddressToString(): String =
   MacAddress.fromBytes(this.toByteArray()).toString().uppercase()
@@ -293,6 +295,24 @@
   adapter.getRemoteDevice(this.decodeAsMacAddressToString())
 
 fun Connection.toBluetoothDevice(adapter: BluetoothAdapter): BluetoothDevice =
-  adapter.getRemoteDevice(this.cookie.toByteArray().decodeToString())
+  adapter.getRemoteDevice(address)
 
-fun BluetoothDevice.toByteArray(): ByteArray = MacAddress.fromString(this.address).toByteArray()
+val Connection.address: String
+  get() = InternalConnectionRef.parseFrom(this.cookie).address.decodeAsMacAddressToString()
+
+val Connection.transport: Transport
+  get() = InternalConnectionRef.parseFrom(this.cookie).transport
+
+fun newConnection(device: BluetoothDevice, transport: Transport) =
+  Connection.newBuilder()
+    .setCookie(
+      InternalConnectionRef.newBuilder()
+        .setAddress(device.toByteString())
+        .setTransport(transport)
+        .build()
+        .toByteString()
+    )
+    .build()!!
+
+fun BluetoothDevice.toByteString() =
+  ByteString.copyFrom(MacAddress.fromString(this.address).toByteArray())!!
diff --git a/system/bta/gatt/bta_gattc_act.cc b/system/bta/gatt/bta_gattc_act.cc
index 9de15c9..ecb9a87 100644
--- a/system/bta/gatt/bta_gattc_act.cc
+++ b/system/bta/gatt/bta_gattc_act.cc
@@ -768,6 +768,26 @@
       p_clcb->p_srcb->update_count = 0;
       p_clcb->p_srcb->state = BTA_GATTC_SERV_DISC_ACT;
 
+      /* This is workaround for the embedded devices being already on the market
+       * and having a serious problem with handle Read By Type with
+       * GATT_UUID_DATABASE_HASH. With this workaround, Android will assume that
+       * embedded device having LMP version lower than 5.1 (0x0a), it does not
+       * support GATT Caching.
+       */
+      uint8_t lmp_version = 0;
+      if (!BTM_ReadRemoteVersion(p_clcb->bda, &lmp_version, nullptr, nullptr)) {
+        LOG_WARN("Could not read remote version for %s",
+                 p_clcb->bda.ToString().c_str());
+      }
+
+      if (lmp_version < 0x0a) {
+        LOG_WARN(
+            " Device LMP version 0x%02x < Bluetooth 5.1. Ignore database cache "
+            "read.",
+            lmp_version);
+        p_clcb->p_srcb->srvc_hdl_db_hash = false;
+      }
+
       /* read db hash if db hash characteristic exists */
       if (bta_gattc_is_robust_caching_enabled() &&
           p_clcb->p_srcb->srvc_hdl_db_hash &&
diff --git a/system/bta/hf_client/bta_hf_client_rfc.cc b/system/bta/hf_client/bta_hf_client_rfc.cc
index d964003b..2999eb9 100644
--- a/system/bta/hf_client/bta_hf_client_rfc.cc
+++ b/system/bta/hf_client/bta_hf_client_rfc.cc
@@ -116,6 +116,7 @@
       if (client_cb == NULL) {
         APPL_TRACE_ERROR("%s: error allocating a new handle", __func__);
         p_buf->hdr.event = BTA_HF_CLIENT_RFC_CLOSE_EVT;
+        RFCOMM_RemoveConnection(port_handle);
       } else {
         // Set the connection fields for this new CB
         client_cb->conn_handle = port_handle;
diff --git a/system/bta/vc/vc.cc b/system/bta/vc/vc.cc
index 403b93d..1d406a9 100644
--- a/system/bta/vc/vc.cc
+++ b/system/bta/vc/vc.cc
@@ -743,13 +743,23 @@
                                      int group_id, bool is_autonomous,
                                      uint8_t opcode,
                                      std::vector<uint8_t>& arguments) {
-    DLOG(INFO) << __func__ << " num of devices: " << devices.size()
-               << " group_id: " << group_id
-               << " is_autonomous: " << is_autonomous << " opcode: " << +opcode
-               << " arg size: " << arguments.size();
+    LOG_DEBUG(
+        "num of devices: %zu, group_id: %d, is_autonomous: %s  opcode: %d, arg "
+        "size: %zu",
+        devices.size(), group_id, is_autonomous ? "true" : "false", +opcode,
+        arguments.size());
 
-    ongoing_operations_.emplace_back(latest_operation_id_++, group_id,
-                                     is_autonomous, opcode, arguments, devices);
+    if (std::find_if(ongoing_operations_.begin(), ongoing_operations_.end(),
+                     [opcode, &arguments](const VolumeOperation& op) {
+                       return (op.opcode_ == opcode) &&
+                              std::equal(op.arguments_.begin(),
+                                         op.arguments_.end(),
+                                         arguments.begin());
+                     }) == ongoing_operations_.end()) {
+      ongoing_operations_.emplace_back(latest_operation_id_++, group_id,
+                                       is_autonomous, opcode, arguments,
+                                       devices);
+    }
   }
 
   void MuteUnmute(std::variant<RawAddress, int> addr_or_group_id, bool mute) {
@@ -760,11 +770,13 @@
     if (std::holds_alternative<RawAddress>(addr_or_group_id)) {
       LOG_DEBUG("Address: %s: ",
                 (std::get<RawAddress>(addr_or_group_id)).ToString().c_str());
-      std::vector<RawAddress> devices = {
-          std::get<RawAddress>(addr_or_group_id)};
-
-      PrepareVolumeControlOperation(devices, bluetooth::groups::kGroupUnknown,
-                                    false, opcode, arg);
+      VolumeControlDevice* dev = volume_control_devices_.FindByAddress(
+          std::get<RawAddress>(addr_or_group_id));
+      if (dev && dev->IsConnected()) {
+        std::vector<RawAddress> devices = {dev->address};
+        PrepareVolumeControlOperation(devices, bluetooth::groups::kGroupUnknown,
+                                      false, opcode, arg);
+      }
     } else {
       /* Handle group change */
       auto group_id = std::get<int>(addr_or_group_id);
@@ -815,14 +827,17 @@
     uint8_t opcode = kControlPointOpcodeSetAbsoluteVolume;
 
     if (std::holds_alternative<RawAddress>(addr_or_group_id)) {
-      DLOG(INFO) << __func__ << " " << std::get<RawAddress>(addr_or_group_id);
-      std::vector<RawAddress> devices = {
-          std::get<RawAddress>(addr_or_group_id)};
-
-      RemovePendingVolumeControlOperations(devices,
-                                           bluetooth::groups::kGroupUnknown);
-      PrepareVolumeControlOperation(devices, bluetooth::groups::kGroupUnknown,
-                                    false, opcode, arg);
+      LOG_DEBUG("Address: %s: ",
+                std::get<RawAddress>(addr_or_group_id).ToString().c_str());
+      VolumeControlDevice* dev = volume_control_devices_.FindByAddress(
+          std::get<RawAddress>(addr_or_group_id));
+      if (dev && dev->IsConnected() && (dev->volume != volume)) {
+        std::vector<RawAddress> devices = {dev->address};
+        RemovePendingVolumeControlOperations(devices,
+                                             bluetooth::groups::kGroupUnknown);
+        PrepareVolumeControlOperation(devices, bluetooth::groups::kGroupUnknown,
+                                      false, opcode, arg);
+      }
     } else {
       /* Handle group change */
       auto group_id = std::get<int>(addr_or_group_id);
diff --git a/system/bta/vc/vc_test.cc b/system/bta/vc/vc_test.cc
index bdc93e1..71fca36 100644
--- a/system/bta/vc/vc_test.cc
+++ b/system/bta/vc/vc_test.cc
@@ -915,10 +915,37 @@
 };
 
 TEST_F(VolumeControlValueSetTest, test_set_volume) {
-  std::vector<uint8_t> expected_data({0x04, 0x00, 0x10});
-  EXPECT_CALL(gatt_queue, WriteCharacteristic(conn_id, 0x0024, expected_data,
-                                              GATT_WRITE, _, _));
+  ON_CALL(gatt_queue, WriteCharacteristic(conn_id, 0x0024, _, GATT_WRITE, _, _))
+      .WillByDefault([this](uint16_t conn_id, uint16_t handle,
+                            std::vector<uint8_t> value,
+                            tGATT_WRITE_TYPE write_type, GATT_WRITE_OP_CB cb,
+                            void* cb_data) {
+        std::vector<uint8_t> ntf_value({
+            value[2],                            // volume level
+            0,                                   // muted
+            static_cast<uint8_t>(value[1] + 1),  // change counter
+        });
+        GetNotificationEvent(0x0021, ntf_value);
+      });
+
+  const std::vector<uint8_t> vol_x10({0x04, 0x00, 0x10});
+  EXPECT_CALL(gatt_queue,
+              WriteCharacteristic(conn_id, 0x0024, vol_x10, GATT_WRITE, _, _))
+      .Times(1);
   VolumeControl::Get()->SetVolume(test_address, 0x10);
+
+  // Same volume level should not be applied twice
+  const std::vector<uint8_t> vol_x10_2({0x04, 0x01, 0x10});
+  EXPECT_CALL(gatt_queue,
+              WriteCharacteristic(conn_id, 0x0024, vol_x10_2, GATT_WRITE, _, _))
+      .Times(0);
+  VolumeControl::Get()->SetVolume(test_address, 0x10);
+
+  const std::vector<uint8_t> vol_x20({0x04, 0x01, 0x20});
+  EXPECT_CALL(gatt_queue,
+              WriteCharacteristic(conn_id, 0x0024, vol_x20, GATT_WRITE, _, _))
+      .Times(1);
+  VolumeControl::Get()->SetVolume(test_address, 0x20);
 }
 
 TEST_F(VolumeControlValueSetTest, test_mute) {
diff --git a/system/gd/rust/common/src/init_flags.rs b/system/gd/rust/common/src/init_flags.rs
index 4fabae0..87a68dc 100644
--- a/system/gd/rust/common/src/init_flags.rs
+++ b/system/gd/rust/common/src/init_flags.rs
@@ -74,6 +74,7 @@
 
 init_flags!(
     flags: {
+        sdp_serialization,
         gd_core,
         gd_security,
         gd_l2cap,
diff --git a/system/gd/rust/shim/src/init_flags.rs b/system/gd/rust/shim/src/init_flags.rs
index fd3015c..d993240 100644
--- a/system/gd/rust/shim/src/init_flags.rs
+++ b/system/gd/rust/shim/src/init_flags.rs
@@ -4,6 +4,7 @@
         fn load(flags: Vec<String>);
         fn set_all_for_testing();
 
+        fn sdp_serialization_is_enabled() -> bool;
         fn gd_core_is_enabled() -> bool;
         fn gd_security_is_enabled() -> bool;
         fn gd_l2cap_is_enabled() -> bool;
diff --git a/system/stack/sdp/sdp_main.cc b/system/stack/sdp/sdp_main.cc
index 1cdeb96..9719d0f 100644
--- a/system/stack/sdp/sdp_main.cc
+++ b/system/stack/sdp/sdp_main.cc
@@ -22,8 +22,10 @@
  *
  ******************************************************************************/
 
+#include <base/logging.h>
 #include <string.h>  // memset
 
+#include "gd/common/init_flags.h"
 #include "osi/include/allocator.h"
 #include "osi/include/osi.h"  // UNUSED_ATTR
 #include "stack/include/bt_hdr.h"
@@ -34,8 +36,6 @@
 #include "stack/sdp/sdpint.h"
 #include "types/raw_address.h"
 
-#include <base/logging.h>
-
 /******************************************************************************/
 /*                     G L O B A L      S D P       D A T A                   */
 /******************************************************************************/
@@ -351,7 +351,8 @@
   // Look for any active sdp connection on the remote device
   cid = sdpu_get_active_ccb_cid(p_bd_addr);
 
-  if (cid == 0) {
+  if (!bluetooth::common::init_flags::sdp_serialization_is_enabled() ||
+      cid == 0) {
     p_ccb->con_state = SDP_STATE_CONN_SETUP;
     cid = L2CA_ConnectReq2(BT_PSM_SDP, p_bd_addr, BTM_SEC_NONE);
   } else {
diff --git a/system/stack/test/sdp/stack_sdp_test.cc b/system/stack/test/sdp/stack_sdp_test.cc
index 0251aab..98bb136 100644
--- a/system/stack/test/sdp/stack_sdp_test.cc
+++ b/system/stack/test/sdp/stack_sdp_test.cc
@@ -110,6 +110,8 @@
 }
 
 TEST_F(StackSdpMainTest, sdp_service_search_request_queuing) {
+  bluetooth::common::InitFlags::SetAllForTesting();
+
   ASSERT_TRUE(SDP_ServiceSearchRequest(addr, sdp_db, nullptr));
   const int cid = L2CA_ConnectReq2_cid;
   tCONN_CB* p_ccb1 = find_ccb(cid, SDP_STATE_CONN_SETUP);
diff --git a/system/tools/scripts/dump_le_audio.py b/system/tools/scripts/dump_le_audio.py
index 806cdfb..048facb 100755
--- a/system/tools/scripts/dump_le_audio.py
+++ b/system/tools/scripts/dump_le_audio.py
@@ -79,6 +79,13 @@
 # opcode for hci command
 OPCODE_HCI_CREATE_CIS = 0x2064
 OPCODE_REMOVE_ISO_DATA_PATH = 0x206F
+OPCODE_LE_SET_PERIODIC_ADVERTISING_DATA = 0x203F
+OPCODE_LE_CREATE_BIG = 0x2068
+OPCODE_LE_SETUP_ISO_DATA_PATH = 0x206E
+
+# HCI event
+EVENT_CODE_LE_META_EVENT = 0x3E
+SUBEVENT_CODE_LE_CREATE_BIG_COMPLETE = 0x1B
 
 TYPE_STREAMING_AUDIO_CONTEXTS = 0x02
 
@@ -114,6 +121,9 @@
 AUDIO_LOCATION_RIGHT = 0x02
 AUDIO_LOCATION_CENTER = 0x04
 
+AD_TYPE_SERVICE_DATA_16_BIT = 0x16
+BASIC_AUDIO_ANNOUNCEMENT_SERVICE = 0x1851
+
 packet_number = 0
 debug_enable = False
 add_header = False
@@ -158,35 +168,77 @@
         print("octets_per_frame: " + str(self.octets_per_frame))
 
 
+class Broadcast:
+
+    def __init__(self):
+        self.num_of_bis = defaultdict(int)  # subgroup - num_of_bis
+        self.bis = defaultdict(BisStream)  # bis_index - codec_config
+        self.bis_index_handle_map = defaultdict(int)  # bis_index - bis_handle
+        self.bis_index_list = []
+
+    def dump(self):
+        for bis_index, iso_stream in self.bis.items():
+            print("bis_index: " + str(bis_index) + " bis handle: " + str(self.bis_index_handle_map[bis_index]))
+            iso_stream.dump()
+
+
+class BisStream:
+
+    def __init__(self):
+        self.sampling_frequencies = 0xFF
+        self.frame_duration = 0xFF
+        self.channel_allocation = 0xFFFFFFFF
+        self.octets_per_frame = 0xFFFF
+        self.output_dump = []
+        self.start_time = 0xFFFFFFFF
+
+    def dump(self):
+        print("start_time: " + str(self.start_time))
+        print("sampling_frequencies: " + str(self.sampling_frequencies))
+        print("frame_duration: " + str(self.frame_duration))
+        print("channel_allocation: " + str(self.channel_allocation))
+        print("octets_per_frame: " + str(self.octets_per_frame))
+
+
 connection_map = defaultdict(Connection)
 cis_acl_map = defaultdict(int)
+broadcast_map = defaultdict(Broadcast)
+big_adv_map = defaultdict(int)
+bis_stream_map = defaultdict(BisStream)
 
 
-def generate_header(file, connection):
+def generate_header(file, stream, is_cis):
+    sf_case = {
+        SAMPLE_FREQUENCY_8000: 80,
+        SAMPLE_FREQUENCY_11025: 110,
+        SAMPLE_FREQUENCY_16000: 160,
+        SAMPLE_FREQUENCY_22050: 220,
+        SAMPLE_FREQUENCY_24000: 240,
+        SAMPLE_FREQUENCY_32000: 320,
+        SAMPLE_FREQUENCY_44100: 441,
+        SAMPLE_FREQUENCY_48000: 480,
+        SAMPLE_FREQUENCY_88200: 882,
+        SAMPLE_FREQUENCY_96000: 960,
+        SAMPLE_FREQUENCY_176400: 1764,
+        SAMPLE_FREQUENCY_192000: 1920,
+        SAMPLE_FREQUENCY_384000: 2840,
+    }
+    fd_case = {FRAME_DURATION_7_5: 7.5, FRAME_DURATION_10: 10}
+    al_case = {AUDIO_LOCATION_MONO: 1, AUDIO_LOCATION_LEFT: 1, AUDIO_LOCATION_RIGHT: 1, AUDIO_LOCATION_CENTER: 2}
+
     header = bytearray.fromhex('1ccc1200')
-    for ase in connection.ase.values():
-        sf_case = {
-            SAMPLE_FREQUENCY_8000: 80,
-            SAMPLE_FREQUENCY_11025: 110,
-            SAMPLE_FREQUENCY_16000: 160,
-            SAMPLE_FREQUENCY_22050: 220,
-            SAMPLE_FREQUENCY_24000: 240,
-            SAMPLE_FREQUENCY_32000: 320,
-            SAMPLE_FREQUENCY_44100: 441,
-            SAMPLE_FREQUENCY_48000: 480,
-            SAMPLE_FREQUENCY_88200: 882,
-            SAMPLE_FREQUENCY_96000: 960,
-            SAMPLE_FREQUENCY_176400: 1764,
-            SAMPLE_FREQUENCY_192000: 1920,
-            SAMPLE_FREQUENCY_384000: 2840,
-        }
-        header = header + struct.pack("<H", sf_case[ase.sampling_frequencies])
-        fd_case = {FRAME_DURATION_7_5: 7.5, FRAME_DURATION_10: 10}
-        header = header + struct.pack("<H", int(ase.octets_per_frame * 8 * 10 / fd_case[ase.frame_duration]))
-        al_case = {AUDIO_LOCATION_MONO: 1, AUDIO_LOCATION_LEFT: 1, AUDIO_LOCATION_RIGHT: 1, AUDIO_LOCATION_CENTER: 2}
-        header = header + struct.pack("<HHHL", al_case[ase.channel_allocation], fd_case[ase.frame_duration] * 100, 0,
-                                      48000000)
-        break
+    if is_cis:
+        for ase in stream.ase.values():
+            header = header + struct.pack("<H", sf_case[ase.sampling_frequencies])
+            header = header + struct.pack("<H", int(ase.octets_per_frame * 8 * 10 / fd_case[ase.frame_duration]))
+            header = header + struct.pack("<HHHL", al_case[ase.channel_allocation], fd_case[ase.frame_duration] * 100,
+                                          0, 48000000)
+            break
+    else:
+        header = header + struct.pack("<H", sf_case[stream.sampling_frequencies])
+        header = header + struct.pack("<H", int(stream.octets_per_frame * 8 * 10 / fd_case[stream.frame_duration]))
+        header = header + struct.pack("<HHHL", al_case[stream.channel_allocation], fd_case[stream.frame_duration] * 100,
+                                      0, 48000000)
     file.write(header)
 
 
@@ -206,7 +258,7 @@
             ase.frame_duration = value
         elif config_type == TYPE_CHANNEL_ALLOCATION:
             ase.channel_allocation = value
-        elif TYPE_OCTETS_PER_FRAME:
+        elif config_type == TYPE_OCTETS_PER_FRAME:
             ase.octets_per_frame = value
         length -= (config_length + 1)
 
@@ -284,6 +336,64 @@
     packet_handle.get((opcode, flags), lambda x, y, z: None)(packet, connection_handle, timestamp)
 
 
+def parse_big_codec_information(adv_handle, packet):
+    # Ignore presentation delay
+    packet = unpack_data(packet, 3, True)
+    number_of_subgroup, packet = unpack_data(packet, 1, False)
+    for subgroup in range(number_of_subgroup):
+        num_of_bis, packet = unpack_data(packet, 1, False)
+        broadcast_map[adv_handle].num_of_bis[subgroup] = num_of_bis
+        # Ignore codec id
+        packet = unpack_data(packet, 5, True)
+        length, packet = unpack_data(packet, 1, False)
+        if len(packet) < length:
+            print("Invalid subgroup codec information length")
+            return
+
+        while length > 0:
+            config_length, packet = unpack_data(packet, 1, False)
+            config_type, packet = unpack_data(packet, 1, False)
+            value, packet = unpack_data(packet, config_length - 1, False)
+            if config_type == TYPE_SAMPLING_FREQUENCIES:
+                sampling_frequencies = value
+            elif config_type == TYPE_FRAME_DURATION:
+                frame_duration = value
+            elif config_type == TYPE_OCTETS_PER_FRAME:
+                octets_per_frame = value
+            else:
+                print("Unknown config type")
+            length -= (config_length + 1)
+
+        # Ignore metadata
+        metadata_length, packet = unpack_data(packet, 1, False)
+        packet = unpack_data(packet, metadata_length, True)
+
+        for count in range(num_of_bis):
+            bis_index, packet = unpack_data(packet, 1, False)
+            broadcast_map[adv_handle].bis_index_list.append(bis_index)
+            length, packet = unpack_data(packet, 1, False)
+            if len(packet) < length:
+                print("Invalid level 3 codec information length")
+                return
+
+            while length > 0:
+                config_length, packet = unpack_data(packet, 1, False)
+                config_type, packet = unpack_data(packet, 1, False)
+                value, packet = unpack_data(packet, config_length - 1, False)
+                if config_type == TYPE_CHANNEL_ALLOCATION:
+                    channel_allocation = value
+                else:
+                    print("Ignored config type")
+                length -= (config_length + 1)
+
+            broadcast_map[adv_handle].bis[bis_index].sampling_frequencies = sampling_frequencies
+            broadcast_map[adv_handle].bis[bis_index].frame_duration = frame_duration
+            broadcast_map[adv_handle].bis[bis_index].octets_per_frame = octets_per_frame
+            broadcast_map[adv_handle].bis[bis_index].channel_allocation = channel_allocation
+
+    return packet
+
+
 def debug_print(log):
     global packet_number
     print("#" + str(packet_number) + ": " + log)
@@ -303,7 +413,7 @@
     return value, data[byte:]
 
 
-def parse_command_packet(packet):
+def parse_command_packet(packet, timestamp):
     opcode, packet = unpack_data(packet, 2, False)
     if opcode == OPCODE_HCI_CREATE_CIS:
         debug_print("OPCODE_HCI_CREATE_CIS")
@@ -330,9 +440,96 @@
             debug_print("Invalid cmd length")
             return
 
-        cis_handle, packet = unpack_data(packet, 2, False)
-        acl_handle = cis_acl_map[cis_handle]
-        dump_audio_data_to_file(acl_handle)
+        iso_handle, packet = unpack_data(packet, 2, False)
+        # CIS stream
+        if iso_handle in cis_acl_map:
+            acl_handle = cis_acl_map[iso_handle]
+            dump_cis_audio_data_to_file(acl_handle)
+        # To Do: BIS stream
+        elif iso_handle in bis_stream_map:
+            dump_bis_audio_data_to_file(iso_handle)
+    elif opcode == OPCODE_LE_SET_PERIODIC_ADVERTISING_DATA:
+        debug_print("OPCODE_LE_SET_PERIODIC_ADVERTISING_DATA")
+
+        length, packet = unpack_data(packet, 1, False)
+        if length != len(packet):
+            debug_print("Invalid cmd length")
+            return
+
+        if length < 21:
+            debug_print("Ignored. Not basic audio announcement")
+            return
+
+        adv_hdl, packet = unpack_data(packet, 1, False)
+        #ignore operation, advertising_data_length
+        packet = unpack_data(packet, 2, True)
+        length, packet = unpack_data(packet, 1, False)
+        if length != len(packet):
+            debug_print("Invalid AD element length")
+            return
+
+        ad_type, packet = unpack_data(packet, 1, False)
+        service, packet = unpack_data(packet, 2, False)
+        if ad_type != AD_TYPE_SERVICE_DATA_16_BIT or service != BASIC_AUDIO_ANNOUNCEMENT_SERVICE:
+            debug_print("Ignored. Not basic audio announcement")
+            return
+
+        packet = parse_big_codec_information(adv_hdl, packet)
+    elif opcode == OPCODE_LE_CREATE_BIG:
+        debug_print("OPCODE_LE_CREATE_BIG")
+
+        length, packet = unpack_data(packet, 1, False)
+        if length != len(packet) and length < 31:
+            debug_print("Invalid Create BIG command length")
+            return
+
+        big_handle, packet = unpack_data(packet, 1, False)
+        adv_handle, packet = unpack_data(packet, 1, False)
+        big_adv_map[big_handle] = adv_handle
+    elif opcode == OPCODE_LE_SETUP_ISO_DATA_PATH:
+        debug_print("OPCODE_LE_SETUP_ISO_DATA_PATH")
+        length, packet = unpack_data(packet, 1, False)
+        if len(packet) != length:
+            debug_print("Invalid LE SETUP ISO DATA PATH command length")
+            return
+
+        iso_handle, packet = unpack_data(packet, 2, False)
+        if iso_handle in bis_stream_map:
+            bis_stream_map[iso_handle].start_time = timestamp
+
+
+def parse_event_packet(packet):
+    event_code, packet = unpack_data(packet, 1, False)
+    if event_code != EVENT_CODE_LE_META_EVENT:
+        return
+
+    length, packet = unpack_data(packet, 1, False)
+    if len(packet) != length:
+        print("Invalid LE mata event length")
+        return
+
+    subevent_code, packet = unpack_data(packet, 1, False)
+    if subevent_code != SUBEVENT_CODE_LE_CREATE_BIG_COMPLETE:
+        return
+
+    status, packet = unpack_data(packet, 1, False)
+    if status != 0x00:
+        debug_print("Create_BIG failed")
+        return
+
+    big_handle, packet = unpack_data(packet, 1, False)
+    if big_handle not in big_adv_map:
+        print("Invalid BIG handle")
+        return
+    adv_handle = big_adv_map[big_handle]
+    # Ignore, we don't care these parameter
+    packet = unpack_data(packet, 15, True)
+    num_of_bis, packet = unpack_data(packet, 1, False)
+    for count in range(num_of_bis):
+        bis_handle, packet = unpack_data(packet, 2, False)
+        bis_index = broadcast_map[adv_handle].bis_index_list[count]
+        broadcast_map[adv_handle].bis_index_handle_map[bis_index] = bis_handle
+        bis_stream_map[bis_handle] = broadcast_map[adv_handle].bis[bis_index]
 
 
 def convert_time_str(timestamp):
@@ -348,7 +545,7 @@
     return full_str_format
 
 
-def dump_audio_data_to_file(acl_handle):
+def dump_cis_audio_data_to_file(acl_handle):
     if debug_enable:
         connection_map[acl_handle].dump()
     file_name = ""
@@ -389,20 +586,20 @@
         break
 
     if connection_map[acl_handle].input_dump != []:
-        debug_print("Dump input...")
+        debug_print("Dump unicast input...")
         f = open(file_name + "_input.bin", 'wb')
         if add_header == True:
-            generate_header(f, connection_map[acl_handle])
+            generate_header(f, connection_map[acl_handle], True)
         arr = bytearray(connection_map[acl_handle].input_dump)
         f.write(arr)
         f.close()
         connection_map[acl_handle].input_dump = []
 
     if connection_map[acl_handle].output_dump != []:
-        debug_print("Dump output...")
+        debug_print("Dump unicast output...")
         f = open(file_name + "_output.bin", 'wb')
         if add_header == True:
-            generate_header(f, connection_map[acl_handle])
+            generate_header(f, connection_map[acl_handle], True)
         arr = bytearray(connection_map[acl_handle].output_dump)
         f.write(arr)
         f.close()
@@ -411,6 +608,51 @@
     return
 
 
+def dump_bis_audio_data_to_file(iso_handle):
+    if debug_enable:
+        bis_stream_map[iso_handle].dump()
+    file_name = "broadcast"
+    sf_case = {
+        SAMPLE_FREQUENCY_8000: "8000",
+        SAMPLE_FREQUENCY_11025: "11025",
+        SAMPLE_FREQUENCY_16000: "16000",
+        SAMPLE_FREQUENCY_22050: "22050",
+        SAMPLE_FREQUENCY_24000: "24000",
+        SAMPLE_FREQUENCY_32000: "32000",
+        SAMPLE_FREQUENCY_44100: "44100",
+        SAMPLE_FREQUENCY_48000: "48000",
+        SAMPLE_FREQUENCY_88200: "88200",
+        SAMPLE_FREQUENCY_96000: "96000",
+        SAMPLE_FREQUENCY_176400: "176400",
+        SAMPLE_FREQUENCY_192000: "192000",
+        SAMPLE_FREQUENCY_384000: "284000"
+    }
+    file_name += ("_sf" + sf_case[bis_stream_map[iso_handle].sampling_frequencies])
+    fd_case = {FRAME_DURATION_7_5: "7_5", FRAME_DURATION_10: "10"}
+    file_name += ("_fd" + fd_case[bis_stream_map[iso_handle].frame_duration])
+    al_case = {
+        AUDIO_LOCATION_MONO: "mono",
+        AUDIO_LOCATION_LEFT: "left",
+        AUDIO_LOCATION_RIGHT: "right",
+        AUDIO_LOCATION_CENTER: "center"
+    }
+    file_name += ("_" + al_case[bis_stream_map[iso_handle].channel_allocation])
+    file_name += ("_frame" + str(bis_stream_map[iso_handle].octets_per_frame))
+    file_name += ("_" + convert_time_str(bis_stream_map[iso_handle].start_time))
+
+    if bis_stream_map[iso_handle].output_dump != []:
+        debug_print("Dump broadcast output...")
+        f = open(file_name + "_output.bin", 'wb')
+        if add_header == True:
+            generate_header(f, bis_stream_map[iso_handle], False)
+        arr = bytearray(bis_stream_map[iso_handle].output_dump)
+        f.write(arr)
+        f.close()
+        bis_stream_map[iso_handle].output_dump = []
+
+    return
+
+
 def parse_acl_packet(packet, flags, timestamp):
     # Check the minimum acl length, HCI leader (4 bytes)
     # + L2CAP header (4 bytes)
@@ -441,8 +683,8 @@
 
 
 def parse_iso_packet(packet, flags):
-    cis_handle, packet = unpack_data(packet, 2, False)
-    cis_handle &= 0x0EFF
+    iso_handle, packet = unpack_data(packet, 2, False)
+    iso_handle &= 0x0EFF
     iso_data_load_length, packet = unpack_data(packet, 2, False)
     if iso_data_load_length != len(packet):
         debug_print("Invalid iso data load length")
@@ -457,13 +699,18 @@
         debug_print("Invalid iso sdu length")
         return
 
-    acl_handle = cis_acl_map[cis_handle]
-    if flags == SENT:
-        connection_map[acl_handle].output_dump.extend(struct.pack("<H", len(packet)))
-        connection_map[acl_handle].output_dump.extend(list(packet))
-    elif flags == RECEIVED:
-        connection_map[acl_handle].input_dump.extend(struct.pack("<H", len(packet)))
-        connection_map[acl_handle].input_dump.extend(list(packet))
+    # CIS stream
+    if iso_handle in cis_acl_map:
+        acl_handle = cis_acl_map[iso_handle]
+        if flags == SENT:
+            connection_map[acl_handle].output_dump.extend(struct.pack("<H", len(packet)))
+            connection_map[acl_handle].output_dump.extend(list(packet))
+        elif flags == RECEIVED:
+            connection_map[acl_handle].input_dump.extend(struct.pack("<H", len(packet)))
+            connection_map[acl_handle].input_dump.extend(list(packet))
+    elif iso_handle in bis_stream_map:
+        bis_stream_map[iso_handle].output_dump.extend(struct.pack("<H", len(packet)))
+        bis_stream_map[iso_handle].output_dump.extend(list(packet))
 
 
 def parse_next_packet(btsnoop_file):
@@ -490,10 +737,10 @@
         return False
 
     packet_handle = {
-        COMMADN_PACKET: (lambda x, y, z: parse_command_packet(x)),
+        COMMADN_PACKET: (lambda x, y, z: parse_command_packet(x, z)),
         ACL_PACKET: (lambda x, y, z: parse_acl_packet(x, y, z)),
         SCO_PACKET: (lambda x, y, z: None),
-        EVENT_PACKET: (lambda x, y, z: None),
+        EVENT_PACKET: (lambda x, y, z: parse_event_packet(x)),
         ISO_PACKET: (lambda x, y, z: parse_iso_packet(x, y))
     }
     packet_handle.get(type, lambda x, y, z: None)(packet, flags, timestamp)
@@ -535,7 +782,10 @@
                 break
 
     for handle in connection_map.keys():
-        dump_audio_data_to_file(handle)
+        dump_cis_audio_data_to_file(handle)
+
+    for handle in bis_stream_map.keys():
+        dump_bis_audio_data_to_file(handle)
 
 
 if __name__ == "__main__":