Merge "Prefer LE Audio over ASHA if both devices support both profiles." into tm-qpr-dev
diff --git a/android/app/jni/com_android_bluetooth_hfpclient.cpp b/android/app/jni/com_android_bluetooth_hfpclient.cpp
index a36e19c..da4dff9 100644
--- a/android/app/jni/com_android_bluetooth_hfpclient.cpp
+++ b/android/app/jni/com_android_bluetooth_hfpclient.cpp
@@ -879,6 +879,37 @@
   return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
 }
 
+static jboolean sendAndroidAtNative(JNIEnv* env, jobject object,
+                                    jbyteArray address, jstring arg_str) {
+  std::shared_lock<std::shared_mutex> lock(interface_mutex);
+  if (!sBluetoothHfpClientInterface) return JNI_FALSE;
+
+  jbyte* addr = env->GetByteArrayElements(address, NULL);
+  if (!addr) {
+    jniThrowIOException(env, EINVAL);
+    return JNI_FALSE;
+  }
+
+  const char* arg = NULL;
+  if (arg_str != NULL) {
+    arg = env->GetStringUTFChars(arg_str, NULL);
+  }
+
+  bt_status_t status = sBluetoothHfpClientInterface->send_android_at(
+      (const RawAddress*)addr, arg);
+
+  if (status != BT_STATUS_SUCCESS) {
+    ALOGE("FAILED to control volume, status: %d", status);
+  }
+
+  if (arg != NULL) {
+    env->ReleaseStringUTFChars(arg_str, arg);
+  }
+
+  env->ReleaseByteArrayElements(address, addr, 0);
+  return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
+}
+
 static JNINativeMethod sMethods[] = {
     {"classInitNative", "()V", (void*)classInitNative},
     {"initializeNative", "()V", (void*)initializeNative},
@@ -903,6 +934,8 @@
     {"requestLastVoiceTagNumberNative", "([B)Z",
      (void*)requestLastVoiceTagNumberNative},
     {"sendATCmdNative", "([BIIILjava/lang/String;)Z", (void*)sendATCmdNative},
+    {"sendAndroidAtNative", "([BLjava/lang/String;)Z",
+     (void*)sendAndroidAtNative},
 };
 
 int register_com_android_bluetooth_hfpclient(JNIEnv* env) {
diff --git a/android/app/res/values-ky/strings.xml b/android/app/res/values-ky/strings.xml
index 954556f..014f3c5 100644
--- a/android/app/res/values-ky/strings.xml
+++ b/android/app/res/values-ky/strings.xml
@@ -121,7 +121,7 @@
     <string name="bluetooth_map_settings_intro" msgid="4748160773998753325">"Bluetooth аркылуу бөлүшө турган аккаунттарды тандаңыз. Туташкан сайын аккаунттарга кирүүгө уруксат берип турушуңуз керек."</string>
     <string name="bluetooth_map_settings_count" msgid="183013143617807702">"Калган көзөнөктөр:"</string>
     <string name="bluetooth_map_settings_app_icon" msgid="3501432663809664982">"Колдонмонун сүрөтчөсү"</string>
-    <string name="bluetooth_map_settings_title" msgid="4226030082708590023">"Bluetooth билдирүү бөлүшүү жөндөөлөрү"</string>
+    <string name="bluetooth_map_settings_title" msgid="4226030082708590023">"Bluetooth билдирүү бөлүшүү параметрлери"</string>
     <string name="bluetooth_map_settings_no_account_slots_left" msgid="755024228476065757">"Аккаунт тандалбай жатат: 0 орун калды"</string>
     <string name="bluetooth_connected" msgid="5687474377090799447">"Bluetooth аудио туташты"</string>
     <string name="bluetooth_disconnected" msgid="6841396291728343534">"Bluetooth аудио ажыратылды"</string>
diff --git a/android/app/res/values/config.xml b/android/app/res/values/config.xml
index 913c77f..1e062b5 100644
--- a/android/app/res/values/config.xml
+++ b/android/app/res/values/config.xml
@@ -33,6 +33,34 @@
     <integer name="gatt_low_power_min_interval">80</integer>
     <integer name="gatt_low_power_max_interval">100</integer>
 
+    <!-- min/max connection intervals/latencies for companion devices -->
+    <!-- Primary companion -->
+    <integer name="gatt_high_priority_min_interval_primary">6</integer>
+    <integer name="gatt_high_priority_max_interval_primary">8</integer>
+    <integer name="gatt_high_priority_latency_primary">45</integer>
+
+    <integer name="gatt_balanced_priority_min_interval_primary">6</integer>
+    <integer name="gatt_balanced_priority_max_interval_primary">10</integer>
+    <integer name="gatt_balanced_priority_latency_primary">120</integer>
+
+    <integer name="gatt_low_power_min_interval_primary">8</integer>
+    <integer name="gatt_low_power_max_interval_primary">10</integer>
+    <integer name="gatt_low_power_latency_primary">150</integer>
+
+    <!-- Secondary companion -->
+    <integer name="gatt_high_priority_min_interval_secondary">6</integer>
+    <integer name="gatt_high_priority_max_interval_secondary">6</integer>
+    <integer name="gatt_high_priority_latency_secondary">0</integer>
+
+    <integer name="gatt_balanced_priority_min_interval_secondary">12</integer>
+    <integer name="gatt_balanced_priority_max_interval_secondary">12</integer>
+    <integer name="gatt_balanced_priority_latency_secondary">30</integer>
+
+    <integer name="gatt_low_power_min_interval_secondary">80</integer>
+    <integer name="gatt_low_power_max_interval_secondary">100</integer>
+    <integer name="gatt_low_power_latency_secondary">15</integer>
+    <!-- ============================================================ -->
+
     <!-- Specifies latency parameters for high priority, balanced and low power
          GATT configurations. These values represents the number of packets a
          peripheral device is allowed to skip. -->
diff --git a/android/app/res/values/styles.xml b/android/app/res/values/styles.xml
index 91f402d..ea94695 100644
--- a/android/app/res/values/styles.xml
+++ b/android/app/res/values/styles.xml
@@ -31,7 +31,7 @@
         <item name="android:paddingTop">10dip</item>
         <item name="android:textAlignment">viewStart</item>
         <item name="android:textAppearance">@android:style/TextAppearance.Material.Body1</item>
-        <item name="android:textColor">@*android:color/secondary_text_default_material_light</item>
+        <item name="android:textColor">?android:attr/textColorSecondary</item>
     </style>
 
     <style name="file_transfer_item_content">
@@ -42,7 +42,7 @@
         <item name="android:paddingBottom">10dip</item>
         <item name="android:textAlignment">viewStart</item>
         <item name="android:textAppearance">@android:style/TextAppearance.Material.Subhead</item>
-        <item name="android:textColor">@*android:color/primary_text_default_material_light</item>
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
     </style>
 
     <style name="dialog" parent="android:style/Theme.Material.Light.Dialog.Alert" />
diff --git a/android/app/src/com/android/bluetooth/audio_util/MediaPlayerList.java b/android/app/src/com/android/bluetooth/audio_util/MediaPlayerList.java
index 1eebe99..97df3ce 100644
--- a/android/app/src/com/android/bluetooth/audio_util/MediaPlayerList.java
+++ b/android/app/src/com/android/bluetooth/audio_util/MediaPlayerList.java
@@ -31,6 +31,7 @@
 import android.media.session.PlaybackState;
 import android.os.Handler;
 import android.os.Looper;
+import android.os.SystemProperties;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.KeyEvent;
@@ -146,10 +147,45 @@
                 mContext.getMainExecutor(), mMediaKeyEventSessionChangedListener);
     }
 
+    private void constructCurrentPlayers() {
+        // Construct the list of current players
+        d("Initializing list of current media players");
+        List<android.media.session.MediaController> controllers =
+                mMediaSessionManager.getActiveSessions(null);
+
+        for (android.media.session.MediaController controller : controllers) {
+            addMediaPlayer(controller);
+        }
+
+        // If there were any active players and we don't already have one due to the Media
+        // Framework Callbacks then set the highest priority one to active
+        if (mActivePlayerId == 0 && mMediaPlayers.size() > 0) {
+            String packageName = mMediaSessionManager.getMediaKeyEventSessionPackageName();
+            if (!TextUtils.isEmpty(packageName) && haveMediaPlayer(packageName)) {
+                Log.i(TAG, "Set active player to MediaKeyEvent session = " + packageName);
+                setActivePlayer(mMediaPlayerIds.get(packageName));
+            } else {
+                Log.i(TAG, "Set active player to first default");
+                setActivePlayer(1);
+            }
+        }
+    }
+
     public void init(MediaUpdateCallback callback) {
         Log.v(TAG, "Initializing MediaPlayerList");
         mCallback = callback;
 
+        if (!SystemProperties.getBoolean("bluetooth.avrcp.browsable_media_player.enabled", true)) {
+            // Allow to disable BrowsablePlayerConnector with systemproperties.
+            // This is useful when for watches because:
+            //   1. It is not a regular use case
+            //   2. Registering to all players is a very loading task
+
+            Log.i(TAG, "init: without Browsable Player");
+            constructCurrentPlayers();
+            return;
+        }
+
         // Build the list of browsable players and afterwards, build the list of media players
         Intent intent = new Intent(android.service.media.MediaBrowserService.SERVICE_INTERFACE);
         List<ResolveInfo> playerList =
@@ -185,27 +221,7 @@
                             });
                 }
 
-                // Construct the list of current players
-                d("Initializing list of current media players");
-                List<android.media.session.MediaController> controllers =
-                        mMediaSessionManager.getActiveSessions(null);
-
-                for (android.media.session.MediaController controller : controllers) {
-                    addMediaPlayer(controller);
-                }
-
-                // If there were any active players and we don't already have one due to the Media
-                // Framework Callbacks then set the highest priority one to active
-                if (mActivePlayerId == 0 && mMediaPlayers.size() > 0) {
-                    String packageName = mMediaSessionManager.getMediaKeyEventSessionPackageName();
-                    if (!TextUtils.isEmpty(packageName) && haveMediaPlayer(packageName)) {
-                        Log.i(TAG, "Set active player to MediaKeyEvent session = " + packageName);
-                        setActivePlayer(mMediaPlayerIds.get(packageName));
-                    } else {
-                        Log.i(TAG, "Set active player to first default");
-                        setActivePlayer(1);
-                    }
-                }
+                constructCurrentPlayers();
             });
     }
 
diff --git a/android/app/src/com/android/bluetooth/btservice/ActiveDeviceManager.java b/android/app/src/com/android/bluetooth/btservice/ActiveDeviceManager.java
index f043394..127fa31 100644
--- a/android/app/src/com/android/bluetooth/btservice/ActiveDeviceManager.java
+++ b/android/app/src/com/android/bluetooth/btservice/ActiveDeviceManager.java
@@ -20,6 +20,7 @@
 import android.annotation.SuppressLint;
 import android.bluetooth.BluetoothA2dp;
 import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothAudioPolicy;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHapClient;
 import android.bluetooth.BluetoothHeadset;
@@ -689,10 +690,14 @@
         if (headsetService == null) {
             return;
         }
-        if (!headsetService.setActiveDevice(device)) {
-            return;
+        BluetoothAudioPolicy audioPolicy = headsetService.getHfpCallAudioPolicy(device);
+        if (audioPolicy == null || audioPolicy.getConnectingTimePolicy()
+                != BluetoothAudioPolicy.POLICY_NOT_ALLOWED) {
+            if (!headsetService.setActiveDevice(device)) {
+                return;
+            }
+            mHfpActiveDevice = device;
         }
-        mHfpActiveDevice = device;
     }
 
     private void setHearingAidActiveDevice(BluetoothDevice device) {
diff --git a/android/app/src/com/android/bluetooth/btservice/AdapterService.java b/android/app/src/com/android/bluetooth/btservice/AdapterService.java
index c09e164..9d9f887 100644
--- a/android/app/src/com/android/bluetooth/btservice/AdapterService.java
+++ b/android/app/src/com/android/bluetooth/btservice/AdapterService.java
@@ -42,6 +42,7 @@
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothAdapter.ActiveDeviceProfile;
 import android.bluetooth.BluetoothAdapter.ActiveDeviceUse;
+import android.bluetooth.BluetoothAudioPolicy;
 import android.bluetooth.BluetoothClass;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothFrameworkInitializer;
@@ -300,6 +301,7 @@
     private ActiveDeviceManager mActiveDeviceManager;
     private DatabaseManager mDatabaseManager;
     private SilenceDeviceManager mSilenceDeviceManager;
+    private CompanionManager mBtCompanionManager;
     private AppOpsManager mAppOps;
 
     private BluetoothSocketManagerBinder mBluetoothSocketManagerBinder;
@@ -441,6 +443,7 @@
                         getAdapterPropertyNative(AbstractionLayer.BT_PROPERTY_LOCAL_IO_CAPS_BLE);
                         getAdapterPropertyNative(AbstractionLayer.BT_PROPERTY_DYNAMIC_AUDIO_BUFFER);
                         mAdapterStateMachine.sendMessage(AdapterState.BREDR_STARTED);
+                        mBtCompanionManager.loadCompanionInfo();
                     }
                     break;
                 case BluetoothAdapter.STATE_OFF:
@@ -542,6 +545,8 @@
                 Looper.getMainLooper());
         mSilenceDeviceManager.start();
 
+        mBtCompanionManager = new CompanionManager(this, new ServiceFactory());
+
         mBluetoothSocketManagerBinder = new BluetoothSocketManagerBinder(this);
 
         mActivityAttributionService = new ActivityAttributionService();
@@ -1557,6 +1562,32 @@
     }
 
     /**
+     *  Get an metadata of given device and key
+     *
+     *  @param device Bluetooth device
+     *  @param key Metadata key
+     *  @param value Metadata value
+     *  @return if metadata is set successfully
+     */
+    public boolean setMetadata(BluetoothDevice device, int key, byte[] value) {
+        if (value == null || value.length > BluetoothDevice.METADATA_MAX_LENGTH) {
+            return false;
+        }
+        return mDatabaseManager.setCustomMeta(device, key, value);
+    }
+
+    /**
+     *  Get an metadata of given device and key
+     *
+     *  @param device Bluetooth device
+     *  @param key Metadata key
+     *  @return value of given device and key combination
+     */
+    public byte[] getMetadata(BluetoothDevice device, int key) {
+        return mDatabaseManager.getCustomMeta(device, key);
+    }
+
+    /**
      * Handlers for incoming service calls
      */
     private AdapterServiceBinder mBinder;
@@ -3171,6 +3202,10 @@
                 service.mBluetoothKeystoreService.factoryReset();
             }
 
+            if (service.mBtCompanionManager != null) {
+                service.mBtCompanionManager.factoryReset();
+            }
+
             return service.factoryResetNative();
         }
 
@@ -3636,6 +3671,73 @@
         }
 
         @Override
+        public void getAudioPolicyRemoteSupported(BluetoothDevice device,
+                AttributionSource source, SynchronousResultReceiver receiver) {
+            try {
+                receiver.send(getAudioPolicyRemoteSupported(device, source));
+            } catch (RuntimeException e) {
+                receiver.propagateException(e);
+            }
+        }
+        private int getAudioPolicyRemoteSupported(BluetoothDevice device,
+                AttributionSource source) {
+            AdapterService service = getService();
+            if (service == null
+                    || !callerIsSystemOrActiveOrManagedUser(service, TAG,
+                        "getAudioPolicyRemoteSupported")
+                    || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) {
+                return BluetoothAudioPolicy.FEATURE_UNCONFIGURED_BY_REMOTE;
+            }
+            enforceBluetoothPrivilegedPermission(service);
+            return service.getAudioPolicyRemoteSupported(device);
+        }
+
+        @Override
+        public void setAudioPolicy(BluetoothDevice device, BluetoothAudioPolicy policies,
+                AttributionSource source, SynchronousResultReceiver receiver) {
+            try {
+                receiver.send(setAudioPolicy(device, policies, source));
+            } catch (RuntimeException e) {
+                receiver.propagateException(e);
+            }
+        }
+        private int setAudioPolicy(BluetoothDevice device, BluetoothAudioPolicy policies,
+                AttributionSource source) {
+            AdapterService service = getService();
+            if (service == null) {
+                return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
+            } else if (!callerIsSystemOrActiveOrManagedUser(service, TAG, "setAudioPolicy")) {
+                return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ALLOWED;
+            } else if (!Utils.checkConnectPermissionForDataDelivery(
+                    service, source, TAG)) {
+                return BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION;
+            }
+            enforceBluetoothPrivilegedPermission(service);
+            return service.setAudioPolicy(device, policies);
+        }
+
+        @Override
+        public void getAudioPolicy(BluetoothDevice device,
+                AttributionSource source, SynchronousResultReceiver receiver) {
+            try {
+                receiver.send(getAudioPolicy(device, source));
+            } catch (RuntimeException e) {
+                receiver.propagateException(e);
+            }
+        }
+        private BluetoothAudioPolicy getAudioPolicy(BluetoothDevice device,
+                AttributionSource source) {
+            AdapterService service = getService();
+            if (service == null
+                    || !callerIsSystemOrActiveOrManagedUser(service, TAG, "getAudioPolicy")
+                    || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) {
+                return null;
+            }
+            enforceBluetoothPrivilegedPermission(service);
+            return service.getAudioPolicy(device);
+        }
+
+        @Override
         public void requestActivityInfo(IBluetoothActivityEnergyInfoListener listener,
                     AttributionSource source) {
             BluetoothActivityEnergyInfo info = reportActivityInfo(source);
@@ -5458,6 +5560,84 @@
         return getMetricIdNative(Utils.getByteAddress(device));
     }
 
+    public CompanionManager getCompanionManager() {
+        return mBtCompanionManager;
+    }
+
+    /**
+     *  Call for the AdapterService receives bond state change
+     *
+     *  @param device Bluetooth device
+     *  @param state bond state
+     */
+    public void onBondStateChanged(BluetoothDevice device, int state) {
+        if (mBtCompanionManager != null) {
+            mBtCompanionManager.onBondStateChanged(device, state);
+        }
+    }
+
+    /**
+     * Get audio policy feature support status
+     *
+     * @param device Bluetooth device to be checked for audio policy support
+     * @return int status of the remote support for audio policy feature
+     */
+    public int getAudioPolicyRemoteSupported(BluetoothDevice device) {
+        if (mHeadsetClientService != null) {
+            return mHeadsetClientService.getAudioPolicyRemoteSupported(device);
+        } else {
+            Log.e(TAG, "No audio transport connected");
+            return BluetoothAudioPolicy.FEATURE_UNCONFIGURED_BY_REMOTE;
+        }
+    }
+
+    /**
+     * Set audio policy for remote device
+     *
+     * @param device Bluetooth device to be set policy for
+     * @return int result status for setAudioPolicy API
+     */
+    public int setAudioPolicy(BluetoothDevice device, BluetoothAudioPolicy policies) {
+        DeviceProperties deviceProp = mRemoteDevices.getDeviceProperties(device);
+        if (deviceProp == null) {
+            return BluetoothStatusCodes.ERROR_DEVICE_NOT_BONDED;
+        }
+
+        if (mHeadsetClientService != null) {
+            if (getAudioPolicyRemoteSupported(device)
+                    != BluetoothAudioPolicy.FEATURE_SUPPORTED_BY_REMOTE) {
+                Log.w(TAG, "Audio Policy feature not supported by AG");
+                return BluetoothStatusCodes.FEATURE_NOT_SUPPORTED;
+            }
+            deviceProp.setHfAudioPolicyForRemoteAg(policies);
+            mHeadsetClientService.setAudioPolicy(device, policies);
+            return BluetoothStatusCodes.SUCCESS;
+        } else {
+            Log.e(TAG, "HeadsetClient not connected");
+            return BluetoothStatusCodes.ERROR_PROFILE_NOT_CONNECTED;
+        }
+    }
+
+    /**
+     * Get audio policy for remote device
+     *
+     * @param device Bluetooth device to be set policy for
+     * @return {@link BluetoothAudioPolicy} policy stored for the device
+     */
+    public BluetoothAudioPolicy getAudioPolicy(BluetoothDevice device) {
+        DeviceProperties deviceProp = mRemoteDevices.getDeviceProperties(device);
+        if (deviceProp == null) {
+            return null;
+        }
+
+        if (mHeadsetClientService != null) {
+            return deviceProp.getHfAudioPolicyForRemoteAg();
+        } else {
+            Log.e(TAG, "HeadsetClient not connected");
+            return null;
+        }
+    }
+
     /**
      *  Allow audio low latency
      *
diff --git a/android/app/src/com/android/bluetooth/btservice/BondStateMachine.java b/android/app/src/com/android/bluetooth/btservice/BondStateMachine.java
index 713545f..c6633da 100644
--- a/android/app/src/com/android/bluetooth/btservice/BondStateMachine.java
+++ b/android/app/src/com/android/bluetooth/btservice/BondStateMachine.java
@@ -470,6 +470,7 @@
         if (newState == BluetoothDevice.BOND_NONE) {
             intent.putExtra(BluetoothDevice.EXTRA_UNBOND_REASON, reason);
         }
+        mAdapterService.onBondStateChanged(device, newState);
         mAdapterService.sendBroadcastAsUser(intent, UserHandle.ALL, BLUETOOTH_CONNECT,
                 Utils.getTempAllowlistBroadcastOptions());
         infoLog("Bond State Change Intent:" + device + " " + state2str(oldState) + " => "
diff --git a/android/app/src/com/android/bluetooth/btservice/CompanionManager.java b/android/app/src/com/android/bluetooth/btservice/CompanionManager.java
new file mode 100644
index 0000000..da3cd63
--- /dev/null
+++ b/android/app/src/com/android/bluetooth/btservice/CompanionManager.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 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.btservice;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.bluetooth.R;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+  A CompanionManager to specify parameters between companion devices and regular devices.
+
+  1.  A paired device is recognized as a companion device if its METADATA_SOFTWARE_VERSION is
+      set to BluetoothDevice.COMPANION_TYPE_PRIMARY or BluetoothDevice.COMPANION_TYPE_SECONDARY.
+  2.  Only can have one companion device at a time.
+  3.  Remove bond does not remove the companion device record.
+  4.  Factory reset Bluetooth removes the companion device.
+  5.  Companion device has individual GATT connection parameters.
+*/
+
+public class CompanionManager {
+    private static final String TAG = "BluetoothCompanionManager";
+
+    private BluetoothDevice mCompanionDevice;
+    private int mCompanionType;
+
+    private final int[] mGattConnHighPrimary;
+    private final int[] mGattConnBalancePrimary;
+    private final int[] mGattConnLowPrimary;
+    private final int[] mGattConnHighSecondary;
+    private final int[] mGattConnBalanceSecondary;
+    private final int[] mGattConnLowSecondary;
+    private final int[] mGattConnHighDefault;
+    private final int[] mGattConnBalanceDefault;
+    private final int[] mGattConnLowDefault;
+
+    @VisibleForTesting static final int COMPANION_TYPE_NONE      = 0;
+    @VisibleForTesting static final int COMPANION_TYPE_PRIMARY   = 1;
+    @VisibleForTesting static final int COMPANION_TYPE_SECONDARY = 2;
+
+    public static final int GATT_CONN_INTERVAL_MIN = 0;
+    public static final int GATT_CONN_INTERVAL_MAX = 1;
+    public static final int GATT_CONN_LATENCY      = 2;
+
+    @VisibleForTesting static final String COMPANION_INFO = "bluetooth_companion_info";
+    @VisibleForTesting static final String COMPANION_DEVICE_KEY = "companion_device";
+    @VisibleForTesting static final String COMPANION_TYPE_KEY = "companion_type";
+
+    private final AdapterService mAdapterService;
+    private final BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter();
+    private final Set<BluetoothDevice> mMetadataListeningDevices = new HashSet<>();
+
+    public CompanionManager(AdapterService service, ServiceFactory factory) {
+        mAdapterService = service;
+        mGattConnHighDefault = new int[] {
+                service.getResources().getInteger(R.integer.gatt_high_priority_min_interval),
+                service.getResources().getInteger(R.integer.gatt_high_priority_max_interval),
+                service.getResources().getInteger(R.integer.gatt_high_priority_latency)};
+        mGattConnBalanceDefault = new int[] {
+                service.getResources().getInteger(R.integer.gatt_balanced_priority_min_interval),
+                service.getResources().getInteger(R.integer.gatt_balanced_priority_max_interval),
+                service.getResources().getInteger(R.integer.gatt_balanced_priority_latency)};
+        mGattConnLowDefault = new int[] {
+                service.getResources().getInteger(R.integer.gatt_low_power_min_interval),
+                service.getResources().getInteger(R.integer.gatt_low_power_max_interval),
+                service.getResources().getInteger(R.integer.gatt_low_power_latency)};
+
+        mGattConnHighPrimary = new int[] {
+                service.getResources().getInteger(
+                        R.integer.gatt_high_priority_min_interval_primary),
+                service.getResources().getInteger(
+                        R.integer.gatt_high_priority_max_interval_primary),
+                service.getResources().getInteger(
+                        R.integer.gatt_high_priority_latency_primary)};
+        mGattConnBalancePrimary = new int[] {
+                service.getResources().getInteger(
+                        R.integer.gatt_balanced_priority_min_interval_primary),
+                service.getResources().getInteger(
+                        R.integer.gatt_balanced_priority_max_interval_primary),
+                service.getResources().getInteger(
+                        R.integer.gatt_balanced_priority_latency_primary)};
+        mGattConnLowPrimary = new int[] {
+                service.getResources().getInteger(R.integer.gatt_low_power_min_interval_primary),
+                service.getResources().getInteger(R.integer.gatt_low_power_max_interval_primary),
+                service.getResources().getInteger(R.integer.gatt_low_power_latency_primary)};
+
+        mGattConnHighSecondary = new int[] {
+                service.getResources().getInteger(
+                        R.integer.gatt_high_priority_min_interval_secondary),
+                service.getResources().getInteger(
+                        R.integer.gatt_high_priority_max_interval_secondary),
+                service.getResources().getInteger(R.integer.gatt_high_priority_latency_secondary)};
+        mGattConnBalanceSecondary = new int[] {
+                service.getResources().getInteger(
+                        R.integer.gatt_balanced_priority_min_interval_secondary),
+                service.getResources().getInteger(
+                        R.integer.gatt_balanced_priority_max_interval_secondary),
+                service.getResources().getInteger(
+                        R.integer.gatt_balanced_priority_latency_secondary)};
+        mGattConnLowSecondary = new int[] {
+                service.getResources().getInteger(R.integer.gatt_low_power_min_interval_secondary),
+                service.getResources().getInteger(R.integer.gatt_low_power_max_interval_secondary),
+                service.getResources().getInteger(R.integer.gatt_low_power_latency_secondary)};
+    }
+
+    void loadCompanionInfo() {
+        synchronized (mMetadataListeningDevices) {
+            String address = getCompanionPreferences().getString(COMPANION_DEVICE_KEY, "");
+
+            try {
+                mCompanionDevice = mAdapter.getRemoteDevice(address);
+                mCompanionType = getCompanionPreferences().getInt(
+                        COMPANION_TYPE_KEY, COMPANION_TYPE_NONE);
+            } catch (IllegalArgumentException e) {
+                mCompanionDevice = null;
+                mCompanionType = COMPANION_TYPE_NONE;
+            }
+        }
+
+        if (mCompanionDevice == null) {
+            // We don't have any companion phone registered, try look from the bonded devices
+            for (BluetoothDevice device : mAdapter.getBondedDevices()) {
+                byte[] metadata = mAdapterService.getMetadata(device,
+                        BluetoothDevice.METADATA_SOFTWARE_VERSION);
+                if (metadata == null) {
+                    continue;
+                }
+                String valueStr = new String(metadata);
+                if ((valueStr.equals(BluetoothDevice.COMPANION_TYPE_PRIMARY)
+                        || valueStr.equals(BluetoothDevice.COMPANION_TYPE_SECONDARY))) {
+                    // found the companion device, store and unregister all listeners
+                    Log.i(TAG, "Found companion device from the database!");
+                    setCompanionDevice(device, valueStr);
+                    break;
+                }
+                registerMetadataListener(device);
+            }
+        }
+        Log.i(TAG, "Companion device is " + mCompanionDevice + ", type=" + mCompanionType);
+    }
+
+    final BluetoothAdapter.OnMetadataChangedListener mMetadataListener =
+            new BluetoothAdapter.OnMetadataChangedListener() {
+                @Override
+                public void onMetadataChanged(BluetoothDevice device, int key, byte[] value) {
+                    String valueStr = new String(value);
+                    Log.d(TAG, String.format("Metadata updated in Device %s: %d = %s.", device,
+                            key, value == null ? null : valueStr));
+                    if (key == BluetoothDevice.METADATA_SOFTWARE_VERSION
+                            && (valueStr.equals(BluetoothDevice.COMPANION_TYPE_PRIMARY)
+                            || valueStr.equals(BluetoothDevice.COMPANION_TYPE_SECONDARY))) {
+                        setCompanionDevice(device, valueStr);
+                    }
+                }
+            };
+
+    private void setCompanionDevice(BluetoothDevice companionDevice, String type) {
+        synchronized (mMetadataListeningDevices) {
+            Log.i(TAG, "setCompanionDevice: " + companionDevice + ", type=" + type);
+            mCompanionDevice = companionDevice;
+            mCompanionType = type.equals(BluetoothDevice.COMPANION_TYPE_PRIMARY)
+                    ? COMPANION_TYPE_PRIMARY : COMPANION_TYPE_SECONDARY;
+
+            // unregister all metadata listeners
+            for (BluetoothDevice device : mMetadataListeningDevices) {
+                try {
+                    mAdapter.removeOnMetadataChangedListener(device, mMetadataListener);
+                } catch (IllegalArgumentException e) {
+                    Log.e(TAG, "failed to unregister metadata listener for "
+                            + device + " " + e);
+                }
+            }
+            mMetadataListeningDevices.clear();
+
+            SharedPreferences.Editor pref = getCompanionPreferences().edit();
+            pref.putString(COMPANION_DEVICE_KEY, mCompanionDevice.getAddress());
+            pref.putInt(COMPANION_TYPE_KEY, mCompanionType);
+            pref.apply();
+        }
+    }
+
+    private SharedPreferences getCompanionPreferences() {
+        return mAdapterService.getSharedPreferences(COMPANION_INFO, Context.MODE_PRIVATE);
+    }
+
+    /**
+     * Bond state change event from the AdapterService
+     *
+     * @param device the Bluetooth device
+     * @param state the new Bluetooth bond state of the device
+     */
+    public void onBondStateChanged(BluetoothDevice device, int state) {
+        synchronized (mMetadataListeningDevices) {
+            if (mCompanionDevice != null) {
+                // We already have the companion device, do not care bond state change any more.
+                return;
+            }
+            switch (state) {
+                case BluetoothDevice.BOND_BONDING:
+                    registerMetadataListener(device);
+                    break;
+                case BluetoothDevice.BOND_NONE:
+                    removeMetadataListener(device);
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+
+    private void registerMetadataListener(BluetoothDevice device) {
+        synchronized (mMetadataListeningDevices) {
+            Log.d(TAG, "register metadata listener: " + device);
+            try {
+                mAdapter.addOnMetadataChangedListener(
+                        device, mAdapterService.getMainExecutor(), mMetadataListener);
+            } catch (IllegalArgumentException e) {
+                Log.e(TAG, "failed to register metadata listener for "
+                        + device + " " + e);
+            }
+            mMetadataListeningDevices.add(device);
+        }
+    }
+
+    private void removeMetadataListener(BluetoothDevice device) {
+        synchronized (mMetadataListeningDevices) {
+            if (!mMetadataListeningDevices.contains(device)) return;
+
+            Log.d(TAG, "remove metadata listener: " + device);
+            try {
+                mAdapter.removeOnMetadataChangedListener(device, mMetadataListener);
+            } catch (IllegalArgumentException e) {
+                Log.e(TAG, "failed to unregister metadata listener for "
+                        + device + " " + e);
+            }
+            mMetadataListeningDevices.remove(device);
+        }
+    }
+
+
+    /**
+     * Method to get the stored companion device
+     *
+     * @return the companion Bluetooth device
+     */
+    public BluetoothDevice getCompanionDevice() {
+        return mCompanionDevice;
+    }
+
+    /**
+     * Method to check whether it is a companion device
+     *
+     * @param address the address of the device
+     * @return true if the address is a companion device, otherwise false
+     */
+    public boolean isCompanionDevice(String address) {
+        try {
+            return isCompanionDevice(mAdapter.getRemoteDevice(address));
+        } catch (IllegalArgumentException e) {
+            return false;
+        }
+    }
+
+    /**
+     * Method to check whether it is a companion device
+     *
+     * @param device the Bluetooth device
+     * @return true if the device is a companion device, otherwise false
+     */
+    public boolean isCompanionDevice(BluetoothDevice device) {
+        if (device == null) return false;
+        return device.equals(mCompanionDevice);
+    }
+
+    /**
+     * Method to reset the stored companion info
+     */
+    public void factoryReset() {
+        synchronized (mMetadataListeningDevices) {
+            mCompanionDevice = null;
+            mCompanionType = COMPANION_TYPE_NONE;
+
+            SharedPreferences.Editor pref = getCompanionPreferences().edit();
+            pref.remove(COMPANION_DEVICE_KEY);
+            pref.remove(COMPANION_TYPE_KEY);
+            pref.apply();
+        }
+    }
+
+    /**
+     * Gets the GATT connection parameters of the device
+     *
+     * @param address the address of the Bluetooth device
+     * @param type type of the parameter, can be GATT_CONN_INTERVAL_MIN, GATT_CONN_INTERVAL_MAX
+     * or GATT_CONN_LATENCY
+     * @param priority the priority of the connection, can be
+     * BluetoothGatt.CONNECTION_PRIORITY_HIGH, BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER or
+     * BluetoothGatt.CONNECTION_PRIORITY_BALANCED
+     * @return the connection parameter in integer
+     */
+    public int getGattConnParameters(String address, int type, int priority) {
+        int companionType = isCompanionDevice(address) ? mCompanionType : COMPANION_TYPE_NONE;
+        int parameter;
+        switch (companionType) {
+            case COMPANION_TYPE_PRIMARY:
+                parameter = getGattConnParameterPrimary(type, priority);
+                break;
+            case COMPANION_TYPE_SECONDARY:
+                parameter = getGattConnParameterSecondary(type, priority);
+                break;
+            default:
+                parameter = getGattConnParameterDefault(type, priority);
+                break;
+        }
+        return parameter;
+    }
+
+    private int getGattConnParameterPrimary(int type, int priority) {
+        switch (priority) {
+            case BluetoothGatt.CONNECTION_PRIORITY_HIGH:
+                return mGattConnHighPrimary[type];
+            case BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER:
+                return mGattConnLowPrimary[type];
+        }
+        return mGattConnBalancePrimary[type];
+    }
+
+    private int getGattConnParameterSecondary(int type, int priority) {
+        switch (priority) {
+            case BluetoothGatt.CONNECTION_PRIORITY_HIGH:
+                return mGattConnHighSecondary[type];
+            case BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER:
+                return mGattConnLowSecondary[type];
+        }
+        return mGattConnBalanceSecondary[type];
+    }
+
+    private int getGattConnParameterDefault(int type, int mode) {
+        switch (mode) {
+            case BluetoothGatt.CONNECTION_PRIORITY_HIGH:
+                return mGattConnHighDefault[type];
+            case BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER:
+                return mGattConnLowDefault[type];
+        }
+        return mGattConnBalanceDefault[type];
+    }
+}
diff --git a/android/app/src/com/android/bluetooth/btservice/RemoteDevices.java b/android/app/src/com/android/bluetooth/btservice/RemoteDevices.java
index 3fb1080..dd1297c 100644
--- a/android/app/src/com/android/bluetooth/btservice/RemoteDevices.java
+++ b/android/app/src/com/android/bluetooth/btservice/RemoteDevices.java
@@ -22,6 +22,7 @@
 import android.app.admin.SecurityLog;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothAssignedNumbers;
+import android.bluetooth.BluetoothAudioPolicy;
 import android.bluetooth.BluetoothClass;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
@@ -312,6 +313,7 @@
         @VisibleForTesting int mBondState;
         @VisibleForTesting int mDeviceType;
         @VisibleForTesting ParcelUuid[] mUuids;
+        private BluetoothAudioPolicy mAudioPolicy;
 
         DeviceProperties() {
             mBondState = BluetoothDevice.BOND_NONE;
@@ -499,6 +501,14 @@
                 return mIsCoordinatedSetMember;
             }
         }
+
+        public void setHfAudioPolicyForRemoteAg(BluetoothAudioPolicy policies) {
+            mAudioPolicy = policies;
+        }
+
+        public BluetoothAudioPolicy getHfAudioPolicyForRemoteAg() {
+            return mAudioPolicy;
+        }
     }
 
     private void sendUuidIntent(BluetoothDevice device, DeviceProperties prop) {
diff --git a/android/app/src/com/android/bluetooth/btservice/storage/AudioPolicyEntity.java b/android/app/src/com/android/bluetooth/btservice/storage/AudioPolicyEntity.java
new file mode 100644
index 0000000..8a302ff
--- /dev/null
+++ b/android/app/src/com/android/bluetooth/btservice/storage/AudioPolicyEntity.java
@@ -0,0 +1,61 @@
+/*
+ * 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.btservice.storage;
+
+import android.bluetooth.BluetoothAudioPolicy;
+
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+
+@Entity
+class AudioPolicyEntity {
+    @ColumnInfo(name = "call_establish_audio_policy")
+    public int callEstablishAudioPolicy;
+    @ColumnInfo(name = "connecting_time_audio_policy")
+    public int connectingTimeAudioPolicy;
+    @ColumnInfo(name = "in_band_ringtone_audio_policy")
+    public int inBandRingtoneAudioPolicy;
+
+    AudioPolicyEntity() {
+        callEstablishAudioPolicy = BluetoothAudioPolicy.POLICY_UNCONFIGURED;
+        connectingTimeAudioPolicy = BluetoothAudioPolicy.POLICY_UNCONFIGURED;
+        inBandRingtoneAudioPolicy = BluetoothAudioPolicy.POLICY_UNCONFIGURED;
+    }
+
+    AudioPolicyEntity(int callEstablishAudioPolicy, int connectingTimeAudioPolicy,
+            int inBandRingtoneAudioPolicy) {
+        this.callEstablishAudioPolicy = callEstablishAudioPolicy;
+        this.connectingTimeAudioPolicy = connectingTimeAudioPolicy;
+        this.inBandRingtoneAudioPolicy = inBandRingtoneAudioPolicy;
+    }
+
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("callEstablishAudioPolicy=")
+                .append(metadataToString(callEstablishAudioPolicy))
+                .append("|connectingTimeAudioPolicy=")
+                .append(metadataToString(connectingTimeAudioPolicy))
+                .append("|inBandRingtoneAudioPolicy=")
+                .append(metadataToString(inBandRingtoneAudioPolicy));
+
+        return builder.toString();
+    }
+
+    private String metadataToString(int metadata) {
+        return String.valueOf(metadata);
+    }
+}
diff --git a/android/app/src/com/android/bluetooth/btservice/storage/CustomizedMetadataEntity.java b/android/app/src/com/android/bluetooth/btservice/storage/CustomizedMetadataEntity.java
index b3746a2..e2f0765 100644
--- a/android/app/src/com/android/bluetooth/btservice/storage/CustomizedMetadataEntity.java
+++ b/android/app/src/com/android/bluetooth/btservice/storage/CustomizedMetadataEntity.java
@@ -47,6 +47,8 @@
     public byte[] spatial_audio;
     public byte[] fastpair_customized;
     public byte[] le_audio;
+    public byte[] gmcs_cccd;
+    public byte[] gtbs_cccd;
 
     public String toString() {
         StringBuilder builder = new StringBuilder();
@@ -103,7 +105,11 @@
                 .append("|fastpair_customized=")
                 .append(metadataToString(fastpair_customized))
                 .append("|le_audio=")
-                .append(metadataToString(le_audio));
+                .append(metadataToString(le_audio))
+                .append("|gmcs_cccd=")
+                .append(metadataToString(gmcs_cccd))
+                .append("|gtbs_cccd=")
+                .append(metadataToString(gtbs_cccd));
 
 
         return builder.toString();
diff --git a/android/app/src/com/android/bluetooth/btservice/storage/DatabaseManager.java b/android/app/src/com/android/bluetooth/btservice/storage/DatabaseManager.java
index fa85049..df8505e 100644
--- a/android/app/src/com/android/bluetooth/btservice/storage/DatabaseManager.java
+++ b/android/app/src/com/android/bluetooth/btservice/storage/DatabaseManager.java
@@ -20,6 +20,7 @@
 import android.bluetooth.BluetoothA2dp.OptionalCodecsPreferenceStatus;
 import android.bluetooth.BluetoothA2dp.OptionalCodecsSupportStatus;
 import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothAudioPolicy;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothProfile;
 import android.bluetooth.BluetoothProtoEnums;
@@ -284,6 +285,59 @@
     }
 
     /**
+     * Set audio policy metadata to database with requested key
+     */
+    @VisibleForTesting
+    public boolean setAudioPolicyMetadata(BluetoothDevice device, BluetoothAudioPolicy policies) {
+        synchronized (mMetadataCache) {
+            if (device == null) {
+                Log.e(TAG, "setAudioPolicyMetadata: device is null");
+                return false;
+            }
+
+            String address = device.getAddress();
+            if (!mMetadataCache.containsKey(address)) {
+                createMetadata(address, false);
+            }
+            Metadata data = mMetadataCache.get(address);
+            AudioPolicyEntity entity = data.audioPolicyMetadata;
+            entity.callEstablishAudioPolicy = policies.getCallEstablishPolicy();
+            entity.connectingTimeAudioPolicy = policies.getConnectingTimePolicy();
+            entity.inBandRingtoneAudioPolicy = policies.getInBandRingtonePolicy();
+
+            updateDatabase(data);
+            return true;
+        }
+    }
+
+    /**
+     * Get audio policy metadata from database with requested key
+     */
+    @VisibleForTesting
+    public BluetoothAudioPolicy getAudioPolicyMetadata(BluetoothDevice device) {
+        synchronized (mMetadataCache) {
+            if (device == null) {
+                Log.e(TAG, "getAudioPolicyMetadata: device is null");
+                return null;
+            }
+
+            String address = device.getAddress();
+
+            if (!mMetadataCache.containsKey(address)) {
+                Log.d(TAG, "getAudioPolicyMetadata: device " + address + " is not in cache");
+                return null;
+            }
+
+            AudioPolicyEntity entity = mMetadataCache.get(address).audioPolicyMetadata;
+            return new BluetoothAudioPolicy.Builder()
+                    .setCallEstablishPolicy(entity.callEstablishAudioPolicy)
+                    .setConnectingTimePolicy(entity.connectingTimeAudioPolicy)
+                    .setInBandRingtonePolicy(entity.inBandRingtoneAudioPolicy)
+                    .build();
+        }
+    }
+
+    /**
      * Set the device profile connection policy
      *
      * @param device {@link BluetoothDevice} wish to set
diff --git a/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java b/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java
index f64f35e..756b6d7 100644
--- a/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java
+++ b/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java
@@ -58,6 +58,25 @@
     public long last_active_time;
     public boolean is_active_a2dp_device;
 
+    @Embedded
+    public AudioPolicyEntity audioPolicyMetadata;
+
+    /**
+     * The preferred profile to be used for {@link BluetoothDevice#AUDIO_MODE_OUTPUT_ONLY}. This can
+     * be either {@link BluetoothProfile#A2DP} or {@link BluetoothProfile#LE_AUDIO}. This value is
+     * only used if the remote device supports both A2DP and LE Audio and both transports are
+     * connected and active.
+     */
+    public int preferred_output_only_profile;
+
+    /**
+     * The preferred profile to be used for {@link BluetoothDevice#AUDIO_MODE_DUPLEX}. This can
+     * be either {@link BluetoothProfile#HEADSET} or {@link BluetoothProfile#LE_AUDIO}. This value
+     * is only used if the remote device supports both HFP and LE Audio and both transports are
+     * connected and active.
+     */
+    public int preferred_duplex_profile;
+
     Metadata(String address) {
         this.address = address;
         migrated = false;
@@ -67,6 +86,9 @@
         a2dpOptionalCodecsEnabled = BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN;
         last_active_time = MetadataDatabase.sCurrentConnectionNumber++;
         is_active_a2dp_device = true;
+        audioPolicyMetadata = new AudioPolicyEntity();
+        preferred_output_only_profile = 0;
+        preferred_duplex_profile = 0;
     }
 
     /**
@@ -290,6 +312,12 @@
             case BluetoothDevice.METADATA_LE_AUDIO:
                 publicMetadata.le_audio = value;
                 break;
+            case BluetoothDevice.METADATA_GMCS_CCCD:
+                publicMetadata.gmcs_cccd = value;
+                break;
+            case BluetoothDevice.METADATA_GTBS_CCCD:
+                publicMetadata.gtbs_cccd = value;
+                break;
         }
     }
 
@@ -381,6 +409,12 @@
             case BluetoothDevice.METADATA_LE_AUDIO:
                 value = publicMetadata.le_audio;
                 break;
+            case BluetoothDevice.METADATA_GMCS_CCCD:
+                value = publicMetadata.gmcs_cccd;
+                break;
+            case BluetoothDevice.METADATA_GTBS_CCCD:
+                value = publicMetadata.gtbs_cccd;
+                break;
         }
         return value;
     }
@@ -407,6 +441,8 @@
             .append(a2dpOptionalCodecsEnabled)
             .append("), custom metadata(")
             .append(publicMetadata)
+            .append("), hfp client audio policy(")
+            .append(audioPolicyMetadata)
             .append(")}");
 
         return builder.toString();
diff --git a/android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java b/android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java
index a71a548..2776e65 100644
--- a/android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java
+++ b/android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java
@@ -33,7 +33,7 @@
 /**
  * MetadataDatabase is a Room database stores Bluetooth persistence data
  */
-@Database(entities = {Metadata.class}, version = 114)
+@Database(entities = {Metadata.class}, version = 117)
 public abstract class MetadataDatabase extends RoomDatabase {
     /**
      * The metadata database file name
@@ -67,6 +67,9 @@
                 .addMigrations(MIGRATION_111_112)
                 .addMigrations(MIGRATION_112_113)
                 .addMigrations(MIGRATION_113_114)
+                .addMigrations(MIGRATION_114_115)
+                .addMigrations(MIGRATION_115_116)
+                .addMigrations(MIGRATION_116_117)
                 .allowMainThreadQueries()
                 .build();
     }
@@ -500,4 +503,67 @@
             }
         }
     };
+
+    @VisibleForTesting
+    static final Migration MIGRATION_114_115 = new Migration(114, 115) {
+        @Override
+        public void migrate(SupportSQLiteDatabase database) {
+            try {
+                database.execSQL(
+                        "ALTER TABLE metadata ADD COLUMN `call_establish_audio_policy` "
+                        + "INTEGER DEFAULT 0");
+                database.execSQL(
+                        "ALTER TABLE metadata ADD COLUMN `connecting_time_audio_policy` "
+                        + "INTEGER DEFAULT 0");
+                database.execSQL(
+                        "ALTER TABLE metadata ADD COLUMN `in_band_ringtone_audio_policy` "
+                        + "INTEGER DEFAULT 0");
+            } catch (SQLException ex) {
+                // Check if user has new schema, but is just missing the version update
+                Cursor cursor = database.query("SELECT * FROM metadata");
+                if (cursor == null
+                        || cursor.getColumnIndex("call_establish_audio_policy") == -1) {
+                    throw ex;
+                }
+            }
+        }
+    };
+
+    @VisibleForTesting
+    static final Migration MIGRATION_115_116 = new Migration(115, 116) {
+        @Override
+        public void migrate(SupportSQLiteDatabase database) {
+            try {
+                database.execSQL("ALTER TABLE metadata ADD COLUMN `preferred_output_only_profile` "
+                        + "INTEGER NOT NULL DEFAULT 0");
+                database.execSQL("ALTER TABLE metadata ADD COLUMN `preferred_duplex_profile` "
+                        + "INTEGER NOT NULL DEFAULT 0");
+            } catch (SQLException ex) {
+                // Check if user has new schema, but is just missing the version update
+                Cursor cursor = database.query("SELECT * FROM metadata");
+                if (cursor == null
+                        || cursor.getColumnIndex("preferred_output_only_profile") == -1
+                        || cursor.getColumnIndex("preferred_duplex_profile") == -1) {
+                    throw ex;
+                }
+            }
+        }
+    };
+
+    @VisibleForTesting
+    static final Migration MIGRATION_116_117 = new Migration(116, 117) {
+        @Override
+        public void migrate(SupportSQLiteDatabase database) {
+            try {
+                database.execSQL("ALTER TABLE metadata ADD COLUMN `gmcs_cccd` BLOB");
+                database.execSQL("ALTER TABLE metadata ADD COLUMN `gtbs_cccd` BLOB");
+            } catch (SQLException ex) {
+                // Check if user has new schema, but is just missing the version update
+                Cursor cursor = database.query("SELECT * FROM metadata");
+                if (cursor == null || cursor.getColumnIndex("gmcs_cccd") == -1) {
+                    throw ex;
+                }
+            }
+        }
+    };
 }
diff --git a/android/app/src/com/android/bluetooth/gatt/GattService.java b/android/app/src/com/android/bluetooth/gatt/GattService.java
index 21f596d..1b8c61d 100644
--- a/android/app/src/com/android/bluetooth/gatt/GattService.java
+++ b/android/app/src/com/android/bluetooth/gatt/GattService.java
@@ -79,6 +79,7 @@
 import com.android.bluetooth.btservice.AbstractionLayer;
 import com.android.bluetooth.btservice.AdapterService;
 import com.android.bluetooth.btservice.BluetoothAdapterProxy;
+import com.android.bluetooth.btservice.CompanionManager;
 import com.android.bluetooth.btservice.ProfileService;
 import com.android.bluetooth.util.NumberUtils;
 import com.android.internal.annotations.VisibleForTesting;
@@ -3865,33 +3866,21 @@
         // Link supervision timeout is measured in N * 10ms
         int timeout = 500; // 5s
 
-        switch (connectionPriority) {
-            case BluetoothGatt.CONNECTION_PRIORITY_HIGH:
-                minInterval = getResources().getInteger(R.integer.gatt_high_priority_min_interval);
-                maxInterval = getResources().getInteger(R.integer.gatt_high_priority_max_interval);
-                latency = getResources().getInteger(R.integer.gatt_high_priority_latency);
-                break;
 
-            case BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER:
-                minInterval = getResources().getInteger(R.integer.gatt_low_power_min_interval);
-                maxInterval = getResources().getInteger(R.integer.gatt_low_power_max_interval);
-                latency = getResources().getInteger(R.integer.gatt_low_power_latency);
-                break;
+        CompanionManager manager =
+                AdapterService.getAdapterService().getCompanionManager();
 
-            default:
-                // Using the values for CONNECTION_PRIORITY_BALANCED.
-                minInterval =
-                        getResources().getInteger(R.integer.gatt_balanced_priority_min_interval);
-                maxInterval =
-                        getResources().getInteger(R.integer.gatt_balanced_priority_max_interval);
-                latency = getResources().getInteger(R.integer.gatt_balanced_priority_latency);
-                break;
-        }
+        minInterval = manager.getGattConnParameters(
+                address, CompanionManager.GATT_CONN_INTERVAL_MIN, connectionPriority);
+        maxInterval = manager.getGattConnParameters(
+                address, CompanionManager.GATT_CONN_INTERVAL_MAX, connectionPriority);
+        latency = manager.getGattConnParameters(
+                address, CompanionManager.GATT_CONN_LATENCY, connectionPriority);
 
-        if (DBG) {
-            Log.d(TAG, "connectionParameterUpdate() - address=" + address + "params="
-                    + connectionPriority + " interval=" + minInterval + "/" + maxInterval);
-        }
+        Log.d(TAG, "connectionParameterUpdate() - address=" + address + " params="
+                + connectionPriority + " interval=" + minInterval + "/" + maxInterval
+                + " timeout=" + timeout);
+
         gattConnectionParameterUpdateNative(clientIf, address, minInterval, maxInterval, latency,
                 timeout, 0, 0);
     }
@@ -3906,14 +3895,11 @@
             return;
         }
 
-        if (DBG) {
-            Log.d(TAG, "leConnectionUpdate() - address=" + address + ", intervals="
-                        + minInterval + "/" + maxInterval + ", latency=" + peripheralLatency
-                        + ", timeout=" + supervisionTimeout + "msec" + ", min_ce="
-                        + minConnectionEventLen + ", max_ce=" + maxConnectionEventLen);
+        Log.d(TAG, "leConnectionUpdate() - address=" + address + ", intervals="
+                    + minInterval + "/" + maxInterval + ", latency=" + peripheralLatency
+                    + ", timeout=" + supervisionTimeout + "msec" + ", min_ce="
+                    + minConnectionEventLen + ", max_ce=" + maxConnectionEventLen);
 
-
-        }
         gattConnectionParameterUpdateNative(clientIf, address, minInterval, maxInterval,
                                             peripheralLatency, supervisionTimeout,
                                             minConnectionEventLen, maxConnectionEventLen);
diff --git a/android/app/src/com/android/bluetooth/hfp/HeadsetService.java b/android/app/src/com/android/bluetooth/hfp/HeadsetService.java
index 19bffa0..536c054 100644
--- a/android/app/src/com/android/bluetooth/hfp/HeadsetService.java
+++ b/android/app/src/com/android/bluetooth/hfp/HeadsetService.java
@@ -23,6 +23,7 @@
 
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
+import android.bluetooth.BluetoothAudioPolicy;
 import android.bluetooth.BluetoothClass;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
@@ -1364,6 +1365,24 @@
     }
 
     /**
+     * Get the Bluetooth Audio Policy stored in the state machine
+     *
+     * @param device the device to change silence mode
+     * @return a {@link BluetoothAudioPolicy} object
+     */
+    public BluetoothAudioPolicy getHfpCallAudioPolicy(BluetoothDevice device) {
+        synchronized (mStateMachines) {
+            final HeadsetStateMachine stateMachine = mStateMachines.get(device);
+            if (stateMachine == null) {
+                Log.e(TAG, "getHfpCallAudioPolicy(), " + device
+                        + " does not have a state machine");
+                return null;
+            }
+            return stateMachine.getHfpCallAudioPolicy();
+        }
+    }
+
+    /**
      * Remove the active device
      */
     private void removeActiveDevice() {
@@ -1891,7 +1910,24 @@
                 mSystemInterface.getAudioManager().setA2dpSuspended(false);
             }
         });
-
+        if (callState == HeadsetHalConstants.CALL_STATE_IDLE) {
+            final HeadsetStateMachine stateMachine = mStateMachines.get(mActiveDevice);
+            if (stateMachine == null) {
+                Log.d(TAG, "phoneStateChanged: CALL_STATE_IDLE, mActiveDevice is Null");
+            } else {
+                BluetoothAudioPolicy currentPolicy = stateMachine.getHfpCallAudioPolicy();
+                if (currentPolicy != null && currentPolicy.getConnectingTimePolicy()
+                        == BluetoothAudioPolicy.POLICY_NOT_ALLOWED) {
+                    /**
+                     * If the active device was set because of the pick up audio policy
+                     * and the connecting policy is NOT_ALLOWED, then after the call is
+                     * terminated, we must de-activate this device.
+                     * If there is a fallback mechanism, we should follow it.
+                     */
+                    removeActiveDevice();
+                }
+            }
+        }
     }
 
     @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
@@ -1940,8 +1976,23 @@
     public boolean isInbandRingingEnabled() {
         boolean isInbandRingingSupported = getResources().getBoolean(
                 com.android.bluetooth.R.bool.config_bluetooth_hfp_inband_ringing_support);
+
+        boolean inbandRingtoneAllowedByPolicy = true;
+        List<BluetoothDevice> audioConnectableDevices = getConnectedDevices();
+        if (audioConnectableDevices.size() == 1) {
+            BluetoothDevice connectedDevice = audioConnectableDevices.get(0);
+            BluetoothAudioPolicy callAudioPolicy =
+                    getHfpCallAudioPolicy(connectedDevice);
+            if (callAudioPolicy != null && callAudioPolicy.getInBandRingtonePolicy()
+                    == BluetoothAudioPolicy.POLICY_NOT_ALLOWED) {
+                inbandRingtoneAllowedByPolicy = false;
+            }
+        }
+
         return isInbandRingingSupported && !SystemProperties.getBoolean(
-                DISABLE_INBAND_RINGING_PROPERTY, false) && !mInbandRingingRuntimeDisable;
+                DISABLE_INBAND_RINGING_PROPERTY, false)
+                && !mInbandRingingRuntimeDisable
+                && inbandRingtoneAllowedByPolicy;
     }
 
     /**
diff --git a/android/app/src/com/android/bluetooth/hfp/HeadsetStateMachine.java b/android/app/src/com/android/bluetooth/hfp/HeadsetStateMachine.java
index b237e7d..cec0a39 100644
--- a/android/app/src/com/android/bluetooth/hfp/HeadsetStateMachine.java
+++ b/android/app/src/com/android/bluetooth/hfp/HeadsetStateMachine.java
@@ -21,6 +21,7 @@
 import android.annotation.RequiresPermission;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothAssignedNumbers;
+import android.bluetooth.BluetoothAudioPolicy;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
 import android.bluetooth.BluetoothProfile;
@@ -43,6 +44,7 @@
 import com.android.bluetooth.Utils;
 import com.android.bluetooth.btservice.AdapterService;
 import com.android.bluetooth.btservice.ProfileService;
+import com.android.bluetooth.btservice.storage.DatabaseManager;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
@@ -134,6 +136,7 @@
     private final AdapterService mAdapterService;
     private final HeadsetNativeInterface mNativeInterface;
     private final HeadsetSystemInterface mSystemInterface;
+    private DatabaseManager mDatabaseManager;
 
     // Runtime states
     @VisibleForTesting
@@ -155,6 +158,10 @@
     // Audio disconnect timeout retry count
     private int mAudioDisconnectRetry = 0;
 
+    static final int HFP_SET_AUDIO_POLICY = 1;
+
+    private BluetoothAudioPolicy mHsClientAudioPolicy;
+
     // Keys are AT commands, and values are the company IDs.
     private static final Map<String, Integer> VENDOR_SPECIFIC_AT_COMMAND_COMPANY_ID;
 
@@ -187,7 +194,21 @@
         mSystemInterface =
                 Objects.requireNonNull(systemInterface, "systemInterface cannot be null");
         mAdapterService = Objects.requireNonNull(adapterService, "AdapterService cannot be null");
+        mDatabaseManager = Objects.requireNonNull(
+            AdapterService.getAdapterService().getDatabase(),
+            "DatabaseManager cannot be null when HeadsetClientStateMachine is created");
         mDeviceSilenced = false;
+
+        BluetoothAudioPolicy storedAudioPolicy = mDatabaseManager.getAudioPolicyMetadata(device);
+        if (storedAudioPolicy == null) {
+            Log.w(TAG, "Audio Policy not created in database! Creating...");
+            mHsClientAudioPolicy = new BluetoothAudioPolicy.Builder().build();
+            mDatabaseManager.setAudioPolicyMetadata(device, mHsClientAudioPolicy);
+        } else {
+            Log.i(TAG, "Audio Policy found in database!");
+            mHsClientAudioPolicy = storedAudioPolicy;
+        }
+
         // Create phonebook helper
         mPhonebook = new AtPhonebook(mHeadsetService, mNativeInterface);
         // Initialize state machine
@@ -241,6 +262,8 @@
         ProfileService.println(sb, "  mMicVolume: " + mMicVolume);
         ProfileService.println(sb,
                 "  mConnectingTimestampMs(uptimeMillis): " + mConnectingTimestampMs);
+        ProfileService.println(sb, "  mHsClientAudioPolicy: " + mHsClientAudioPolicy.toString());
+
         ProfileService.println(sb, "  StateMachine: " + this);
         // Dump the state machine logs
         StringWriter stringWriter = new StringWriter();
@@ -1909,6 +1932,119 @@
     }
 
     /**
+     * Process Android specific AT commands.
+     *
+     * @param atString AT command after the "AT+" prefix. Starts with "ANDROID"
+     * @param device Remote device that has sent this command
+     */
+    private void processAndroidAt(String atString, BluetoothDevice device) {
+        log("processAndroidSpecificAt - atString = " + atString);
+
+        if (atString.equals("+ANDROID=?")) {
+            // feature request type command
+            processAndroidAtFeatureRequest(device);
+        } else if (atString.startsWith("+ANDROID=")) {
+            // set type command
+            int equalIndex = atString.indexOf("=");
+            String arg = atString.substring(equalIndex + 1);
+
+            if (arg.isEmpty()) {
+                Log.e(TAG, "Command Invalid!");
+                mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
+                return;
+            }
+
+            Object[] args = generateArgs(arg);
+
+            if (!(args[0] instanceof Integer)) {
+                Log.e(TAG, "Type ID is invalid");
+                mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
+                return;
+            }
+
+            int type = (Integer) args[0];
+
+            if (type == HFP_SET_AUDIO_POLICY) {
+                processAndroidAtSetAudioPolicy(args, device);
+            } else {
+                Log.w(TAG, "Undefined AT+ANDROID command");
+                mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
+                return;
+            }
+        } else {
+            Log.e(TAG, "Undefined AT+ANDROID command");
+            mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
+            return;
+        }
+        mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_OK, 0);
+    }
+
+    private void processAndroidAtFeatureRequest(BluetoothDevice device) {
+        /*
+            replying with +ANDROID=1
+            here, 1 is the feature id for audio policy
+
+            currently we only support one type of feature
+        */
+        mNativeInterface.atResponseString(device,
+                BluetoothHeadset.VENDOR_RESULT_CODE_COMMAND_ANDROID
+                + ": " + HFP_SET_AUDIO_POLICY);
+    }
+
+    /**
+     * Process AT+ANDROID AT command
+     *
+     * @param args command arguments after the equal sign
+     * @param device Remote device that has sent this command
+     */
+    private void processAndroidAtSetAudioPolicy(Object[] args, BluetoothDevice device) {
+        if (args.length != 4) {
+            Log.e(TAG, "processAndroidAtSetAudioPolicy() args length must be 4: "
+                    + String.valueOf(args.length));
+            return;
+        }
+        if (!(args[1] instanceof Integer) || !(args[2] instanceof Integer)
+                || !(args[3] instanceof Integer)) {
+            Log.e(TAG, "processAndroidAtSetAudioPolicy() argument types not matched");
+            return;
+        }
+
+        if (!mDevice.equals(device)) {
+            Log.e(TAG, "processAndroidAtSetAudioPolicy(): argument device " + device
+                    + " doesn't match mDevice " + mDevice);
+            return;
+        }
+
+        int callEstablishPolicy = (Integer) args[1];
+        int connectingTimePolicy = (Integer) args[2];
+        int inbandPolicy = (Integer) args[3];
+
+        setHfpCallAudioPolicy(new BluetoothAudioPolicy.Builder()
+                .setCallEstablishPolicy(callEstablishPolicy)
+                .setConnectingTimePolicy(connectingTimePolicy)
+                .setInBandRingtonePolicy(inbandPolicy)
+                .build());
+    }
+
+    /**
+     * sets the audio policy of the client device and stores in the database
+     *
+     * @param policies policies to be set and stored
+     */
+    public void setHfpCallAudioPolicy(BluetoothAudioPolicy policies) {
+        mHsClientAudioPolicy = policies;
+        mDatabaseManager.setAudioPolicyMetadata(mDevice, policies);
+    }
+
+    /**
+     * get the audio policy of the client device
+     *
+     */
+    public BluetoothAudioPolicy getHfpCallAudioPolicy() {
+        return mHsClientAudioPolicy;
+    }
+
+    /**
      * Process AT+XAPL AT command
      *
      * @param args command arguments after the equal sign
@@ -1959,6 +2095,8 @@
             processAtCpbs(atCommand.substring(5), commandType, device);
         } else if (atCommand.startsWith("+CPBR")) {
             processAtCpbr(atCommand.substring(5), commandType, device);
+        } else if (atCommand.startsWith("+ANDROID")) {
+            processAndroidAt(atCommand, device);
         } else {
             processVendorSpecificAt(atCommand, device);
         }
diff --git a/android/app/src/com/android/bluetooth/hfp/HeadsetSystemInterface.java b/android/app/src/com/android/bluetooth/hfp/HeadsetSystemInterface.java
index 08f4bdb..0820544 100644
--- a/android/app/src/com/android/bluetooth/hfp/HeadsetSystemInterface.java
+++ b/android/app/src/com/android/bluetooth/hfp/HeadsetSystemInterface.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
+import android.bluetooth.BluetoothAudioPolicy;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
 import android.content.ActivityNotFoundException;
@@ -155,13 +156,19 @@
     @VisibleForTesting
     @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
     public void answerCall(BluetoothDevice device) {
+        Log.d(TAG, "answerCall");
         if (device == null) {
             Log.w(TAG, "answerCall device is null");
             return;
         }
         BluetoothInCallService bluetoothInCallService = getBluetoothInCallServiceInstance();
         if (bluetoothInCallService != null) {
-            mHeadsetService.setActiveDevice(device);
+            BluetoothAudioPolicy callAudioPolicy =
+                    mHeadsetService.getHfpCallAudioPolicy(device);
+            if (callAudioPolicy == null || callAudioPolicy.getCallEstablishPolicy()
+                    != BluetoothAudioPolicy.POLICY_NOT_ALLOWED) {
+                mHeadsetService.setActiveDevice(device);
+            }
             bluetoothInCallService.answerCall();
         } else {
             Log.e(TAG, "Handsfree phone proxy null for answering call");
diff --git a/android/app/src/com/android/bluetooth/hfpclient/HeadsetClientService.java b/android/app/src/com/android/bluetooth/hfpclient/HeadsetClientService.java
index db43df5..bce1c79 100644
--- a/android/app/src/com/android/bluetooth/hfpclient/HeadsetClientService.java
+++ b/android/app/src/com/android/bluetooth/hfpclient/HeadsetClientService.java
@@ -17,6 +17,7 @@
 package com.android.bluetooth.hfpclient;
 
 import android.annotation.RequiresPermission;
+import android.bluetooth.BluetoothAudioPolicy;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadsetClient;
 import android.bluetooth.BluetoothHeadsetClientCall;
@@ -59,7 +60,7 @@
  * @hide
  */
 public class HeadsetClientService extends ProfileService {
-    private static final boolean DBG = false;
+    private static final boolean DBG = true;
     private static final String TAG = "HeadsetClientService";
 
     // This is also used as a lock for shared data in {@link HeadsetClientService}
@@ -612,8 +613,10 @@
                 List<BluetoothHeadsetClientCall> defaultValue = new ArrayList<>();
                 if (service != null) {
                     List<HfpClientCall> calls = service.getCurrentCalls(device);
-                    for (HfpClientCall call : calls) {
-                        defaultValue.add(toLegacyCall(call));
+                    if (calls != null) {
+                        for (HfpClientCall call : calls) {
+                            defaultValue.add(toLegacyCall(call));
+                        }
                     }
                 }
                 receiver.send(defaultValue);
@@ -923,6 +926,53 @@
         return false;
     }
 
+    /**
+     * sends the {@link BluetoothAudioPolicy} object to the state machine of the corresponding
+     * device to store and send to the remote device using Android specific AT commands.
+     *
+     * @param device for whom the policies to be set
+     * @param policies to be set policies
+     */
+    public void setAudioPolicy(BluetoothDevice device, BluetoothAudioPolicy policies) {
+        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
+        Log.i(TAG, "setAudioPolicy: device=" + device + ", " + policies.toString() + ", "
+                + Utils.getUidPidString());
+        HeadsetClientStateMachine sm = getStateMachine(device);
+        if (sm != null) {
+            sm.setAudioPolicy(policies);
+        }
+    }
+
+    /**
+     * sets the audio policy feature support status for the corresponding device.
+     *
+     * @param device for whom the policies to be set
+     * @param supported support status
+     */
+    public void setAudioPolicyRemoteSupported(BluetoothDevice device, boolean supported) {
+        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
+        Log.i(TAG, "setAudioPolicyRemoteSupported: " + supported);
+        HeadsetClientStateMachine sm = getStateMachine(device);
+        if (sm != null) {
+            sm.setAudioPolicyRemoteSupported(supported);
+        }
+    }
+
+    /**
+     * gets the audio policy feature support status for the corresponding device.
+     *
+     * @param device for whom the policies to be set
+     * @return int support status
+     */
+    public int getAudioPolicyRemoteSupported(BluetoothDevice device) {
+        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
+        HeadsetClientStateMachine sm = getStateMachine(device);
+        if (sm != null) {
+            return sm.getAudioPolicyRemoteSupported();
+        }
+        return BluetoothAudioPolicy.FEATURE_UNCONFIGURED_BY_REMOTE;
+    }
+
     public boolean connectAudio(BluetoothDevice device) {
         Log.i(TAG, "connectAudio: device=" + device + ", " + Utils.getUidPidString());
         HeadsetClientStateMachine sm = getStateMachine(device);
diff --git a/android/app/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java b/android/app/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java
index d7d86fa..581b258 100644
--- a/android/app/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java
+++ b/android/app/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java
@@ -37,6 +37,7 @@
 import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
 
 import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothAudioPolicy;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadsetClient;
 import android.bluetooth.BluetoothHeadsetClient.NetworkServiceState;
@@ -109,6 +110,7 @@
     public static final int DISABLE_NREC = 20;
     public static final int SEND_VENDOR_AT_COMMAND = 21;
     public static final int SEND_BIEV = 22;
+    public static final int SEND_ANDROID_AT_COMMAND = 23;
 
     // internal actions
     @VisibleForTesting
@@ -180,6 +182,12 @@
     int mAudioState;
     // Indicates whether audio can be routed to the device
     private boolean mAudioRouteAllowed;
+
+    private static final int CALL_AUDIO_POLICY_FEATURE_ID = 1;
+
+    public int mAudioPolicyRemoteSupported;
+    private BluetoothAudioPolicy mHsClientAudioPolicy;
+
     private boolean mAudioWbs;
     private int mVoiceRecognitionActive;
     private final BluetoothAdapter mAdapter;
@@ -227,6 +235,8 @@
         ProfileService.println(sb, "  mOperatorName: " + mOperatorName);
         ProfileService.println(sb, "  mSubscriberInfo: " + mSubscriberInfo);
         ProfileService.println(sb, "  mAudioRouteAllowed: " + mAudioRouteAllowed);
+        ProfileService.println(sb, "  mAudioPolicyRemoteSupported: " + mAudioPolicyRemoteSupported);
+        ProfileService.println(sb, "  mHsClientAudioPolicy: " + mHsClientAudioPolicy);
 
         ProfileService.println(sb, "  mCalls:");
         if (mCalls != null) {
@@ -867,6 +877,8 @@
         mAudioRouteAllowed = context.getResources().getBoolean(
             R.bool.headset_client_initial_audio_route_allowed);
 
+        mHsClientAudioPolicy = new BluetoothAudioPolicy.Builder().build();
+
         mIndicatorNetworkState = HeadsetClientHalConstants.NETWORK_STATE_NOT_AVAILABLE;
         mIndicatorNetworkType = HeadsetClientHalConstants.SERVICE_TYPE_HOME;
         mIndicatorNetworkSignal = 0;
@@ -1149,6 +1161,40 @@
                             deferMessage(message);
                             break;
                         case StackEvent.EVENT_TYPE_CMD_RESULT:
+                            logD("Connecting: CMD_RESULT valueInt:" + event.valueInt
+                                    + " mQueuedActions.size=" + mQueuedActions.size());
+                            if (!mQueuedActions.isEmpty()) {
+                                logD("queuedAction:" + mQueuedActions.peek().first);
+                            }
+                            Pair<Integer, Object> queuedAction = mQueuedActions.poll();
+                            if (queuedAction == null || queuedAction.first == NO_ACTION) {
+                                break;
+                            }
+                            switch (queuedAction.first) {
+                                case SEND_ANDROID_AT_COMMAND:
+                                    if (event.valueInt == StackEvent.CMD_RESULT_TYPE_OK) {
+                                        Log.w(TAG, "Received OK instead of +ANDROID");
+                                    } else {
+                                        Log.w(TAG, "Received ERROR instead of +ANDROID");
+                                    }
+                                    setAudioPolicyRemoteSupported(false);
+                                    transitionTo(mConnected);
+                                    break;
+                                default:
+                                    Log.w(TAG, "Ignored CMD Result");
+                                    break;
+                            }
+                            break;
+
+                        case StackEvent.EVENT_TYPE_UNKNOWN_EVENT:
+                            if (mVendorProcessor.processEvent(event.valueString, event.device)) {
+                                mQueuedActions.poll();
+                                transitionTo(mConnected);
+                            } else {
+                                Log.e(TAG, "Unknown event :" + event.valueString
+                                        + " for device " + event.device);
+                            }
+                            break;
                         case StackEvent.EVENT_TYPE_SUBSCRIBER_INFO:
                         case StackEvent.EVENT_TYPE_CURRENT_CALLS:
                         case StackEvent.EVENT_TYPE_OPERATOR_NAME:
@@ -1212,7 +1258,11 @@
                             mAudioManager.isMicrophoneMute() ? 0 : 15, 0));
                     // query subscriber info
                     deferMessage(obtainMessage(HeadsetClientStateMachine.SUBSCRIBER_INFO));
-                    transitionTo(mConnected);
+
+                    if (!queryRemoteSupportedFeatures()) {
+                        Log.w(TAG, "Couldn't query Android AT remote supported!");
+                        transitionTo(mConnected);
+                    }
                     break;
 
                 case HeadsetClientHalConstants.CONNECTION_STATE_CONNECTED:
@@ -1597,6 +1647,8 @@
                                                 oldState, mVoiceRecognitionActive);
                                     }
                                     break;
+                                case SEND_ANDROID_AT_COMMAND:
+                                    logD("Connected: Received OK for AT+ANDROID");
                                 default:
                                     Log.w(TAG, "Unhandled AT OK " + event);
                                     break;
@@ -2098,9 +2150,81 @@
 
     public void setAudioRouteAllowed(boolean allowed) {
         mAudioRouteAllowed = allowed;
+
+        int establishPolicy = allowed
+                ? BluetoothAudioPolicy.POLICY_ALLOWED :
+                BluetoothAudioPolicy.POLICY_NOT_ALLOWED;
+
+        /*
+         * Backward compatibility for mAudioRouteAllowed
+         */
+        setAudioPolicy(new BluetoothAudioPolicy.Builder(mHsClientAudioPolicy)
+                .setCallEstablishPolicy(establishPolicy).build());
     }
 
     public boolean getAudioRouteAllowed() {
         return mAudioRouteAllowed;
     }
+
+    private String createMaskString(BluetoothAudioPolicy policies) {
+        StringBuilder mask = new StringBuilder();
+        mask.append(Integer.toString(CALL_AUDIO_POLICY_FEATURE_ID));
+        mask.append("," + policies.getCallEstablishPolicy());
+        mask.append("," + policies.getConnectingTimePolicy());
+        mask.append("," + policies.getInBandRingtonePolicy());
+        return mask.toString();
+    }
+
+    /**
+     * sets the {@link BluetoothAudioPolicy} object device and send to the remote
+     * device using Android specific AT commands.
+     *
+     * @param policies to be set policies
+     */
+    public void setAudioPolicy(BluetoothAudioPolicy policies) {
+        logD("setAudioPolicy: " + policies);
+        mHsClientAudioPolicy = policies;
+
+        if (mAudioPolicyRemoteSupported != BluetoothAudioPolicy.FEATURE_SUPPORTED_BY_REMOTE) {
+            Log.e(TAG, "Audio Policy feature not supported!");
+            return;
+        }
+
+        if (!mNativeInterface.sendAndroidAt(mCurrentDevice,
+                "+ANDROID=" + createMaskString(policies))) {
+            Log.e(TAG, "ERROR: Couldn't send call audio policies");
+        }
+    }
+
+    private boolean queryRemoteSupportedFeatures() {
+        Log.i(TAG, "queryRemoteSupportedFeatures");
+        if (!mNativeInterface.sendAndroidAt(mCurrentDevice, "+ANDROID=?")) {
+            Log.e(TAG, "ERROR: Couldn't send audio policy feature query");
+            return false;
+        }
+        addQueuedAction(SEND_ANDROID_AT_COMMAND);
+        return true;
+    }
+
+    /**
+     * sets the audio policy feature support status
+     *
+     * @param supported support status
+     */
+    public void setAudioPolicyRemoteSupported(boolean supported) {
+        if (supported) {
+            mAudioPolicyRemoteSupported = BluetoothAudioPolicy.FEATURE_SUPPORTED_BY_REMOTE;
+        } else {
+            mAudioPolicyRemoteSupported = BluetoothAudioPolicy.FEATURE_NOT_SUPPORTED_BY_REMOTE;
+        }
+    }
+
+    /**
+     * gets the audio policy feature support status
+     *
+     * @return int support status
+     */
+    public int getAudioPolicyRemoteSupported() {
+        return mAudioPolicyRemoteSupported;
+    }
 }
diff --git a/android/app/src/com/android/bluetooth/hfpclient/NativeInterface.java b/android/app/src/com/android/bluetooth/hfpclient/NativeInterface.java
index 1cb2cf7..2615b73 100644
--- a/android/app/src/com/android/bluetooth/hfpclient/NativeInterface.java
+++ b/android/app/src/com/android/bluetooth/hfpclient/NativeInterface.java
@@ -266,6 +266,17 @@
         return sendATCmdNative(getByteAddress(device), atCmd, val1, val2, arg);
     }
 
+    /**
+     * Set call audio policy to the specified paired device
+     *
+     * @param cmd Android specific command string
+     * @return True on success, False on failure
+     */
+    @VisibleForTesting
+    public boolean sendAndroidAt(BluetoothDevice device, String cmd) {
+        return sendAndroidAtNative(getByteAddress(device), cmd);
+    }
+
     // Native methods that call into the JNI interface
     private static native void classInitNative();
 
@@ -306,6 +317,8 @@
     private static native boolean sendATCmdNative(byte[] address, int atCmd, int val1, int val2,
             String arg);
 
+    private static native boolean sendAndroidAtNative(byte[] address, String cmd);
+
     private BluetoothDevice getDevice(byte[] address) {
         return mAdapterService.getDeviceFromByte(address);
     }
diff --git a/android/app/src/com/android/bluetooth/hfpclient/StackEvent.java b/android/app/src/com/android/bluetooth/hfpclient/StackEvent.java
index a0b8ce6..4c08946 100644
--- a/android/app/src/com/android/bluetooth/hfpclient/StackEvent.java
+++ b/android/app/src/com/android/bluetooth/hfpclient/StackEvent.java
@@ -51,6 +51,9 @@
     public static final int EVENT_TYPE_RING_INDICATION = 21;
     public static final int EVENT_TYPE_UNKNOWN_EVENT = 22;
 
+    public static final int CMD_RESULT_TYPE_OK = 0;
+    public static final int CMD_RESULT_TYPE_CME_ERROR = 7;
+
     public int type = EVENT_TYPE_NONE;
     public int valueInt = 0;
     public int valueInt2 = 0;
diff --git a/android/app/src/com/android/bluetooth/hfpclient/VendorCommandResponseProcessor.java b/android/app/src/com/android/bluetooth/hfpclient/VendorCommandResponseProcessor.java
index 205ba40..cd2ec0d 100644
--- a/android/app/src/com/android/bluetooth/hfpclient/VendorCommandResponseProcessor.java
+++ b/android/app/src/com/android/bluetooth/hfpclient/VendorCommandResponseProcessor.java
@@ -69,6 +69,9 @@
         SUPPORTED_VENDOR_EVENTS.put(
                 "+XAPL=",
                 BluetoothAssignedNumbers.APPLE);
+        SUPPORTED_VENDOR_EVENTS.put(
+                "+ANDROID:",
+                BluetoothAssignedNumbers.GOOGLE);
     }
 
     VendorCommandResponseProcessor(HeadsetClientService context, NativeInterface nativeInterface) {
@@ -148,10 +151,14 @@
         if (vendorId == null) {
             Log.e(TAG, "Invalid response: " + atString + ". " + eventCode);
             return false;
+        } else if (vendorId == BluetoothAssignedNumbers.GOOGLE) {
+            Log.i(TAG, "received +ANDROID event. Setting Audio policy to true");
+            mService.setAudioPolicyRemoteSupported(device, true);
+        } else {
+            broadcastVendorSpecificEventIntent(vendorId, eventCode, atString, device);
+            logD("process vendor event " + vendorId + ", " + eventCode + ", "
+                    + atString + " for device" + device);
         }
-        broadcastVendorSpecificEventIntent(vendorId, eventCode, atString, device);
-        logD("process vendor event " + vendorId + ", " + eventCode + ", "
-                + atString + " for device" + device);
         return true;
     }
 
diff --git a/android/app/src/com/android/bluetooth/mapclient/MapClientService.java b/android/app/src/com/android/bluetooth/mapclient/MapClientService.java
index ff38353..25a8a2f 100644
--- a/android/app/src/com/android/bluetooth/mapclient/MapClientService.java
+++ b/android/app/src/com/android/bluetooth/mapclient/MapClientService.java
@@ -166,9 +166,6 @@
     }
 
     private synchronized void addDeviceToMapAndConnect(BluetoothDevice device) {
-        if (Utils.isInstrumentationTestMode()) {
-            Log.d(TAG, "addDeviceToMapAndConnect: device=" + device, new Exception());
-        }
         // When creating a new statemachine, its state is set to CONNECTING - which will trigger
         // connect.
         MceStateMachine mapStateMachine = new MceStateMachine(this, device);
@@ -358,6 +355,7 @@
             }
             stateMachine.doQuit();
         }
+        mMapInstanceMap.clear();
         return true;
     }
 
@@ -367,10 +365,6 @@
             Log.d(TAG, "in Cleanup");
         }
         removeUncleanAccounts();
-        mMapInstanceMap.clear();
-        if (Utils.isInstrumentationTestMode()) {
-            Log.d(TAG, "cleanup() called.", new Exception());
-        }
         // TODO(b/72948646): should be moved to stop()
         setMapClientService(null);
     }
diff --git a/android/app/src/com/android/bluetooth/mcp/MediaControlGattService.java b/android/app/src/com/android/bluetooth/mcp/MediaControlGattService.java
index 0f1fef7..b37cbaa 100644
--- a/android/app/src/com/android/bluetooth/mcp/MediaControlGattService.java
+++ b/android/app/src/com/android/bluetooth/mcp/MediaControlGattService.java
@@ -17,6 +17,7 @@
 
 package com.android.bluetooth.mcp;
 
+import static android.bluetooth.BluetoothDevice.METADATA_GMCS_CCCD;
 import static android.bluetooth.BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED;
 import static android.bluetooth.BluetoothGattCharacteristic.PERMISSION_WRITE_ENCRYPTED;
 import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_NOTIFY;
@@ -28,6 +29,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothGatt;
 import android.bluetooth.BluetoothGattCharacteristic;
@@ -37,12 +39,17 @@
 import android.bluetooth.BluetoothGattService;
 import android.bluetooth.BluetoothManager;
 import android.bluetooth.BluetoothProfile;
+import android.bluetooth.IBluetoothManager;
+import android.bluetooth.IBluetoothStateChangeCallback;
 import android.content.Context;
 import android.os.Handler;
 import android.os.Looper;
+import android.os.ParcelUuid;
+import android.os.RemoteException;
 import android.util.Log;
 import android.util.Pair;
 
+import com.android.bluetooth.Utils;
 import com.android.bluetooth.a2dp.A2dpService;
 import com.android.bluetooth.btservice.AdapterService;
 import com.android.bluetooth.hearingaid.HearingAidService;
@@ -441,7 +448,7 @@
                 } else {
                     status = BluetoothGatt.GATT_SUCCESS;
                     setCcc(device, op.mDescriptor.getCharacteristic().getUuid(), op.mOffset,
-                            op.mValue);
+                            op.mValue, true);
                 }
 
                 if (op.mResponseNeeded) {
@@ -522,6 +529,34 @@
         }
     }
 
+    private void restoreCccValuesForStoredDevices() {
+        for (BluetoothDevice device : mAdapterService.getBondedDevices()) {
+            byte[] gmcs_cccd = device.getMetadata(METADATA_GMCS_CCCD);
+
+            if ((gmcs_cccd == null) || (gmcs_cccd.length == 0)) {
+                return;
+            }
+
+            List<ParcelUuid> uuidList = Arrays.asList(Utils.byteArrayToUuid(gmcs_cccd));
+
+            /* Restore CCCD values for device */
+            for (ParcelUuid uuid : uuidList) {
+                setCcc(device, uuid.getUuid(), 0,
+                        BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE, false);
+            }
+        }
+    }
+
+    private final IBluetoothStateChangeCallback mBluetoothStateChangeCallback =
+            new IBluetoothStateChangeCallback.Stub() {
+                public void onBluetoothStateChange(boolean up) {
+                    if (DBG) Log.d(TAG, "onBluetoothStateChange: up=" + up);
+                    if (up) {
+                        restoreCccValuesForStoredDevices();
+                    }
+                }
+            };
+
     @VisibleForTesting
     final BluetoothGattServerCallback mServerCallback = new BluetoothGattServerCallback() {
         @Override
@@ -551,6 +586,7 @@
 
             mCharacteristics.get(CharId.CONTENT_CONTROL_ID)
                     .setValue(mCcid, BluetoothGattCharacteristic.FORMAT_UINT8, 0);
+            restoreCccValuesForStoredDevices();
             setInitialCharacteristicValuesAndNotify();
             initialStateRequest();
         }
@@ -796,6 +832,15 @@
         mMcpService = mcpService;
         mAdapterService =  Objects.requireNonNull(AdapterService.getAdapterService(),
                 "AdapterService shouldn't be null when creating MediaControlCattService");
+
+        IBluetoothManager mgr = BluetoothAdapter.getDefaultAdapter().getBluetoothManager();
+        if (mgr != null) {
+            try {
+                mgr.registerStateChangeCallback(mBluetoothStateChangeCallback);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
     }
 
     protected boolean init(UUID scvUuid) {
@@ -987,16 +1032,77 @@
         return mBluetoothGattServer.addService(mGattService);
     }
 
+    private void removeUuidFromMetadata(ParcelUuid charUuid, BluetoothDevice device) {
+        List<ParcelUuid> uuidList;
+        byte[] gmcs_cccd = device.getMetadata(METADATA_GMCS_CCCD);
+
+        if ((gmcs_cccd == null) || (gmcs_cccd.length == 0)) {
+            uuidList = new ArrayList<ParcelUuid>();
+        } else {
+            uuidList = new ArrayList<>(Arrays.asList(Utils.byteArrayToUuid(gmcs_cccd)));
+
+            if (!uuidList.contains(charUuid)) {
+                Log.d(TAG, "Characteristic CCCD can't be removed (not cached): "
+                        + charUuid.toString());
+                return;
+            }
+        }
+
+        uuidList.remove(charUuid);
+
+        if (!device.setMetadata(METADATA_GMCS_CCCD,
+                Utils.uuidsToByteArray(uuidList.toArray(new ParcelUuid[0])))) {
+            Log.e(TAG, "Can't set CCCD for GMCS characteristic UUID: " + charUuid.toString()
+                    + ", (remove)");
+        }
+    }
+
+    private void addUuidToMetadata(ParcelUuid charUuid, BluetoothDevice device) {
+        List<ParcelUuid> uuidList;
+        byte[] gmcs_cccd = device.getMetadata(METADATA_GMCS_CCCD);
+
+        if ((gmcs_cccd == null) || (gmcs_cccd.length == 0)) {
+            uuidList = new ArrayList<ParcelUuid>();
+        } else {
+            uuidList = new ArrayList<>(Arrays.asList(Utils.byteArrayToUuid(gmcs_cccd)));
+
+            if (uuidList.contains(charUuid)) {
+                Log.d(TAG, "Characteristic CCCD already added: " + charUuid.toString());
+                return;
+            }
+        }
+
+        uuidList.add(charUuid);
+
+        if (!device.setMetadata(METADATA_GMCS_CCCD,
+                Utils.uuidsToByteArray(uuidList.toArray(new ParcelUuid[0])))) {
+            Log.e(TAG, "Can't set CCCD for GMCS characteristic UUID: " + charUuid.toString()
+                    + ", (add)");
+        }
+    }
+
     @VisibleForTesting
-    void setCcc(BluetoothDevice device, UUID charUuid, int offset, byte[] value) {
+    void setCcc(BluetoothDevice device, UUID charUuid, int offset, byte[] value, boolean store) {
         HashMap<UUID, Short> characteristicCcc = mCccDescriptorValues.get(device.getAddress());
         if (characteristicCcc == null) {
             characteristicCcc = new HashMap<>();
             mCccDescriptorValues.put(device.getAddress(), characteristicCcc);
         }
 
-        characteristicCcc.put(
-                charUuid, ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN).getShort());
+        characteristicCcc.put(charUuid,
+                ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN).getShort());
+
+        if (!store) {
+            return;
+        }
+
+        if (Arrays.equals(value, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)) {
+            addUuidToMetadata(new ParcelUuid(charUuid), device);
+        } else if (Arrays.equals(value, BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)) {
+            removeUuidFromMetadata(new ParcelUuid(charUuid), device);
+        } else {
+            Log.e(TAG, "Not handled CCC value: " + Arrays.toString(value));
+        }
     }
 
     private byte[] getCccBytes(BluetoothDevice device, UUID charUuid) {
diff --git a/android/app/src/com/android/bluetooth/tbs/BluetoothGattServerProxy.java b/android/app/src/com/android/bluetooth/tbs/BluetoothGattServerProxy.java
index a68af61..59688a6 100644
--- a/android/app/src/com/android/bluetooth/tbs/BluetoothGattServerProxy.java
+++ b/android/app/src/com/android/bluetooth/tbs/BluetoothGattServerProxy.java
@@ -27,6 +27,7 @@
 import android.content.Context;
 
 import java.util.List;
+import java.util.UUID;
 
 /**
  * A proxy class that facilitates testing of the TbsService class.
@@ -63,6 +64,21 @@
         return mBluetoothGattServer.addService(service);
     }
 
+    /**
+     * A proxy that Returns a {@link BluetoothGattService} from the list of services offered
+     * by this device.
+     *
+     * <p>If multiple instances of the same service (as identified by UUID)
+     * exist, the first instance of the service is returned.
+     *
+     * @param uuid UUID of the requested service
+     * @return BluetoothGattService if supported, or null if the requested service is not offered by
+     * this device.
+     */
+    public BluetoothGattService getService(UUID uuid) {
+        return mBluetoothGattServer.getService(uuid);
+    }
+
     public boolean sendResponse(BluetoothDevice device, int requestId, int status, int offset,
             byte[] value) {
         return mBluetoothGattServer.sendResponse(device, requestId, status, offset, value);
diff --git a/android/app/src/com/android/bluetooth/tbs/TbsGatt.java b/android/app/src/com/android/bluetooth/tbs/TbsGatt.java
index 7c3f9fc..4c1833a 100644
--- a/android/app/src/com/android/bluetooth/tbs/TbsGatt.java
+++ b/android/app/src/com/android/bluetooth/tbs/TbsGatt.java
@@ -17,17 +17,27 @@
 
 package com.android.bluetooth.tbs;
 
+import static android.bluetooth.BluetoothDevice.METADATA_GTBS_CCCD;
+
+import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothGatt;
 import android.bluetooth.BluetoothGattCharacteristic;
 import android.bluetooth.BluetoothGattDescriptor;
 import android.bluetooth.BluetoothGattServerCallback;
 import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.IBluetoothManager;
+import android.bluetooth.IBluetoothStateChangeCallback;
 import android.content.Context;
 import android.os.Handler;
 import android.os.Looper;
+import android.os.ParcelUuid;
+import android.os.RemoteException;
 import android.util.Log;
 
+import com.android.bluetooth.btservice.AdapterService;
+import com.android.bluetooth.Utils;
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.io.ByteArrayOutputStream;
@@ -35,6 +45,7 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.UUID;
 
 public class TbsGatt {
@@ -131,9 +142,11 @@
     private final GattCharacteristic mTerminationReasonCharacteristic;
     private final GattCharacteristic mIncomingCallCharacteristic;
     private final GattCharacteristic mCallFriendlyNameCharacteristic;
+    private List<BluetoothDevice> mSubscribers = new ArrayList<>();
     private BluetoothGattServerProxy mBluetoothGattServer;
     private Handler mHandler;
     private Callback mCallback;
+    private AdapterService mAdapterService;
 
     public static abstract class Callback {
 
@@ -144,6 +157,17 @@
     }
 
     TbsGatt(Context context) {
+        mAdapterService =  Objects.requireNonNull(AdapterService.getAdapterService(),
+                "AdapterService shouldn't be null when creating MediaControlCattService");
+        IBluetoothManager mgr = BluetoothAdapter.getDefaultAdapter().getBluetoothManager();
+        if (mgr != null) {
+            try {
+                mgr.registerStateChangeCallback(mBluetoothStateChangeCallback);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
         mContext = context;
         mBearerProviderNameCharacteristic = new GattCharacteristic(UUID_BEARER_PROVIDER_NAME,
                 BluetoothGattCharacteristic.PROPERTY_READ
@@ -252,11 +276,55 @@
         return mContext;
     }
 
+    private void removeUuidFromMetadata(ParcelUuid charUuid, BluetoothDevice device) {
+        List<ParcelUuid> uuidList;
+        byte[] gtbs_cccd = device.getMetadata(METADATA_GTBS_CCCD);
+
+        if ((gtbs_cccd == null) || (gtbs_cccd.length == 0)) {
+            uuidList = new ArrayList<ParcelUuid>();
+        } else {
+            uuidList = new ArrayList<>(Arrays.asList(Utils.byteArrayToUuid(gtbs_cccd)));
+
+            if (!uuidList.contains(charUuid)) {
+                Log.d(TAG, "Characteristic CCCD can't be removed (not cached): "
+                        + charUuid.toString());
+                return;
+            }
+        }
+
+        uuidList.remove(charUuid);
+
+        if (!device.setMetadata(METADATA_GTBS_CCCD,
+                Utils.uuidsToByteArray(uuidList.toArray(new ParcelUuid[0])))) {
+            Log.e(TAG, "Can't set CCCD for GTBS characteristic UUID: " + charUuid + ", (remove)");
+        }
+    }
+
+    private void addUuidToMetadata(ParcelUuid charUuid, BluetoothDevice device) {
+        List<ParcelUuid> uuidList;
+        byte[] gtbs_cccd = device.getMetadata(METADATA_GTBS_CCCD);
+
+        if ((gtbs_cccd == null) || (gtbs_cccd.length == 0)) {
+            uuidList = new ArrayList<ParcelUuid>();
+        } else {
+            uuidList = new ArrayList<>(Arrays.asList(Utils.byteArrayToUuid(gtbs_cccd)));
+
+            if (uuidList.contains(charUuid)) {
+                Log.d(TAG, "Characteristic CCCD already add: " + charUuid.toString());
+                return;
+            }
+        }
+
+        uuidList.add(charUuid);
+
+        if (!device.setMetadata(METADATA_GTBS_CCCD,
+                Utils.uuidsToByteArray(uuidList.toArray(new ParcelUuid[0])))) {
+            Log.e(TAG, "Can't set CCCD for GTBS characteristic UUID: " + charUuid + ", (add)");
+        }
+    }
+
     /** Class that handles GATT characteristic notifications */
     private class BluetoothGattCharacteristicNotifier {
-
-        private List<BluetoothDevice> mSubscribers = new ArrayList<>();
-
         public int setSubscriptionConfiguration(BluetoothDevice device, byte[] configuration) {
             if (Arrays.equals(configuration, BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)) {
                 mSubscribers.remove(device);
@@ -455,6 +523,14 @@
                 return BluetoothGatt.GATT_FAILURE;
             }
 
+            if (Arrays.equals(value, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)) {
+                addUuidToMetadata(new ParcelUuid(characteristic.getUuid()), device);
+            } else if (Arrays.equals(value, BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)) {
+                removeUuidFromMetadata(new ParcelUuid(characteristic.getUuid()), device);
+            } else {
+                Log.e(TAG, "Not handled CCC value: " + Arrays.toString(value));
+            }
+
             return characteristic.setSubscriptionConfiguration(device, value);
         }
     }
@@ -659,13 +735,56 @@
         return UUID.fromString(UUID_PREFIX + uuid16 + UUID_SUFFIX);
     }
 
+    private void restoreCccValuesForStoredDevices() {
+        BluetoothGattService gattService = mBluetoothGattServer.getService(UUID_GTBS);
+
+        for (BluetoothDevice device : mAdapterService.getBondedDevices()) {
+            byte[] gtbs_cccd = device.getMetadata(METADATA_GTBS_CCCD);
+
+            if ((gtbs_cccd == null) || (gtbs_cccd.length == 0)) {
+                return;
+            }
+
+            List<ParcelUuid> uuidList = Arrays.asList(Utils.byteArrayToUuid(gtbs_cccd));
+
+            /* Restore CCCD values for device */
+            for (ParcelUuid uuid : uuidList) {
+                BluetoothGattCharacteristic characteristic =
+                        gattService.getCharacteristic(uuid.getUuid());
+                if (characteristic == null) {
+                    Log.e(TAG, "Invalid UUID stored in metadata: " + uuid.toString());
+                    continue;
+                }
+
+                BluetoothGattDescriptor descriptor =
+                        characteristic.getDescriptor(UUID_CLIENT_CHARACTERISTIC_CONFIGURATION);
+                if (descriptor == null) {
+                    Log.e(TAG, "Invalid characteristic, does not include CCCD");
+                    continue;
+                }
+
+                descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
+                mSubscribers.add(device);
+            }
+        }
+    }
+
+    private final IBluetoothStateChangeCallback mBluetoothStateChangeCallback =
+            new IBluetoothStateChangeCallback.Stub() {
+                public void onBluetoothStateChange(boolean up) {
+                    if (DBG) Log.d(TAG, "onBluetoothStateChange: up=" + up);
+                    if (up) {
+                        restoreCccValuesForStoredDevices();
+                    }
+                }
+            };
+
     /**
      * Callback to handle incoming requests to the GATT server. All read/write requests for
      * characteristics and descriptors are handled here.
      */
     @VisibleForTesting
     final BluetoothGattServerCallback mGattServerCallback = new BluetoothGattServerCallback() {
-
         @Override
         public void onServiceAdded(int status, BluetoothGattService service) {
             if (DBG) {
@@ -674,6 +793,8 @@
             if (mCallback != null) {
                 mCallback.onServiceAdded(status == BluetoothGatt.GATT_SUCCESS);
             }
+
+            restoreCccValuesForStoredDevices();
         }
 
         @Override
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/ActiveDeviceManagerTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/ActiveDeviceManagerTest.java
index 0854962..5574b87 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/btservice/ActiveDeviceManagerTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/ActiveDeviceManagerTest.java
@@ -26,6 +26,7 @@
 
 import android.bluetooth.BluetoothA2dp;
 import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothAudioPolicy;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHapClient;
 import android.bluetooth.BluetoothHeadset;
@@ -123,6 +124,8 @@
         mMostRecentDevice = null;
 
         when(mA2dpService.setActiveDevice(any())).thenReturn(true);
+        when(mHeadsetService.getHfpCallAudioPolicy(any())).thenReturn(
+                new BluetoothAudioPolicy.Builder().build());
         when(mHeadsetService.setActiveDevice(any())).thenReturn(true);
         when(mHearingAidService.setActiveDevice(any())).thenReturn(true);
         when(mLeAudioService.setActiveDevice(any())).thenReturn(true);
@@ -310,6 +313,23 @@
     }
 
     /**
+     * A headset device with connecting audio policy set to NOT ALLOWED.
+     */
+    @Test
+    public void notAllowedConnectingPolicyHeadsetConnected_noSetActiveDevice() {
+        // setting connecting policy to NOT ALLOWED
+        when(mHeadsetService.getHfpCallAudioPolicy(mHeadsetDevice))
+                .thenReturn(new BluetoothAudioPolicy.Builder()
+                        .setCallEstablishPolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+                        .setConnectingTimePolicy(BluetoothAudioPolicy.POLICY_NOT_ALLOWED)
+                        .setInBandRingtonePolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+                        .build());
+
+        headsetConnected(mHeadsetDevice);
+        verify(mHeadsetService, never()).setActiveDevice(mHeadsetDevice);
+    }
+
+    /**
      * A combo (A2DP + Headset) device is connected. Then a Hearing Aid is connected.
      */
     @Test
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java
index 4d30966..a229499 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java
@@ -280,6 +280,10 @@
                 .thenReturn(mBatteryStatsManager);
         when(mMockContext.getSystemServiceName(BatteryStatsManager.class))
                 .thenReturn(Context.BATTERY_STATS_SERVICE);
+        when(mMockContext.getSharedPreferences(anyString(), anyInt()))
+                .thenReturn(InstrumentationRegistry.getTargetContext()
+                        .getSharedPreferences("AdapterServiceTestPrefs", Context.MODE_PRIVATE));
+
         when(mMockContext.getAttributionSource()).thenReturn(mAttributionSource);
         doAnswer(invocation -> {
             Object[] args = invocation.getArguments();
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/CompanionManagerTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/CompanionManagerTest.java
new file mode 100644
index 0000000..41746f0
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/CompanionManagerTest.java
@@ -0,0 +1,169 @@
+/*
+ * 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.btservice;
+
+import static org.mockito.Mockito.*;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.HandlerThread;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.TestUtils;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class CompanionManagerTest {
+
+    private static final String TEST_DEVICE = "11:22:33:44:55:66";
+
+    private AdapterProperties mAdapterProperties;
+    private Context mTargetContext;
+    private CompanionManager mCompanionManager;
+
+    private HandlerThread mHandlerThread;
+
+    @Mock
+    private AdapterService mAdapterService;
+    @Mock
+    SharedPreferences mSharedPreferences;
+    @Mock
+    SharedPreferences.Editor mEditor;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mTargetContext = InstrumentationRegistry.getTargetContext();
+        // Prepare the TestUtils
+        TestUtils.setAdapterService(mAdapterService);
+        // Start handler thread for this test
+        mHandlerThread = new HandlerThread("CompanionManagerTestHandlerThread");
+        mHandlerThread.start();
+        // Mock the looper
+        doReturn(mHandlerThread.getLooper()).when(mAdapterService).getMainLooper();
+        // Mock SharedPreferences
+        when(mSharedPreferences.edit()).thenReturn(mEditor);
+        doReturn(mSharedPreferences).when(mAdapterService).getSharedPreferences(eq(
+                CompanionManager.COMPANION_INFO), eq(Context.MODE_PRIVATE));
+        // Tell the AdapterService that it is a mock (see isMock documentation)
+        doReturn(true).when(mAdapterService).isMock();
+        // Use the resources in the instrumentation instead of the mocked AdapterService
+        when(mAdapterService.getResources()).thenReturn(mTargetContext.getResources());
+
+        // Must be called to initialize services
+        mCompanionManager = new CompanionManager(mAdapterService, null);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mHandlerThread.quit();
+        TestUtils.clearAdapterService(mAdapterService);
+    }
+
+    @Test
+    public void testLoadCompanionInfo_hasCompanionDeviceKey() {
+        loadCompanionInfoHelper(TEST_DEVICE, CompanionManager.COMPANION_TYPE_PRIMARY);
+    }
+
+    @Test
+    public void testLoadCompanionInfo_noCompanionDeviceSetButHaveBondedDevices_shouldNotCrash() {
+        BluetoothDevice[] devices = new BluetoothDevice[2];
+        doReturn(devices).when(mAdapterService).getBondedDevices();
+        doThrow(new IllegalArgumentException())
+                .when(mSharedPreferences)
+                .getInt(eq(CompanionManager.COMPANION_TYPE_KEY), anyInt());
+        mCompanionManager.loadCompanionInfo();
+    }
+
+    @Test
+    public void testIsCompanionDevice() {
+        loadCompanionInfoHelper(TEST_DEVICE, CompanionManager.COMPANION_TYPE_NONE);
+        Assert.assertTrue(mCompanionManager.isCompanionDevice(TEST_DEVICE));
+
+        loadCompanionInfoHelper(TEST_DEVICE, CompanionManager.COMPANION_TYPE_PRIMARY);
+        Assert.assertTrue(mCompanionManager.isCompanionDevice(TEST_DEVICE));
+
+        loadCompanionInfoHelper(TEST_DEVICE, CompanionManager.COMPANION_TYPE_SECONDARY);
+        Assert.assertTrue(mCompanionManager.isCompanionDevice(TEST_DEVICE));
+    }
+
+    @Test
+    public void testGetGattConnParameterPrimary() {
+        loadCompanionInfoHelper(TEST_DEVICE, CompanionManager.COMPANION_TYPE_PRIMARY);
+        checkReasonableConnParameterHelper(BluetoothGatt.CONNECTION_PRIORITY_HIGH);
+        checkReasonableConnParameterHelper(BluetoothGatt.CONNECTION_PRIORITY_BALANCED);
+        checkReasonableConnParameterHelper(BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER);
+
+        loadCompanionInfoHelper(TEST_DEVICE, CompanionManager.COMPANION_TYPE_SECONDARY);
+        checkReasonableConnParameterHelper(BluetoothGatt.CONNECTION_PRIORITY_HIGH);
+        checkReasonableConnParameterHelper(BluetoothGatt.CONNECTION_PRIORITY_BALANCED);
+        checkReasonableConnParameterHelper(BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER);
+
+        loadCompanionInfoHelper(TEST_DEVICE, CompanionManager.COMPANION_TYPE_NONE);
+        checkReasonableConnParameterHelper(BluetoothGatt.CONNECTION_PRIORITY_HIGH);
+        checkReasonableConnParameterHelper(BluetoothGatt.CONNECTION_PRIORITY_BALANCED);
+        checkReasonableConnParameterHelper(BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER);
+    }
+
+    private void loadCompanionInfoHelper(String address, int companionType) {
+        doReturn(address)
+                .when(mSharedPreferences)
+                .getString(eq(CompanionManager.COMPANION_DEVICE_KEY), anyString());
+        doReturn(companionType)
+                .when(mSharedPreferences)
+                .getInt(eq(CompanionManager.COMPANION_TYPE_KEY), anyInt());
+        mCompanionManager.loadCompanionInfo();
+    }
+
+    private void checkReasonableConnParameterHelper(int priority) {
+        // Max/Min values from the Bluetooth spec Version 5.3 | Vol 4, Part E | 7.8.18
+        final int minInterval = 6;    // 0x0006
+        final int maxInterval = 3200; // 0x0C80
+        final int minLatency = 0;     // 0x0000
+        final int maxLatency = 499;   // 0x01F3
+
+        int min = mCompanionManager.getGattConnParameters(
+                TEST_DEVICE, CompanionManager.GATT_CONN_INTERVAL_MIN,
+                priority);
+        int max = mCompanionManager.getGattConnParameters(
+                TEST_DEVICE, CompanionManager.GATT_CONN_INTERVAL_MAX,
+                priority);
+        int latency = mCompanionManager.getGattConnParameters(
+                TEST_DEVICE, CompanionManager.GATT_CONN_LATENCY,
+                priority);
+
+        Assert.assertTrue(max >= min);
+        Assert.assertTrue(max >= minInterval);
+        Assert.assertTrue(min >= minInterval);
+        Assert.assertTrue(max <= maxInterval);
+        Assert.assertTrue(min <= maxInterval);
+        Assert.assertTrue(latency >= minLatency);
+        Assert.assertTrue(latency <= maxLatency);
+    }
+}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/RemoteDevicesTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/RemoteDevicesTest.java
index 42e93e5..22df72c 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/btservice/RemoteDevicesTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/RemoteDevicesTest.java
@@ -6,6 +6,7 @@
 
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothAssignedNumbers;
+import android.bluetooth.BluetoothAudioPolicy;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
 import android.bluetooth.BluetoothHeadsetClient;
@@ -21,6 +22,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.bluetooth.Utils;
+import com.android.bluetooth.btservice.RemoteDevices.DeviceProperties;
 import com.android.bluetooth.hfp.HeadsetHalConstants;
 
 import org.junit.After;
@@ -517,6 +519,26 @@
         Assert.assertNull(mRemoteDevices.getDeviceProperties(mDevice1));
     }
 
+    @Test
+    public void testSetgetHfAudioPolicyForRemoteAg() {
+        // Verify that device property is null initially
+        Assert.assertNull(mRemoteDevices.getDeviceProperties(mDevice1));
+
+        mRemoteDevices.addDeviceProperties(Utils.getBytesFromAddress(TEST_BT_ADDR_1));
+
+        DeviceProperties deviceProp = mRemoteDevices.getDeviceProperties(mDevice1);
+        BluetoothAudioPolicy policies = new BluetoothAudioPolicy.Builder()
+                .setCallEstablishPolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+                .setConnectingTimePolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+                .setInBandRingtonePolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+                .build();
+        deviceProp.setHfAudioPolicyForRemoteAg(policies);
+
+        // Verify that the audio policy properties are set and get propperly
+        Assert.assertEquals(policies, mRemoteDevices.getDeviceProperties(mDevice1)
+                .getHfAudioPolicyForRemoteAg());
+    }
+
     private static void verifyBatteryLevelChangedIntent(BluetoothDevice device, int batteryLevel,
             ArgumentCaptor<Intent> intentArgument) {
         verifyBatteryLevelChangedIntent(device, batteryLevel, intentArgument.getValue());
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
index 28357b6..2bf7302 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
@@ -28,6 +28,7 @@
 
 import android.bluetooth.BluetoothA2dp;
 import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothAudioPolicy;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothProfile;
 import android.content.ContentValues;
@@ -387,6 +388,10 @@
                 value, true);
         testSetGetCustomMetaCase(false, BluetoothDevice.METADATA_LE_AUDIO,
                 value, true);
+        testSetGetCustomMetaCase(false, BluetoothDevice.METADATA_GMCS_CCCD,
+                value, true);
+        testSetGetCustomMetaCase(false, BluetoothDevice.METADATA_GTBS_CCCD,
+                value, true);
         testSetGetCustomMetaCase(false, badKey, value, false);
 
         // Device is in database
@@ -447,6 +452,24 @@
                 value, true);
         testSetGetCustomMetaCase(true, BluetoothDevice.METADATA_LE_AUDIO,
                 value, true);
+        testSetGetCustomMetaCase(true, BluetoothDevice.METADATA_GMCS_CCCD,
+                value, true);
+        testSetGetCustomMetaCase(true, BluetoothDevice.METADATA_GTBS_CCCD,
+                value, true);
+    }
+    @Test
+    public void testSetGetAudioPolicyMetaData() {
+        int badKey = 100;
+        BluetoothAudioPolicy value = new BluetoothAudioPolicy.Builder()
+                .setCallEstablishPolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+                .setConnectingTimePolicy(BluetoothAudioPolicy.POLICY_NOT_ALLOWED)
+                .setInBandRingtonePolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+                .build();
+
+        // Device is not in database
+        testSetGetAudioPolicyMetadataCase(false, value, true);
+        // Device is in database
+        testSetGetAudioPolicyMetadataCase(true, value, true);
     }
 
     @Test
@@ -1143,7 +1166,7 @@
     @Test
     public void testDatabaseMigration_111_112() throws IOException {
         String testString = "TEST STRING";
-        // Create a database with version 109
+        // Create a database with version 111
         SupportSQLiteDatabase db = testHelper.createDatabase(DB_NAME, 111);
         // insert a device to the database
         ContentValues device = new ContentValues();
@@ -1189,7 +1212,7 @@
 
     @Test
     public void testDatabaseMigration_113_114() throws IOException {
-        // Create a database with version 112
+        // Create a database with version 113
         SupportSQLiteDatabase db = testHelper.createDatabase(DB_NAME, 113);
         // insert a device to the database
         ContentValues device = new ContentValues();
@@ -1209,6 +1232,83 @@
         }
     }
 
+    @Test
+    public void testDatabaseMigration_114_115() throws IOException {
+        // Create a database with version 114
+        SupportSQLiteDatabase db = testHelper.createDatabase(DB_NAME, 114);
+        // insert a device to the database
+        ContentValues device = new ContentValues();
+        device.put("address", TEST_BT_ADDR);
+        device.put("migrated", false);
+        assertThat(db.insert("metadata", SQLiteDatabase.CONFLICT_IGNORE, device),
+                CoreMatchers.not(-1));
+
+        // Migrate database from 114 to 115
+        db.close();
+        db = testHelper.runMigrationsAndValidate(DB_NAME, 115, true,
+                MetadataDatabase.MIGRATION_114_115);
+        Cursor cursor = db.query("SELECT * FROM metadata");
+
+        assertHasColumn(cursor, "call_establish_audio_policy", true);
+        assertHasColumn(cursor, "connecting_time_audio_policy", true);
+        assertHasColumn(cursor, "in_band_ringtone_audio_policy", true);
+        while (cursor.moveToNext()) {
+            // Check the new columns was added with default value
+            assertColumnBlobData(cursor, "call_establish_audio_policy", null);
+            assertColumnBlobData(cursor, "connecting_time_audio_policy", null);
+            assertColumnBlobData(cursor, "in_band_ringtone_audio_policy", null);
+        }
+    }
+
+    @Test
+    public void testDatabaseMigration_115_116() throws IOException {
+        // Create a database with version 115
+        SupportSQLiteDatabase db = testHelper.createDatabase(DB_NAME, 115);
+        // insert a device to the database
+        ContentValues device = new ContentValues();
+        device.put("address", TEST_BT_ADDR);
+        device.put("migrated", false);
+        assertThat(db.insert("metadata", SQLiteDatabase.CONFLICT_IGNORE, device),
+                CoreMatchers.not(-1));
+
+        // Migrate database from 115 to 116
+        db.close();
+        db = testHelper.runMigrationsAndValidate(DB_NAME, 116, true,
+                MetadataDatabase.MIGRATION_115_116);
+        Cursor cursor = db.query("SELECT * FROM metadata");
+        assertHasColumn(cursor, "preferred_output_only_profile", true);
+        assertHasColumn(cursor, "preferred_duplex_profile", true);
+        while (cursor.moveToNext()) {
+            // Check the new columns was added with default value
+            assertColumnIntData(cursor, "preferred_output_only_profile", 0);
+            assertColumnIntData(cursor, "preferred_duplex_profile", 0);
+        }
+    }
+
+    @Test
+    public void testDatabaseMigration_116_117() throws IOException {
+        // Create a database with version 116
+        SupportSQLiteDatabase db = testHelper.createDatabase(DB_NAME, 116);
+        // insert a device to the database
+        ContentValues device = new ContentValues();
+        device.put("address", TEST_BT_ADDR);
+        device.put("migrated", false);
+        assertThat(db.insert("metadata", SQLiteDatabase.CONFLICT_IGNORE, device),
+                CoreMatchers.not(-1));
+        // Migrate database from 116 to 117
+        db.close();
+        db = testHelper.runMigrationsAndValidate(DB_NAME, 117, true,
+                MetadataDatabase.MIGRATION_116_117);
+        Cursor cursor = db.query("SELECT * FROM metadata");
+        assertHasColumn(cursor, "gmcs_cccd", true);
+        assertHasColumn(cursor, "gtbs_cccd", true);
+        while (cursor.moveToNext()) {
+            // Check the new columns was added with default value
+            assertColumnBlobData(cursor, "gmcs_cccd", null);
+            assertColumnBlobData(cursor, "gtbs_cccd", null);
+        }
+    }
+
     /**
      * Helper function to check whether the database has the expected column
      */
@@ -1379,4 +1479,38 @@
         // Wait for clear database
         TestUtils.waitForLooperToFinishScheduledTask(mDatabaseManager.getHandlerLooper());
     }
+
+    void testSetGetAudioPolicyMetadataCase(boolean stored,
+                BluetoothAudioPolicy policy, boolean expectedResult) {
+        BluetoothAudioPolicy testPolicy = new BluetoothAudioPolicy.Builder().build();
+        if (stored) {
+            Metadata data = new Metadata(TEST_BT_ADDR);
+            mDatabaseManager.mMetadataCache.put(TEST_BT_ADDR, data);
+            mDatabase.insert(data);
+            Assert.assertEquals(expectedResult,
+                    mDatabaseManager.setAudioPolicyMetadata(mTestDevice, testPolicy));
+        }
+        Assert.assertEquals(expectedResult,
+                mDatabaseManager.setAudioPolicyMetadata(mTestDevice, policy));
+        if (expectedResult) {
+            // Check for callback and get value
+            Assert.assertEquals(policy,
+                    mDatabaseManager.getAudioPolicyMetadata(mTestDevice));
+        } else {
+            Assert.assertNull(mDatabaseManager.getAudioPolicyMetadata(mTestDevice));
+            return;
+        }
+        // Wait for database update
+        TestUtils.waitForLooperToFinishScheduledTask(mDatabaseManager.getHandlerLooper());
+
+        // Check whether the value is saved in database
+        restartDatabaseManagerHelper();
+        Assert.assertEquals(policy,
+                mDatabaseManager.getAudioPolicyMetadata(mTestDevice));
+
+        mDatabaseManager.factoryReset();
+        mDatabaseManager.mMetadataCache.clear();
+        // Wait for clear database
+        TestUtils.waitForLooperToFinishScheduledTask(mDatabaseManager.getHandlerLooper());
+    }
 }
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/115.json b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/115.json
new file mode 100644
index 0000000..5d576dc
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/115.json
@@ -0,0 +1,358 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 115,
+    "identityHash": "c61976c8f6248cefd19ef8f25f543e01",
+    "entities": [
+      {
+        "tableName": "metadata",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`address` TEXT NOT NULL, `migrated` INTEGER NOT NULL, `a2dpSupportsOptionalCodecs` INTEGER NOT NULL, `a2dpOptionalCodecsEnabled` INTEGER NOT NULL, `last_active_time` INTEGER NOT NULL, `is_active_a2dp_device` INTEGER NOT NULL, `a2dp_connection_policy` INTEGER, `a2dp_sink_connection_policy` INTEGER, `hfp_connection_policy` INTEGER, `hfp_client_connection_policy` INTEGER, `hid_host_connection_policy` INTEGER, `pan_connection_policy` INTEGER, `pbap_connection_policy` INTEGER, `pbap_client_connection_policy` INTEGER, `map_connection_policy` INTEGER, `sap_connection_policy` INTEGER, `hearing_aid_connection_policy` INTEGER, `hap_client_connection_policy` INTEGER, `map_client_connection_policy` INTEGER, `le_audio_connection_policy` INTEGER, `volume_control_connection_policy` INTEGER, `csip_set_coordinator_connection_policy` INTEGER, `le_call_control_connection_policy` INTEGER, `bass_client_connection_policy` INTEGER, `battery_connection_policy` INTEGER, `manufacturer_name` BLOB, `model_name` BLOB, `software_version` BLOB, `hardware_version` BLOB, `companion_app` BLOB, `main_icon` BLOB, `is_untethered_headset` BLOB, `untethered_left_icon` BLOB, `untethered_right_icon` BLOB, `untethered_case_icon` BLOB, `untethered_left_battery` BLOB, `untethered_right_battery` BLOB, `untethered_case_battery` BLOB, `untethered_left_charging` BLOB, `untethered_right_charging` BLOB, `untethered_case_charging` BLOB, `enhanced_settings_ui_uri` BLOB, `device_type` BLOB, `main_battery` BLOB, `main_charging` BLOB, `main_low_battery_threshold` BLOB, `untethered_left_low_battery_threshold` BLOB, `untethered_right_low_battery_threshold` BLOB, `untethered_case_low_battery_threshold` BLOB, `spatial_audio` BLOB, `fastpair_customized` BLOB, `le_audio` BLOB, `call_establish_audio_policy` INTEGER, `connecting_time_audio_policy` INTEGER, `in_band_ringtone_audio_policy` INTEGER, PRIMARY KEY(`address`))",
+        "fields": [
+          {
+            "fieldPath": "address",
+            "columnName": "address",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "migrated",
+            "columnName": "migrated",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "a2dpSupportsOptionalCodecs",
+            "columnName": "a2dpSupportsOptionalCodecs",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "a2dpOptionalCodecsEnabled",
+            "columnName": "a2dpOptionalCodecsEnabled",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "last_active_time",
+            "columnName": "last_active_time",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "is_active_a2dp_device",
+            "columnName": "is_active_a2dp_device",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.a2dp_connection_policy",
+            "columnName": "a2dp_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.a2dp_sink_connection_policy",
+            "columnName": "a2dp_sink_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.hfp_connection_policy",
+            "columnName": "hfp_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.hfp_client_connection_policy",
+            "columnName": "hfp_client_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.hid_host_connection_policy",
+            "columnName": "hid_host_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.pan_connection_policy",
+            "columnName": "pan_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.pbap_connection_policy",
+            "columnName": "pbap_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.pbap_client_connection_policy",
+            "columnName": "pbap_client_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.map_connection_policy",
+            "columnName": "map_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.sap_connection_policy",
+            "columnName": "sap_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.hearing_aid_connection_policy",
+            "columnName": "hearing_aid_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.hap_client_connection_policy",
+            "columnName": "hap_client_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.map_client_connection_policy",
+            "columnName": "map_client_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.le_audio_connection_policy",
+            "columnName": "le_audio_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.volume_control_connection_policy",
+            "columnName": "volume_control_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.csip_set_coordinator_connection_policy",
+            "columnName": "csip_set_coordinator_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.le_call_control_connection_policy",
+            "columnName": "le_call_control_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.bass_client_connection_policy",
+            "columnName": "bass_client_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.battery_connection_policy",
+            "columnName": "battery_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.manufacturer_name",
+            "columnName": "manufacturer_name",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.model_name",
+            "columnName": "model_name",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.software_version",
+            "columnName": "software_version",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.hardware_version",
+            "columnName": "hardware_version",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.companion_app",
+            "columnName": "companion_app",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.main_icon",
+            "columnName": "main_icon",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.is_untethered_headset",
+            "columnName": "is_untethered_headset",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_left_icon",
+            "columnName": "untethered_left_icon",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_right_icon",
+            "columnName": "untethered_right_icon",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_case_icon",
+            "columnName": "untethered_case_icon",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_left_battery",
+            "columnName": "untethered_left_battery",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_right_battery",
+            "columnName": "untethered_right_battery",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_case_battery",
+            "columnName": "untethered_case_battery",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_left_charging",
+            "columnName": "untethered_left_charging",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_right_charging",
+            "columnName": "untethered_right_charging",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_case_charging",
+            "columnName": "untethered_case_charging",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.enhanced_settings_ui_uri",
+            "columnName": "enhanced_settings_ui_uri",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.device_type",
+            "columnName": "device_type",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.main_battery",
+            "columnName": "main_battery",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.main_charging",
+            "columnName": "main_charging",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.main_low_battery_threshold",
+            "columnName": "main_low_battery_threshold",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_left_low_battery_threshold",
+            "columnName": "untethered_left_low_battery_threshold",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_right_low_battery_threshold",
+            "columnName": "untethered_right_low_battery_threshold",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_case_low_battery_threshold",
+            "columnName": "untethered_case_low_battery_threshold",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.spatial_audio",
+            "columnName": "spatial_audio",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.fastpair_customized",
+            "columnName": "fastpair_customized",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.le_audio",
+            "columnName": "le_audio",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "audioPolicyMetadata.callEstablishAudioPolicy",
+            "columnName": "call_establish_audio_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "audioPolicyMetadata.connectingTimeAudioPolicy",
+            "columnName": "connecting_time_audio_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "audioPolicyMetadata.inBandRingtoneAudioPolicy",
+            "columnName": "in_band_ringtone_audio_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "address"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": []
+      }
+    ],
+    "views": [],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c61976c8f6248cefd19ef8f25f543e01')"
+    ]
+  }
+}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/116.json b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/116.json
new file mode 100644
index 0000000..2e85b51
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/116.json
@@ -0,0 +1,370 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 116,
+    "identityHash": "0b8549de3acad8b14fe6f7198206ea02",
+    "entities": [
+      {
+        "tableName": "metadata",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`address` TEXT NOT NULL, `migrated` INTEGER NOT NULL, `a2dpSupportsOptionalCodecs` INTEGER NOT NULL, `a2dpOptionalCodecsEnabled` INTEGER NOT NULL, `last_active_time` INTEGER NOT NULL, `is_active_a2dp_device` INTEGER NOT NULL, `preferred_output_only_profile` INTEGER NOT NULL, `preferred_duplex_profile` INTEGER NOT NULL, `a2dp_connection_policy` INTEGER, `a2dp_sink_connection_policy` INTEGER, `hfp_connection_policy` INTEGER, `hfp_client_connection_policy` INTEGER, `hid_host_connection_policy` INTEGER, `pan_connection_policy` INTEGER, `pbap_connection_policy` INTEGER, `pbap_client_connection_policy` INTEGER, `map_connection_policy` INTEGER, `sap_connection_policy` INTEGER, `hearing_aid_connection_policy` INTEGER, `hap_client_connection_policy` INTEGER, `map_client_connection_policy` INTEGER, `le_audio_connection_policy` INTEGER, `volume_control_connection_policy` INTEGER, `csip_set_coordinator_connection_policy` INTEGER, `le_call_control_connection_policy` INTEGER, `bass_client_connection_policy` INTEGER, `battery_connection_policy` INTEGER, `manufacturer_name` BLOB, `model_name` BLOB, `software_version` BLOB, `hardware_version` BLOB, `companion_app` BLOB, `main_icon` BLOB, `is_untethered_headset` BLOB, `untethered_left_icon` BLOB, `untethered_right_icon` BLOB, `untethered_case_icon` BLOB, `untethered_left_battery` BLOB, `untethered_right_battery` BLOB, `untethered_case_battery` BLOB, `untethered_left_charging` BLOB, `untethered_right_charging` BLOB, `untethered_case_charging` BLOB, `enhanced_settings_ui_uri` BLOB, `device_type` BLOB, `main_battery` BLOB, `main_charging` BLOB, `main_low_battery_threshold` BLOB, `untethered_left_low_battery_threshold` BLOB, `untethered_right_low_battery_threshold` BLOB, `untethered_case_low_battery_threshold` BLOB, `spatial_audio` BLOB, `fastpair_customized` BLOB, `le_audio` BLOB, `call_establish_audio_policy` INTEGER, `connecting_time_audio_policy` INTEGER, `in_band_ringtone_audio_policy` INTEGER, PRIMARY KEY(`address`))",
+        "fields": [
+          {
+            "fieldPath": "address",
+            "columnName": "address",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "migrated",
+            "columnName": "migrated",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "a2dpSupportsOptionalCodecs",
+            "columnName": "a2dpSupportsOptionalCodecs",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "a2dpOptionalCodecsEnabled",
+            "columnName": "a2dpOptionalCodecsEnabled",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "last_active_time",
+            "columnName": "last_active_time",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "is_active_a2dp_device",
+            "columnName": "is_active_a2dp_device",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "preferred_output_only_profile",
+            "columnName": "preferred_output_only_profile",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "preferred_duplex_profile",
+            "columnName": "preferred_duplex_profile",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.a2dp_connection_policy",
+            "columnName": "a2dp_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.a2dp_sink_connection_policy",
+            "columnName": "a2dp_sink_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.hfp_connection_policy",
+            "columnName": "hfp_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.hfp_client_connection_policy",
+            "columnName": "hfp_client_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.hid_host_connection_policy",
+            "columnName": "hid_host_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.pan_connection_policy",
+            "columnName": "pan_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.pbap_connection_policy",
+            "columnName": "pbap_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.pbap_client_connection_policy",
+            "columnName": "pbap_client_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.map_connection_policy",
+            "columnName": "map_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.sap_connection_policy",
+            "columnName": "sap_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.hearing_aid_connection_policy",
+            "columnName": "hearing_aid_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.hap_client_connection_policy",
+            "columnName": "hap_client_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.map_client_connection_policy",
+            "columnName": "map_client_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.le_audio_connection_policy",
+            "columnName": "le_audio_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.volume_control_connection_policy",
+            "columnName": "volume_control_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.csip_set_coordinator_connection_policy",
+            "columnName": "csip_set_coordinator_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.le_call_control_connection_policy",
+            "columnName": "le_call_control_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.bass_client_connection_policy",
+            "columnName": "bass_client_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.battery_connection_policy",
+            "columnName": "battery_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.manufacturer_name",
+            "columnName": "manufacturer_name",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.model_name",
+            "columnName": "model_name",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.software_version",
+            "columnName": "software_version",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.hardware_version",
+            "columnName": "hardware_version",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.companion_app",
+            "columnName": "companion_app",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.main_icon",
+            "columnName": "main_icon",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.is_untethered_headset",
+            "columnName": "is_untethered_headset",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_left_icon",
+            "columnName": "untethered_left_icon",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_right_icon",
+            "columnName": "untethered_right_icon",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_case_icon",
+            "columnName": "untethered_case_icon",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_left_battery",
+            "columnName": "untethered_left_battery",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_right_battery",
+            "columnName": "untethered_right_battery",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_case_battery",
+            "columnName": "untethered_case_battery",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_left_charging",
+            "columnName": "untethered_left_charging",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_right_charging",
+            "columnName": "untethered_right_charging",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_case_charging",
+            "columnName": "untethered_case_charging",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.enhanced_settings_ui_uri",
+            "columnName": "enhanced_settings_ui_uri",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.device_type",
+            "columnName": "device_type",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.main_battery",
+            "columnName": "main_battery",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.main_charging",
+            "columnName": "main_charging",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.main_low_battery_threshold",
+            "columnName": "main_low_battery_threshold",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_left_low_battery_threshold",
+            "columnName": "untethered_left_low_battery_threshold",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_right_low_battery_threshold",
+            "columnName": "untethered_right_low_battery_threshold",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_case_low_battery_threshold",
+            "columnName": "untethered_case_low_battery_threshold",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.spatial_audio",
+            "columnName": "spatial_audio",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.fastpair_customized",
+            "columnName": "fastpair_customized",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.le_audio",
+            "columnName": "le_audio",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "audioPolicyMetadata.callEstablishAudioPolicy",
+            "columnName": "call_establish_audio_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "audioPolicyMetadata.connectingTimeAudioPolicy",
+            "columnName": "connecting_time_audio_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "audioPolicyMetadata.inBandRingtoneAudioPolicy",
+            "columnName": "in_band_ringtone_audio_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "address"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": []
+      }
+    ],
+    "views": [],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0b8549de3acad8b14fe6f7198206ea02')"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/117.json b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/117.json
new file mode 100644
index 0000000..d4c1f7e
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/117.json
@@ -0,0 +1,382 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 117,
+    "identityHash": "b3363a857e6d4f3ece8ba92d57d52c26",
+    "entities": [
+      {
+        "tableName": "metadata",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`address` TEXT NOT NULL, `migrated` INTEGER NOT NULL, `a2dpSupportsOptionalCodecs` INTEGER NOT NULL, `a2dpOptionalCodecsEnabled` INTEGER NOT NULL, `last_active_time` INTEGER NOT NULL, `is_active_a2dp_device` INTEGER NOT NULL, `preferred_output_only_profile` INTEGER NOT NULL, `preferred_duplex_profile` INTEGER NOT NULL, `a2dp_connection_policy` INTEGER, `a2dp_sink_connection_policy` INTEGER, `hfp_connection_policy` INTEGER, `hfp_client_connection_policy` INTEGER, `hid_host_connection_policy` INTEGER, `pan_connection_policy` INTEGER, `pbap_connection_policy` INTEGER, `pbap_client_connection_policy` INTEGER, `map_connection_policy` INTEGER, `sap_connection_policy` INTEGER, `hearing_aid_connection_policy` INTEGER, `hap_client_connection_policy` INTEGER, `map_client_connection_policy` INTEGER, `le_audio_connection_policy` INTEGER, `volume_control_connection_policy` INTEGER, `csip_set_coordinator_connection_policy` INTEGER, `le_call_control_connection_policy` INTEGER, `bass_client_connection_policy` INTEGER, `battery_connection_policy` INTEGER, `manufacturer_name` BLOB, `model_name` BLOB, `software_version` BLOB, `hardware_version` BLOB, `companion_app` BLOB, `main_icon` BLOB, `is_untethered_headset` BLOB, `untethered_left_icon` BLOB, `untethered_right_icon` BLOB, `untethered_case_icon` BLOB, `untethered_left_battery` BLOB, `untethered_right_battery` BLOB, `untethered_case_battery` BLOB, `untethered_left_charging` BLOB, `untethered_right_charging` BLOB, `untethered_case_charging` BLOB, `enhanced_settings_ui_uri` BLOB, `device_type` BLOB, `main_battery` BLOB, `main_charging` BLOB, `main_low_battery_threshold` BLOB, `untethered_left_low_battery_threshold` BLOB, `untethered_right_low_battery_threshold` BLOB, `untethered_case_low_battery_threshold` BLOB, `spatial_audio` BLOB, `fastpair_customized` BLOB, `le_audio` BLOB, `gmcs_cccd` BLOB, `gtbs_cccd` BLOB, `call_establish_audio_policy` INTEGER, `connecting_time_audio_policy` INTEGER, `in_band_ringtone_audio_policy` INTEGER, PRIMARY KEY(`address`))",
+        "fields": [
+          {
+            "fieldPath": "address",
+            "columnName": "address",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "migrated",
+            "columnName": "migrated",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "a2dpSupportsOptionalCodecs",
+            "columnName": "a2dpSupportsOptionalCodecs",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "a2dpOptionalCodecsEnabled",
+            "columnName": "a2dpOptionalCodecsEnabled",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "last_active_time",
+            "columnName": "last_active_time",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "is_active_a2dp_device",
+            "columnName": "is_active_a2dp_device",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "preferred_output_only_profile",
+            "columnName": "preferred_output_only_profile",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "preferred_duplex_profile",
+            "columnName": "preferred_duplex_profile",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.a2dp_connection_policy",
+            "columnName": "a2dp_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.a2dp_sink_connection_policy",
+            "columnName": "a2dp_sink_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.hfp_connection_policy",
+            "columnName": "hfp_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.hfp_client_connection_policy",
+            "columnName": "hfp_client_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.hid_host_connection_policy",
+            "columnName": "hid_host_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.pan_connection_policy",
+            "columnName": "pan_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.pbap_connection_policy",
+            "columnName": "pbap_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.pbap_client_connection_policy",
+            "columnName": "pbap_client_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.map_connection_policy",
+            "columnName": "map_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.sap_connection_policy",
+            "columnName": "sap_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.hearing_aid_connection_policy",
+            "columnName": "hearing_aid_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.hap_client_connection_policy",
+            "columnName": "hap_client_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.map_client_connection_policy",
+            "columnName": "map_client_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.le_audio_connection_policy",
+            "columnName": "le_audio_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.volume_control_connection_policy",
+            "columnName": "volume_control_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.csip_set_coordinator_connection_policy",
+            "columnName": "csip_set_coordinator_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.le_call_control_connection_policy",
+            "columnName": "le_call_control_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.bass_client_connection_policy",
+            "columnName": "bass_client_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.battery_connection_policy",
+            "columnName": "battery_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.manufacturer_name",
+            "columnName": "manufacturer_name",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.model_name",
+            "columnName": "model_name",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.software_version",
+            "columnName": "software_version",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.hardware_version",
+            "columnName": "hardware_version",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.companion_app",
+            "columnName": "companion_app",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.main_icon",
+            "columnName": "main_icon",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.is_untethered_headset",
+            "columnName": "is_untethered_headset",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_left_icon",
+            "columnName": "untethered_left_icon",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_right_icon",
+            "columnName": "untethered_right_icon",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_case_icon",
+            "columnName": "untethered_case_icon",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_left_battery",
+            "columnName": "untethered_left_battery",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_right_battery",
+            "columnName": "untethered_right_battery",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_case_battery",
+            "columnName": "untethered_case_battery",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_left_charging",
+            "columnName": "untethered_left_charging",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_right_charging",
+            "columnName": "untethered_right_charging",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_case_charging",
+            "columnName": "untethered_case_charging",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.enhanced_settings_ui_uri",
+            "columnName": "enhanced_settings_ui_uri",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.device_type",
+            "columnName": "device_type",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.main_battery",
+            "columnName": "main_battery",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.main_charging",
+            "columnName": "main_charging",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.main_low_battery_threshold",
+            "columnName": "main_low_battery_threshold",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_left_low_battery_threshold",
+            "columnName": "untethered_left_low_battery_threshold",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_right_low_battery_threshold",
+            "columnName": "untethered_right_low_battery_threshold",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_case_low_battery_threshold",
+            "columnName": "untethered_case_low_battery_threshold",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.spatial_audio",
+            "columnName": "spatial_audio",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.fastpair_customized",
+            "columnName": "fastpair_customized",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.le_audio",
+            "columnName": "le_audio",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.gmcs_cccd",
+            "columnName": "gmcs_cccd",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.gtbs_cccd",
+            "columnName": "gtbs_cccd",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "audioPolicyMetadata.callEstablishAudioPolicy",
+            "columnName": "call_establish_audio_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "audioPolicyMetadata.connectingTimeAudioPolicy",
+            "columnName": "connecting_time_audio_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "audioPolicyMetadata.inBandRingtoneAudioPolicy",
+            "columnName": "in_band_ringtone_audio_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "address"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": []
+      }
+    ],
+    "views": [],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b3363a857e6d4f3ece8ba92d57d52c26')"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java
index 141ee85..8cdc684 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java
@@ -40,6 +40,7 @@
 import android.bluetooth.le.ScanSettings;
 import android.content.AttributionSource;
 import android.content.Context;
+import android.content.res.Resources;
 import android.os.Binder;
 import android.os.ParcelUuid;
 import android.os.RemoteException;
@@ -53,6 +54,7 @@
 import com.android.bluetooth.R;
 import com.android.bluetooth.TestUtils;
 import com.android.bluetooth.btservice.AdapterService;
+import com.android.bluetooth.btservice.CompanionManager;
 
 import org.junit.After;
 import org.junit.Assert;
@@ -99,7 +101,9 @@
     private BluetoothAdapter mAdapter;
     private AttributionSource mAttributionSource;
 
+    @Mock private Resources mResources;
     @Mock private AdapterService mAdapterService;
+    private CompanionManager mBtCompanionManager;
 
     @Before
     public void setUp() throws Exception {
@@ -113,6 +117,15 @@
         mAttributionSource = mAdapter.getAttributionSource();
         mDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(REMOTE_DEVICE_ADDRESS);
 
+        when(mAdapterService.getResources()).thenReturn(mResources);
+        when(mResources.getInteger(anyInt())).thenReturn(0);
+        when(mAdapterService.getSharedPreferences(anyString(), anyInt()))
+                .thenReturn(InstrumentationRegistry.getTargetContext()
+                        .getSharedPreferences("GattServiceTestPrefs", Context.MODE_PRIVATE));
+
+        mBtCompanionManager = new CompanionManager(mAdapterService, null);
+        doReturn(mBtCompanionManager).when(mAdapterService).getCompanionManager();
+
         TestUtils.startService(mServiceRule, GattService.class);
         mService = GattService.getGattService();
         Assert.assertNotNull(mService);
diff --git a/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceAndStateMachineTest.java b/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceAndStateMachineTest.java
index c89e135..fb27a2a 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceAndStateMachineTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceAndStateMachineTest.java
@@ -26,6 +26,7 @@
 import android.app.Activity;
 import android.app.Instrumentation;
 import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothAudioPolicy;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
 import android.bluetooth.BluetoothProfile;
@@ -181,6 +182,8 @@
                 .getBondState(any(BluetoothDevice.class));
         doAnswer(invocation -> mBondedDevices.toArray(new BluetoothDevice[]{})).when(
                 mAdapterService).getBondedDevices();
+        doReturn(new BluetoothAudioPolicy.Builder().build()).when(mAdapterService)
+                .getAudioPolicy(any(BluetoothDevice.class));
         // Mock system interface
         doNothing().when(mSystemInterface).stop();
         when(mSystemInterface.getHeadsetPhoneState()).thenReturn(mPhoneState);
diff --git a/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java
index e071563..817ea8b 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java
@@ -19,6 +19,7 @@
 import static org.mockito.Mockito.*;
 
 import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothAudioPolicy;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
 import android.bluetooth.BluetoothProfile;
@@ -116,6 +117,8 @@
             Set<BluetoothDevice> keys = mStateMachines.keySet();
             return keys.toArray(new BluetoothDevice[keys.size()]);
         }).when(mAdapterService).getBondedDevices();
+        doReturn(new BluetoothAudioPolicy.Builder().build()).when(mAdapterService)
+                .getAudioPolicy(any(BluetoothDevice.class));
         // Mock system interface
         doNothing().when(mSystemInterface).stop();
         when(mSystemInterface.getHeadsetPhoneState()).thenReturn(mPhoneState);
@@ -991,6 +994,42 @@
         mHeadsetService.dump(sb);
     }
 
+    @Test
+    public void testConnectDeviceNotAllowedInbandRingPolicy_InbandRingStatus() {
+        when(mDatabaseManager.getProfileConnectionPolicy(any(BluetoothDevice.class),
+                eq(BluetoothProfile.HEADSET)))
+                .thenReturn(BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
+        mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0);
+        Assert.assertTrue(mHeadsetService.connect(mCurrentDevice));
+        when(mStateMachines.get(mCurrentDevice).getDevice()).thenReturn(mCurrentDevice);
+        when(mStateMachines.get(mCurrentDevice).getConnectionState()).thenReturn(
+                BluetoothProfile.STATE_CONNECTED);
+        when(mStateMachines.get(mCurrentDevice).getConnectingTimestampMs()).thenReturn(
+                SystemClock.uptimeMillis());
+        Assert.assertEquals(Collections.singletonList(mCurrentDevice),
+                mHeadsetService.getConnectedDevices());
+        mHeadsetService.onConnectionStateChangedFromStateMachine(mCurrentDevice,
+                BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTED);
+
+        when(mStateMachines.get(mCurrentDevice).getHfpCallAudioPolicy()).thenReturn(
+                new BluetoothAudioPolicy.Builder()
+                        .setCallEstablishPolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+                        .setConnectingTimePolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+                        .setInBandRingtonePolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+                        .build()
+        );
+        Assert.assertEquals(true, mHeadsetService.isInbandRingingEnabled());
+
+        when(mStateMachines.get(mCurrentDevice).getHfpCallAudioPolicy()).thenReturn(
+                new BluetoothAudioPolicy.Builder()
+                        .setCallEstablishPolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+                        .setConnectingTimePolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+                        .setInBandRingtonePolicy(BluetoothAudioPolicy.POLICY_NOT_ALLOWED)
+                        .build()
+        );
+        Assert.assertEquals(false, mHeadsetService.isInbandRingingEnabled());
+    }
+
     private void addConnectedDeviceHelper(BluetoothDevice device) {
         mCurrentDevice = device;
         when(mDatabaseManager.getProfileConnectionPolicy(any(BluetoothDevice.class),
diff --git a/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java b/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java
index a3e58d9..025fe05 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java
@@ -51,6 +51,7 @@
 import com.android.bluetooth.R;
 import com.android.bluetooth.TestUtils;
 import com.android.bluetooth.btservice.AdapterService;
+import com.android.bluetooth.btservice.storage.DatabaseManager;
 
 import org.hamcrest.core.IsInstanceOf;
 import org.junit.After;
@@ -86,6 +87,7 @@
     private ArgumentCaptor<Intent> mIntentArgument = ArgumentCaptor.forClass(Intent.class);
 
     @Mock private AdapterService mAdapterService;
+    @Mock private DatabaseManager mDatabaseManager;
     @Mock private HeadsetService mHeadsetService;
     @Mock private HeadsetSystemInterface mSystemInterface;
     @Mock private AudioManager mAudioManager;
@@ -109,6 +111,9 @@
         mAdapter = BluetoothAdapter.getDefaultAdapter();
         // Get a device for testing
         mTestDevice = mAdapter.getRemoteDevice("00:01:02:03:04:05");
+        // Get a database
+        doReturn(mDatabaseManager).when(mAdapterService).getDatabase();
+        doReturn(true).when(mDatabaseManager).setAudioPolicyMetadata(anyObject(), anyObject());
         // Spy on native interface
         mNativeInterface = spy(HeadsetNativeInterface.getInstance());
         doNothing().when(mNativeInterface).init(anyInt(), anyBoolean());
@@ -1437,6 +1442,22 @@
     }
 
     /**
+     * A test to validate received Android AT commands and processing
+     */
+    @Test
+    public void testProcessAndroidAt() {
+        setUpConnectedState();
+        // setup Audio Policy Feature
+        setUpAudioPolicy();
+        // receive and set android policy
+        mHeadsetStateMachine.sendMessage(HeadsetStateMachine.STACK_EVENT,
+                new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_UNKNOWN_AT,
+                        "+ANDROID=1,1,1,1", mTestDevice));
+        verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS))
+                .setAudioPolicyMetadata(anyObject(), anyObject());
+    }
+
+    /**
      * Setup Connecting State
      * @return number of times mHeadsetService.sendBroadcastAsUser() has been invoked
      */
@@ -1550,4 +1571,12 @@
                 IsInstanceOf.instanceOf(HeadsetStateMachine.Disconnecting.class));
         return numBroadcastsSent;
     }
+
+    private void setUpAudioPolicy() {
+        mHeadsetStateMachine.sendMessage(HeadsetStateMachine.STACK_EVENT,
+                new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_UNKNOWN_AT,
+                        "+ANDROID=?", mTestDevice));
+        verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).atResponseString(
+                anyObject(), anyString());
+    }
 }
diff --git a/android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientServiceTest.java
index e85dc17..bc335a5 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientServiceTest.java
@@ -16,6 +16,7 @@
 
 package com.android.bluetooth.hfpclient;
 
+import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyString;
 import static org.mockito.Mockito.doReturn;
@@ -25,6 +26,8 @@
 
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadset;
+import android.bluetooth.BluetoothAudioPolicy;
 import android.bluetooth.BluetoothManager;
 import android.content.Context;
 import android.content.Intent;
@@ -148,4 +151,17 @@
 
         mService.dump(new StringBuilder());
     }
+
+    @Test
+    public void testSetCallAudioPolicy() {
+        // Put mock state machine
+        BluetoothDevice device =
+                BluetoothAdapter.getDefaultAdapter().getRemoteDevice("00:01:02:03:04:05");
+        mService.getStateMachineMap().put(device, mStateMachine);
+
+        mService.setAudioPolicy(device, new BluetoothAudioPolicy.Builder().build());
+
+        verify(mStateMachine, timeout(STANDARD_WAIT_MILLIS).times(1))
+                .setAudioPolicy(any(BluetoothAudioPolicy.class));
+    }
 }
diff --git a/android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java b/android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java
index 0aa98f2..f64cda7 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java
@@ -13,6 +13,7 @@
 import android.bluetooth.BluetoothAssignedNumbers;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadsetClient;
+import android.bluetooth.BluetoothAudioPolicy;
 import android.bluetooth.BluetoothProfile;
 import android.content.Context;
 import android.content.Intent;
@@ -97,6 +98,7 @@
 
         TestUtils.setAdapterService(mAdapterService);
         mNativeInterface = spy(NativeInterface.getInstance());
+        doReturn(true).when(mNativeInterface).sendAndroidAt(anyObject(), anyString());
 
         // This line must be called to make sure relevant objects are initialized properly
         mAdapter = BluetoothAdapter.getDefaultAdapter();
@@ -201,6 +203,9 @@
         slcEvent.valueInt2 = HeadsetClientHalConstants.PEER_FEAT_ECS;
         slcEvent.device = mTestDevice;
         mHeadsetClientStateMachine.sendMessage(StackEvent.STACK_EVENT, slcEvent);
+        TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());
+
+        setUpAndroidAt(false);
 
         // Verify that one connection state broadcast is executed
         ArgumentCaptor<Intent> intentArgument2 = ArgumentCaptor.forClass(Intent.class);
@@ -293,6 +298,9 @@
         slcEvent.valueInt2 = HeadsetClientHalConstants.PEER_FEAT_ECS;
         slcEvent.device = mTestDevice;
         mHeadsetClientStateMachine.sendMessage(StackEvent.STACK_EVENT, slcEvent);
+        TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());
+
+        setUpAndroidAt(false);
 
         verify(mHeadsetClientService,
                 timeout(STANDARD_WAIT_MILLIS).times(expectedBroadcastMultiplePermissionsIndex++))
@@ -396,6 +404,10 @@
 
     /* Utility function to simulate SLC connection. */
     private int setUpServiceLevelConnection(int startBroadcastIndex) {
+        return setUpServiceLevelConnection(startBroadcastIndex, false);
+    }
+
+    private int setUpServiceLevelConnection(int startBroadcastIndex, boolean androidAtSupported) {
         // Trigger SLC connection
         StackEvent slcEvent = new StackEvent(StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
         slcEvent.valueInt = HeadsetClientHalConstants.CONNECTION_STATE_SLC_CONNECTED;
@@ -403,16 +415,46 @@
         slcEvent.valueInt2 |= HeadsetClientHalConstants.PEER_FEAT_HF_IND;
         slcEvent.device = mTestDevice;
         mHeadsetClientStateMachine.sendMessage(StackEvent.STACK_EVENT, slcEvent);
+        TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());
+
+        setUpAndroidAt(androidAtSupported);
+
         ArgumentCaptor<Intent> intentArgument = ArgumentCaptor.forClass(Intent.class);
         verify(mHeadsetClientService, timeout(STANDARD_WAIT_MILLIS).times(startBroadcastIndex))
                 .sendBroadcastMultiplePermissions(intentArgument.capture(),
                                                   any(String[].class), any(BroadcastOptions.class));
         Assert.assertEquals(BluetoothProfile.STATE_CONNECTED,
                 intentArgument.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1));
+        Assert.assertThat(mHeadsetClientStateMachine.getCurrentState(),
+                IsInstanceOf.instanceOf(HeadsetClientStateMachine.Connected.class));
+
         startBroadcastIndex++;
         return startBroadcastIndex;
     }
 
+    /**
+     * Set up and verify AT Android related commands and events.
+     * Make sure this method is invoked after SLC is setup.
+     */
+    private void setUpAndroidAt(boolean androidAtSupported) {
+        verify(mNativeInterface).sendAndroidAt(mTestDevice, "+ANDROID=?");
+        if (androidAtSupported) {
+            StackEvent unknownEvt = new StackEvent(StackEvent.EVENT_TYPE_UNKNOWN_EVENT);
+            unknownEvt.valueString = "+ANDROID: 1";
+            unknownEvt.device = mTestDevice;
+            mHeadsetClientStateMachine.sendMessage(StackEvent.STACK_EVENT, unknownEvt);
+            TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());
+            verify(mHeadsetClientService).setAudioPolicyRemoteSupported(mTestDevice, true);
+            mHeadsetClientStateMachine.setAudioPolicyRemoteSupported(true);
+        } else {
+            // receive CMD_RESULT CME_ERROR due to remote not supporting Android AT
+            StackEvent cmdResEvt = new StackEvent(StackEvent.EVENT_TYPE_CMD_RESULT);
+            cmdResEvt.valueInt = StackEvent.CMD_RESULT_TYPE_CME_ERROR;
+            cmdResEvt.device = mTestDevice;
+            mHeadsetClientStateMachine.sendMessage(StackEvent.STACK_EVENT, cmdResEvt);
+        }
+    }
+
     /* Utility function: supported AT command should lead to native call */
     private void runSupportedVendorAtCommand(String atCommand, int vendorId) {
         // Return true for priority.
@@ -991,4 +1033,44 @@
         Assert.assertEquals(HeadsetClientStateMachine.getMessageName(unknownMessageInt),
                 "UNKNOWN(" + unknownMessageInt + ")");
     }
+    /**
+     * Tests and verify behavior of the case where remote device doesn't support
+     * At Android but tries to send audio policy.
+     */
+    @Test
+    public void testAndroidAtRemoteNotSupported_StateTransition_setAudioPolicy() {
+        // Setup connection state machine to be in connected state
+        when(mHeadsetClientService.getConnectionPolicy(any(BluetoothDevice.class))).thenReturn(
+                BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+        int expectedBroadcastIndex = 1;
+
+        expectedBroadcastIndex = setUpHfpClientConnection(expectedBroadcastIndex);
+        expectedBroadcastIndex = setUpServiceLevelConnection(expectedBroadcastIndex);
+
+        BluetoothAudioPolicy dummyAudioPolicy = new BluetoothAudioPolicy.Builder().build();
+        mHeadsetClientStateMachine.setAudioPolicy(dummyAudioPolicy);
+        verify(mNativeInterface, never()).sendAndroidAt(mTestDevice, "+ANDROID:1,0,0,0");
+    }
+
+    @SmallTest
+    @Test
+    public void testSetGetCallAudioPolicy() {
+        // Return true for priority.
+        when(mHeadsetClientService.getConnectionPolicy(any(BluetoothDevice.class))).thenReturn(
+                BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+
+        int expectedBroadcastIndex = 1;
+
+        expectedBroadcastIndex = setUpHfpClientConnection(expectedBroadcastIndex);
+        expectedBroadcastIndex = setUpServiceLevelConnection(expectedBroadcastIndex, true);
+
+        BluetoothAudioPolicy dummyAudioPolicy = new BluetoothAudioPolicy.Builder()
+                .setCallEstablishPolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+                .setConnectingTimePolicy(BluetoothAudioPolicy.POLICY_NOT_ALLOWED)
+                .setInBandRingtonePolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+                .build();
+
+        mHeadsetClientStateMachine.setAudioPolicy(dummyAudioPolicy);
+        verify(mNativeInterface).sendAndroidAt(mTestDevice, "+ANDROID=1,1,2,1");
+    }
 }
diff --git a/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapSettingsTest.java b/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapSettingsTest.java
index c1d7bc8..df751e3 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapSettingsTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapSettingsTest.java
@@ -37,6 +37,7 @@
 import com.android.bluetooth.R;
 
 import org.junit.After;
+import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -52,11 +53,11 @@
 
     @Before
     public void setUp() {
+        Assume.assumeTrue("Ignore test when BluetoothMapService is not enabled",
+                BluetoothMapService.isEnabled());
         enableActivity(true);
-
         mIntent = new Intent();
         mIntent.setClass(mTargetContext, BluetoothMapSettings.class);
-
         mActivityScenario = ActivityScenario.launch(mIntent);
     }
 
@@ -67,7 +68,6 @@
             Thread.sleep(1_000);
             mActivityScenario.close();
         }
-
         enableActivity(false);
     }
 
diff --git a/android/app/tests/unit/src/com/android/bluetooth/mapclient/MapClientServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/mapclient/MapClientServiceTest.java
index 07e1266..93365e5 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/mapclient/MapClientServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/mapclient/MapClientServiceTest.java
@@ -301,7 +301,7 @@
         MceStateMachine sm = mock(MceStateMachine.class);
         mService.getInstanceMap().put(mRemoteDevice, sm);
 
-        Intent intent = new Intent(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
+        Intent intent = new Intent(BluetoothDevice.ACTION_SDP_RECORD);
         intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mRemoteDevice);
         intent.putExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_DISCONNECTED);
         intent.putExtra(BluetoothDevice.EXTRA_UUID, BluetoothUuid.MAS);
diff --git a/android/app/tests/unit/src/com/android/bluetooth/mapclient/MapClientTest.java b/android/app/tests/unit/src/com/android/bluetooth/mapclient/MapClientTest.java
index 3249a42..7c80bff 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/mapclient/MapClientTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/mapclient/MapClientTest.java
@@ -24,7 +24,6 @@
 import android.bluetooth.IBluetoothMapClient;
 import android.content.Context;
 import android.os.UserHandle;
-import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.MediumTest;
@@ -123,7 +122,6 @@
 
         // is the statemachine created
         Map<BluetoothDevice, MceStateMachine> map = mService.getInstanceMap();
-        Log.d("MapClientTest", "map=" + map);
 
         Assert.assertEquals(1, map.size());
         Assert.assertNotNull(map.get(device));
diff --git a/android/app/tests/unit/src/com/android/bluetooth/mcp/MediaControlGattServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/mcp/MediaControlGattServiceTest.java
index ff7406a..d840126 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/mcp/MediaControlGattServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/mcp/MediaControlGattServiceTest.java
@@ -90,6 +90,7 @@
         mAdapter = BluetoothAdapter.getDefaultAdapter();
 
         doReturn(true).when(mMockGattServer).addService(any(BluetoothGattService.class));
+        doReturn(new BluetoothDevice[0]).when(mAdapterService).getBondedDevices();
 
         mMcpService = new MediaControlGattService(mMockMcpService, mMockMcsCallbacks, TEST_CCID);
         mMcpService.setBluetoothGattServerForTesting(mMockGattServer);
@@ -122,7 +123,7 @@
         List<BluetoothDevice> devices = new ArrayList<BluetoothDevice>();
         devices.add(mCurrentDevice);
         doReturn(devices).when(mMockGattServer).getConnectedDevices();
-        mMcpService.setCcc(mCurrentDevice, characteristic.getUuid(), 0, value);
+        mMcpService.setCcc(mCurrentDevice, characteristic.getUuid(), 0, value, true);
     }
 
     @Test
diff --git a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapUtilsTest.java b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapUtilsTest.java
index 6ea294f..875042f 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapUtilsTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapUtilsTest.java
@@ -48,7 +48,6 @@
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -78,13 +77,13 @@
         BluetoothMethodProxy.setInstanceForTesting(mProxy);
 
         when(mContext.getResources()).thenReturn(mResources);
-        initStaticFields();
+        clearStaticFields();
     }
 
     @After
     public void tearDown() {
         BluetoothMethodProxy.setInstanceForTesting(null);
-        initStaticFields();
+        clearStaticFields();
     }
 
     @Test
@@ -310,15 +309,11 @@
         }
     }
 
-    @Ignore("b/262486295")
     @Test
     public void updateSecondaryVersionCounter_whenContactsAreUpdated() {
         MatrixCursor contactCursor = new MatrixCursor(
                 new String[] {Contacts._ID, Contacts.CONTACT_LAST_UPDATED_TIMESTAMP});
         contactCursor.addRow(new Object[] {"id1", Calendar.getInstance().getTimeInMillis()});
-        contactCursor.addRow(new Object[] {"id2", Calendar.getInstance().getTimeInMillis()});
-        contactCursor.addRow(new Object[] {"id3", Calendar.getInstance().getTimeInMillis()});
-        contactCursor.addRow(new Object[] {"id4", Calendar.getInstance().getTimeInMillis()});
         doReturn(contactCursor).when(mProxy).contentResolverQuery(
                 any(), eq(Contacts.CONTENT_URI), any(), any(), any(), any());
 
@@ -326,31 +321,28 @@
         dataCursor.addRow(new Object[] {"id1", Phone.CONTENT_ITEM_TYPE, "01234567"});
         dataCursor.addRow(new Object[] {"id1", Email.CONTENT_ITEM_TYPE, "android@android.com"});
         dataCursor.addRow(new Object[] {"id1", StructuredPostal.CONTENT_ITEM_TYPE, "01234"});
-        dataCursor.addRow(new Object[] {"id2", StructuredName.CONTENT_ITEM_TYPE, "And Roid"});
+        dataCursor.addRow(new Object[] {"id1", StructuredName.CONTENT_ITEM_TYPE, "And Roid"});
         doReturn(dataCursor).when(mProxy).contentResolverQuery(
                 any(), eq(Data.CONTENT_URI), any(), any(), any(), any());
+        assertThat(BluetoothPbapUtils.sSecondaryVersionCounter).isEqualTo(0);
 
-        HandlerThread handlerThread = new HandlerThread("BluetoothPbapUtilsTest");
-        handlerThread.start();
-        Handler handler = new Handler(handlerThread.getLooper());
+        BluetoothPbapUtils.sTotalContacts = 1;
+        BluetoothPbapUtils.setContactFields(BluetoothPbapUtils.TYPE_NAME, "id1",
+                "test_previous_name_before_update");
 
-        try {
-            BluetoothPbapUtils.sTotalContacts = 4;
+        BluetoothPbapUtils.updateSecondaryVersionCounter(mContext, null);
 
-            BluetoothPbapUtils.updateSecondaryVersionCounter(mContext, handler);
-
-            assertThat(BluetoothPbapUtils.sContactDataset).isNotEmpty();
-        } finally {
-            handlerThread.quit();
-        }
+        assertThat(BluetoothPbapUtils.sSecondaryVersionCounter).isEqualTo(1);
     }
 
-    private static void initStaticFields() {
+    private static void clearStaticFields() {
         BluetoothPbapUtils.sPrimaryVersionCounter = 0;
         BluetoothPbapUtils.sSecondaryVersionCounter = 0;
         BluetoothPbapUtils.sContactSet.clear();
+        BluetoothPbapUtils.sContactDataset.clear();
         BluetoothPbapUtils.sTotalContacts = 0;
         BluetoothPbapUtils.sTotalFields = 0;
         BluetoothPbapUtils.sTotalSvcFields = 0;
+        BluetoothPbapUtils.sContactsLastUpdated = 0;
     }
 }
diff --git a/framework/java/android/bluetooth/BluetoothAudioPolicy.java b/framework/java/android/bluetooth/BluetoothAudioPolicy.java
new file mode 100644
index 0000000..873abcd
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothAudioPolicy.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Represents Bluetooth Audio Policies of a Handsfree (HF) device (if HFP is used)
+ * and Call Terminal (CT) device (if BLE Audio is used), which describes the
+ * preferences of allowing or disallowing audio based on the use cases. The HF/CT
+ * devices shall send objects of this class to send its preference to the AG/CG
+ * devices.
+ *
+ * <p>HF/CT side applications on can use {@link BluetoothDevice#setAudioPolicy}
+ * API to set and send a {@link BluetoothAudioPolicy} object containing the
+ * preference/policy values. This object will be stored in the memory of HF/CT
+ * side, will be send to the AG/CG side using Android Specific AT Commands and will
+ * be stored in the AG side memory and database.
+ *
+ * <p>HF/CT side API {@link BluetoothDevice#getAudioPolicy} can be used to retrieve
+ * the stored audio policies currently.
+ *
+ * <p>Note that the setter APIs of this class will only set the values of the
+ * object. To actually set the policies, API {@link BluetoothDevice#setAudioPolicy}
+ * must need to be invoked with the {@link BluetoothAudioPolicy} object.
+ *
+ * <p>Note that any API related to this feature should be used after configuring
+ * the support of the AG device and after checking whether the AG device supports
+ * this feature or not by invoking {@link BluetoothDevice#getAudioPolicyRemoteSupported}.
+ * Only after getting a {@link BluetoothAudioPolicy#FEATURE_SUPPORTED_BY_REMOTE} response
+ * from the API should the APIs related to this feature be used.
+ *
+ *
+ * @hide
+ */
+public final class BluetoothAudioPolicy implements Parcelable {
+
+    /**
+     * @hide
+    */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+        prefix = {"POLICY_"},
+        value = {
+            POLICY_UNCONFIGURED,
+            POLICY_ALLOWED,
+            POLICY_NOT_ALLOWED,
+        }
+    )
+    public @interface AudioPolicyValues{}
+
+    /**
+     * Audio behavior not configured for the policy.
+     *
+     * If a policy is set with this value, it means that the policy is not
+     * configured with a value yet and should not be used to make any decision.
+     * @hide
+     */
+    public static final int POLICY_UNCONFIGURED = 0;
+
+    /**
+     * Audio is preferred by HF device for the policy.
+     *
+     * If a policy is set with this value, then the HF device will prefer the
+     * audio for the policy use case. For example, if the Call Establish audio
+     * policy is set with this value, then the HF will prefer the audio
+     * during making or picking up a call.
+     * @hide
+     */
+    public static final int POLICY_ALLOWED = 1;
+
+    /**
+     * Audio is not preferred by HF device for the policy.
+     *
+     * If a policy is set with this value, then the HF device will not prefer the
+     * audio for the policy use case. For example, if the Call Establish audio
+     * policy is set with this value, then the HF will not prefer the audio
+     * during making or picking up a call.
+     * @hide
+     */
+    public static final int POLICY_NOT_ALLOWED = 2;
+
+    /**
+     * Remote support status of audio policy feature is unknown/unconfigured.
+     *
+     * @hide
+     */
+    public static final int FEATURE_UNCONFIGURED_BY_REMOTE = 0;
+
+    /**
+     * Remote support status of audio policy feature is supported.
+     *
+     * @hide
+     */
+    public static final int FEATURE_SUPPORTED_BY_REMOTE = 1;
+
+    /**
+     * Remote support status of audio policy feature is not supported.
+     *
+     * @hide
+     */
+    public static final int FEATURE_NOT_SUPPORTED_BY_REMOTE = 2;
+
+
+    @AudioPolicyValues private final int mCallEstablishPolicy;
+    @AudioPolicyValues private final int mConnectingTimePolicy;
+    @AudioPolicyValues private final int mInBandRingtonePolicy;
+
+    /**
+     * @hide
+     */
+    public BluetoothAudioPolicy(int callEstablishPolicy,
+            int connectingTimePolicy, int inBandRingtonePolicy) {
+        mCallEstablishPolicy = callEstablishPolicy;
+        mConnectingTimePolicy = connectingTimePolicy;
+        mInBandRingtonePolicy = inBandRingtonePolicy;
+    }
+
+    /**
+     * Get Call pick up audio policy.
+     *
+     * @return the call pick up audio policy value
+     *
+     */
+    public @AudioPolicyValues int getCallEstablishPolicy() {
+        return mCallEstablishPolicy;
+    }
+
+    /**
+     * Get during connection audio up policy.
+     *
+     * @return the during connection audio policy value
+     *
+     */
+    public @AudioPolicyValues int getConnectingTimePolicy() {
+        return mConnectingTimePolicy;
+    }
+
+    /**
+     * Get In band ringtone audio up policy.
+     *
+     * @return the in band ringtone audio policy value
+     *
+     */
+    public @AudioPolicyValues int getInBandRingtonePolicy() {
+        return mInBandRingtonePolicy;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder("BluetoothAudioPolicy{");
+        builder.append("mCallEstablishPolicy: ");
+        builder.append(mCallEstablishPolicy);
+        builder.append(", mConnectingTimePolicy: ");
+        builder.append(mConnectingTimePolicy);
+        builder.append(", mInBandRingtonePolicy: ");
+        builder.append(mInBandRingtonePolicy);
+        builder.append("}");
+        return builder.toString();
+    }
+
+    /**
+     * {@link Parcelable.Creator} interface implementation.
+     */
+    public static final @android.annotation.NonNull Parcelable.Creator<BluetoothAudioPolicy>
+            CREATOR = new Parcelable.Creator<BluetoothAudioPolicy>() {
+                @Override
+                public BluetoothAudioPolicy createFromParcel(@NonNull Parcel in) {
+                    return new BluetoothAudioPolicy(
+                            in.readInt(), in.readInt(), in.readInt());
+                }
+
+                @Override
+                public BluetoothAudioPolicy[] newArray(int size) {
+                    return new BluetoothAudioPolicy[size];
+                }
+            };
+
+    @Override
+    public void writeToParcel(@NonNull Parcel out, int flags) {
+        out.writeInt(mCallEstablishPolicy);
+        out.writeInt(mConnectingTimePolicy);
+        out.writeInt(mInBandRingtonePolicy);
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+        if (o instanceof BluetoothAudioPolicy) {
+            BluetoothAudioPolicy other = (BluetoothAudioPolicy) o;
+            return (other.mCallEstablishPolicy == mCallEstablishPolicy
+                    && other.mConnectingTimePolicy == mConnectingTimePolicy
+                    && other.mInBandRingtonePolicy == mInBandRingtonePolicy);
+        }
+        return false;
+    }
+
+    /**
+     * Returns a hash representation of this BluetoothCodecConfig
+     * based on all the config values.
+     *
+     * @hide
+     */
+    @Override
+    public int hashCode() {
+        return Objects.hash(mCallEstablishPolicy, mConnectingTimePolicy, mInBandRingtonePolicy);
+    }
+
+    /**
+     * Builder for {@link BluetoothAudioPolicy}.
+     * <p> By default, the audio policy values will be set to
+     * {@link BluetoothAudioPolicy#POLICY_UNCONFIGURED}.
+     */
+    public static final class Builder {
+        private int mCallEstablishPolicy = POLICY_UNCONFIGURED;
+        private int mConnectingTimePolicy = POLICY_UNCONFIGURED;
+        private int mInBandRingtonePolicy = POLICY_UNCONFIGURED;
+
+        public Builder() {
+
+        }
+
+        public Builder(@NonNull BluetoothAudioPolicy policies) {
+            mCallEstablishPolicy = policies.mCallEstablishPolicy;
+            mConnectingTimePolicy = policies.mConnectingTimePolicy;
+            mInBandRingtonePolicy = policies.mInBandRingtonePolicy;
+        }
+
+        /**
+         * Set Call Establish (pick up and answer) policy.
+         * <p>This policy is used to determine the audio preference when the
+         * HF device makes or answers a call. That is, if this device
+         * makes or answers a call, is the audio preferred by HF should be decided
+         * by this policy.
+         *
+         * @return reference to the current object
+         */
+        public @NonNull Builder setCallEstablishPolicy(
+                @AudioPolicyValues int callEstablishPolicy) {
+            mCallEstablishPolicy = callEstablishPolicy;
+            return this;
+        }
+
+        /**
+         * Set during connection audio up policy.
+         * <p>This policy is used to determine the audio preference when the
+         * HF device connects with the AG device. That is, when the
+         * HF device gets connected, should the HF become active and get audio
+         * is decided by this policy. This also covers the case of during a call.
+         * If the HF connects with the AG during an ongoing call, should the call
+         * audio be routed to the HF will be decided by this policy.
+         *
+         * @return reference to the current object
+         */
+        public @NonNull Builder setConnectingTimePolicy(
+                @AudioPolicyValues int connectingTimePolicy) {
+            mConnectingTimePolicy = connectingTimePolicy;
+            return this;
+        }
+
+        /**
+         * Set In band ringtone audio up policy.
+         * <p>This policy is used to determine the audio preference of the
+         * in band ringtone. That is, if there is an incoming call, should the
+         * inband ringtone audio be routed to the HF will be decided by this policy.
+         *
+         * @return reference to the current object
+         *
+         */
+        public @NonNull Builder setInBandRingtonePolicy(
+                @AudioPolicyValues int inBandRingtonePolicy) {
+            mInBandRingtonePolicy = inBandRingtonePolicy;
+            return this;
+        }
+
+        /**
+         * Build {@link BluetoothAudioPolicy}.
+         * @return new BluetoothAudioPolicy object
+         */
+        public @NonNull BluetoothAudioPolicy build() {
+            return new BluetoothAudioPolicy(
+                    mCallEstablishPolicy, mConnectingTimePolicy, mInBandRingtonePolicy);
+        }
+    }
+}
diff --git a/framework/java/android/bluetooth/BluetoothDevice.java b/framework/java/android/bluetooth/BluetoothDevice.java
index f7ab590..5646efb 100644
--- a/framework/java/android/bluetooth/BluetoothDevice.java
+++ b/framework/java/android/bluetooth/BluetoothDevice.java
@@ -510,7 +510,9 @@
             METADATA_UNTETHERED_CASE_LOW_BATTERY_THRESHOLD,
             METADATA_SPATIAL_AUDIO,
             METADATA_FAST_PAIR_CUSTOMIZED_FIELDS,
-            METADATA_LE_AUDIO})
+            METADATA_LE_AUDIO,
+            METADATA_GMCS_CCCD,
+            METADATA_GTBS_CCCD})
     @Retention(RetentionPolicy.SOURCE)
     public @interface MetadataKey{}
 
@@ -663,6 +665,21 @@
     public static final int METADATA_ENHANCED_SETTINGS_UI_URI = 16;
 
     /**
+     * @hide
+     */
+    public static final String COMPANION_TYPE_PRIMARY = "COMPANION_PRIMARY";
+
+    /**
+     * @hide
+     */
+    public static final String COMPANION_TYPE_SECONDARY = "COMPANION_SECONDARY";
+
+    /**
+     * @hide
+     */
+    public static final String COMPANION_TYPE_NONE = "COMPANION_NONE";
+
+    /**
      * Type of the Bluetooth device, must be within the list of
      * BluetoothDevice.DEVICE_TYPE_*
      * Data type should be {@String} as {@link Byte} array.
@@ -742,6 +759,21 @@
      */
     public static final int METADATA_LE_AUDIO = 26;
 
+    /**
+     * The UUIDs (16-bit) of registered to CCC characteristics from Media Control services.
+     * Data type should be {@link Byte} array.
+     * @hide
+     */
+    public static final int METADATA_GMCS_CCCD = 27;
+
+    /**
+     * The UUIDs (16-bit) of registered to CCC characteristics from Telephony Bearer service.
+     * Data type should be {@link Byte} array.
+     * @hide
+     */
+    public static final int METADATA_GTBS_CCCD = 28;
+
+    private static final int METADATA_MAX_KEY = METADATA_GTBS_CCCD;
 
     /**
      * Device type which is used in METADATA_DEVICE_TYPE
@@ -3210,7 +3242,149 @@
      * @hide
      */
     public static @MetadataKey int getMaxMetadataKey() {
-        return METADATA_LE_AUDIO;
+        return METADATA_MAX_KEY;
+    }
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+        prefix = { "REMOTE_STATUS_" },
+        value = {
+            /** Remote support status of audio policy feature is unknown/unconfigured **/
+            BluetoothAudioPolicy.FEATURE_UNCONFIGURED_BY_REMOTE,
+            /** Remote support status of audio policy feature is supported **/
+            BluetoothAudioPolicy.FEATURE_SUPPORTED_BY_REMOTE,
+            /** Remote support status of audio policy feature is not supported **/
+            BluetoothAudioPolicy.FEATURE_NOT_SUPPORTED_BY_REMOTE,
+        }
+    )
+
+    public @interface AudioPolicyRemoteSupport {}
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {
+            BluetoothStatusCodes.SUCCESS,
+            BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED,
+            BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ALLOWED,
+            BluetoothStatusCodes.ERROR_DEVICE_NOT_BONDED,
+            BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION,
+            BluetoothStatusCodes.FEATURE_NOT_SUPPORTED,
+            BluetoothStatusCodes.ERROR_PROFILE_NOT_CONNECTED,
+    })
+    public @interface AudioPolicyReturnValues{}
+
+    /**
+     * Returns whether the audio policy feature is supported by the remote.
+     * This requires a vendor specific command, so the API returns
+     * {@link BluetoothAudioPolicy#FEATURE_UNCONFIGURED_BY_REMOTE} to indicate the remote
+     * device has not yet relayed this information. After the internal configuration,
+     * the support status will be set to either
+     * {@link BluetoothAudioPolicy#FEATURE_NOT_SUPPORTED_BY_REMOTE} or
+     * {@link BluetoothAudioPolicy#FEATURE_SUPPORTED_BY_REMOTE}.
+     * The rest of the APIs related to this feature in both {@link BluetoothDevice}
+     * and {@link BluetoothAudioPolicy} should be invoked  only after getting a
+     * {@link BluetoothAudioPolicy#FEATURE_SUPPORTED_BY_REMOTE} response from this API.
+     *
+     * @return if call audio policy feature is supported or not
+     *
+     * @hide
+     */
+    @RequiresBluetoothConnectPermission
+    @RequiresPermission(allOf = {
+            android.Manifest.permission.BLUETOOTH_CONNECT,
+            android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+    })
+    public @AudioPolicyRemoteSupport int getAudioPolicyRemoteSupported() {
+        if (DBG) log("getAudioPolicyRemoteSupported()");
+        final IBluetooth service = getService();
+        final int defaultValue = BluetoothAudioPolicy.FEATURE_UNCONFIGURED_BY_REMOTE;
+        if (service == null || !isBluetoothEnabled()) {
+            Log.e(TAG, "BT not enabled. Cannot retrieve audio policy support status.");
+            if (DBG) log(Log.getStackTraceString(new Throwable()));
+        } else {
+            try {
+                final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+                service.getAudioPolicyRemoteSupported(this, mAttributionSource, recv);
+                return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+            } catch (TimeoutException e) {
+                Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+            } catch (RemoteException e) {
+                Log.e(TAG, "", e);
+                throw e.rethrowFromSystemServer();
+            }
+        }
+        return defaultValue;
+    }
+
+    /**
+     * Sets call audio preferences and sends them to the remote device.
+     *
+     * @param policies call audio policy preferences
+     * @return whether audio policy was set successfully or not
+     *
+     * @hide
+     */
+    @RequiresBluetoothConnectPermission
+    @RequiresPermission(allOf = {
+            android.Manifest.permission.BLUETOOTH_CONNECT,
+            android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+    })
+    public @AudioPolicyReturnValues int setAudioPolicy(@NonNull BluetoothAudioPolicy policies) {
+        if (DBG) log("setAudioPolicy");
+        final IBluetooth service = getService();
+        final int defaultValue = BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
+        if (service == null || !isBluetoothEnabled()) {
+            Log.e(TAG, "Bluetooth is not enabled. Cannot set Audio Policy.");
+            if (DBG) log(Log.getStackTraceString(new Throwable()));
+        } else {
+            try {
+                final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+                service.setAudioPolicy(this, policies, mAttributionSource, recv);
+                return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+            } catch (RemoteException | TimeoutException e) {
+                Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+            }
+        }
+        return defaultValue;
+    }
+
+    /**
+     * Gets the call audio preferences for the remote device.
+     * <p>Note that the caller should check if the feature is supported by
+     * invoking {@link BluetoothDevice#getAudioPolicyRemoteSupported} first.
+     * <p>This API will return null if
+     * 1. The bleutooth service is not started yet,
+     * 2. It is invoked for a device which is not bonded, or
+     * 3. The used transport, for example, HFP Client profile is not enabled or
+     * connected yet.
+     *
+     * @return call audio policy as {@link BluetoothAudioPolicy} object
+     *
+     * @hide
+     */
+    @RequiresBluetoothConnectPermission
+    @RequiresPermission(allOf = {
+            android.Manifest.permission.BLUETOOTH_CONNECT,
+            android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+    })
+    public @Nullable BluetoothAudioPolicy getAudioPolicy() {
+        if (DBG) log("getAudioPolicy");
+        final IBluetooth service = getService();
+        if (service == null || !isBluetoothEnabled()) {
+            Log.e(TAG, "Bluetooth is not enabled. Cannot get Audio Policy.");
+            if (DBG) log(Log.getStackTraceString(new Throwable()));
+        } else {
+            try {
+                final SynchronousResultReceiver<BluetoothAudioPolicy>
+                        recv = SynchronousResultReceiver.get();
+                service.getAudioPolicy(this, mAttributionSource, recv);
+                return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+            } catch (RemoteException | TimeoutException e) {
+                Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+            }
+        }
+        return null;
     }
 
     /**
diff --git a/system/binder/android/bluetooth/BluetoothAudioPolicy.aidl b/system/binder/android/bluetooth/BluetoothAudioPolicy.aidl
new file mode 100644
index 0000000..d06c286
--- /dev/null
+++ b/system/binder/android/bluetooth/BluetoothAudioPolicy.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+parcelable BluetoothAudioPolicy;
diff --git a/system/binder/android/bluetooth/IBluetooth.aidl b/system/binder/android/bluetooth/IBluetooth.aidl
index 68237c6..d55dbc1 100644
--- a/system/binder/android/bluetooth/IBluetooth.aidl
+++ b/system/binder/android/bluetooth/IBluetooth.aidl
@@ -25,6 +25,7 @@
 import android.bluetooth.IBluetoothSocketManager;
 import android.bluetooth.IBluetoothStateChangeCallback;
 import android.bluetooth.BluetoothActivityEnergyInfo;
+import android.bluetooth.BluetoothAudioPolicy;
 import android.bluetooth.BluetoothClass;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.IncomingRfcommSocketInfo;
@@ -267,6 +268,13 @@
     oneway void allowLowLatencyAudio(in boolean allowed, in BluetoothDevice device, in SynchronousResultReceiver receiver);
 
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
+    void getAudioPolicyRemoteSupported(in BluetoothDevice device, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
+    void setAudioPolicy(in BluetoothDevice device, in BluetoothAudioPolicy policies, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
+    void getAudioPolicy(in BluetoothDevice device, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
+
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
     oneway void startRfcommListener(String name, in ParcelUuid uuid, in PendingIntent intent, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
     oneway void stopRfcommListener(in ParcelUuid uuid, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
diff --git a/system/binder/android/bluetooth/IBluetoothHeadsetClient.aidl b/system/binder/android/bluetooth/IBluetoothHeadsetClient.aidl
index 3e1e83b..d0adfdf 100644
--- a/system/binder/android/bluetooth/IBluetoothHeadsetClient.aidl
+++ b/system/binder/android/bluetooth/IBluetoothHeadsetClient.aidl
@@ -17,6 +17,7 @@
 package android.bluetooth;
 
 import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothAudioPolicy;
 import android.bluetooth.BluetoothHeadsetClientCall;
 import android.content.AttributionSource;
 
@@ -86,6 +87,7 @@
     void setAudioRouteAllowed(in BluetoothDevice device, boolean allowed, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
     void getAudioRouteAllowed(in BluetoothDevice device, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
+
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
     void sendVendorAtCommand(in BluetoothDevice device, int vendorId, String atCommand, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
 
diff --git a/system/bta/dm/bta_dm_act.cc b/system/bta/dm/bta_dm_act.cc
index 3cbb536..cb56089 100644
--- a/system/bta/dm/bta_dm_act.cc
+++ b/system/bta/dm/bta_dm_act.cc
@@ -353,8 +353,10 @@
    * graceful shutdown.
    */
   bta_dm_search_cb.search_timer = alarm_new("bta_dm_search.search_timer");
+  bool delay_close_gatt =
+      osi_property_get_bool("bluetooth.gatt.delay_close.enabled", true);
   bta_dm_search_cb.gatt_close_timer =
-      alarm_new("bta_dm_search.gatt_close_timer");
+      delay_close_gatt ? alarm_new("bta_dm_search.gatt_close_timer") : nullptr;
   bta_dm_search_cb.pending_discovery_queue = fixed_queue_new(SIZE_MAX);
 
   memset(&bta_dm_conn_srvcs, 0, sizeof(bta_dm_conn_srvcs));
@@ -4001,11 +4003,25 @@
   bta_sys_sendmsg(p_msg);
 
   if (conn_id != GATT_INVALID_CONN_ID) {
-    /* start a GATT channel close delay timer */
-    bta_sys_start_timer(bta_dm_search_cb.gatt_close_timer,
-                        BTA_DM_GATT_CLOSE_DELAY_TOUT,
-                        BTA_DM_DISC_CLOSE_TOUT_EVT, 0);
     bta_dm_search_cb.pending_close_bda = bta_dm_search_cb.peer_bdaddr;
+    // Gatt will be close immediately if bluetooth.gatt.delay_close.enabled is
+    // set to false. If property is true / unset there will be a delay
+    if (bta_dm_search_cb.gatt_close_timer != nullptr) {
+      /* start a GATT channel close delay timer */
+      bta_sys_start_timer(bta_dm_search_cb.gatt_close_timer,
+                          BTA_DM_GATT_CLOSE_DELAY_TOUT,
+                          BTA_DM_DISC_CLOSE_TOUT_EVT, 0);
+    } else {
+      p_msg = (tBTA_DM_MSG*)osi_malloc(sizeof(tBTA_DM_MSG));
+      p_msg->hdr.event = BTA_DM_DISC_CLOSE_TOUT_EVT;
+      p_msg->hdr.layer_specific = 0;
+      bta_sys_sendmsg(p_msg);
+    }
+  } else {
+    if (bluetooth::common::init_flags::
+            bta_dm_clear_conn_id_on_client_close_is_enabled()) {
+      bta_dm_search_cb.conn_id = GATT_INVALID_CONN_ID;
+    }
   }
   bta_dm_search_cb.gatt_disc_active = false;
 }
@@ -4138,7 +4154,8 @@
       break;
 
     case BTA_GATTC_CLOSE_EVT:
-      LOG_DEBUG("BTA_GATTC_CLOSE_EVT reason = %d", p_data->close.reason);
+      LOG_INFO("BTA_GATTC_CLOSE_EVT reason = %d", p_data->close.reason);
+
       /* in case of disconnect before search is completed */
       if ((bta_dm_search_cb.state != BTA_DM_SEARCH_IDLE) &&
           (bta_dm_search_cb.state != BTA_DM_SEARCH_ACTIVE) &&
diff --git a/system/bta/dm/bta_dm_main.cc b/system/bta/dm/bta_dm_main.cc
index e844790..59bf00c 100644
--- a/system/bta/dm/bta_dm_main.cc
+++ b/system/bta/dm/bta_dm_main.cc
@@ -61,8 +61,8 @@
  *
  ******************************************************************************/
 bool bta_dm_search_sm_execute(BT_HDR_RIGID* p_msg) {
-  APPL_TRACE_EVENT("bta_dm_search_sm_execute state:%d, event:0x%x",
-                   bta_dm_search_cb.state, p_msg->event);
+  LOG_INFO("bta_dm_search_sm_execute state:%d, event:0x%x",
+           bta_dm_search_get_state(), p_msg->event);
 
   tBTA_DM_MSG* message = (tBTA_DM_MSG*)p_msg;
   switch (bta_dm_search_cb.state) {
@@ -123,6 +123,16 @@
           bta_dm_search_cancel_notify();
           bta_dm_execute_queued_request();
           break;
+        case BTA_DM_DISC_CLOSE_TOUT_EVT:
+          if (bluetooth::common::init_flags::
+                  bta_dm_clear_conn_id_on_client_close_is_enabled()) {
+            bta_dm_close_gatt_conn(message);
+            break;
+          }
+          [[fallthrough]];
+        default:
+          LOG_INFO("Received unexpected event 0x%x in state %d", p_msg->event,
+                   bta_dm_search_cb.state);
       }
       break;
     case BTA_DM_DISCOVER_ACTIVE:
@@ -145,6 +155,16 @@
         case BTA_DM_API_DISCOVER_EVT:
           bta_dm_queue_disc(message);
           break;
+        case BTA_DM_DISC_CLOSE_TOUT_EVT:
+          if (bluetooth::common::init_flags::
+                  bta_dm_clear_conn_id_on_client_close_is_enabled()) {
+            bta_dm_close_gatt_conn(message);
+            break;
+          }
+          [[fallthrough]];
+        default:
+          LOG_INFO("Received unexpected event 0x%x in state %d", p_msg->event,
+                   bta_dm_search_cb.state);
       }
       break;
   }
diff --git a/system/bta/hf_client/bta_hf_client_at.cc b/system/bta/hf_client/bta_hf_client_at.cc
index bb9eec3..7a710bf 100644
--- a/system/bta/hf_client/bta_hf_client_at.cc
+++ b/system/bta/hf_client/bta_hf_client_at.cc
@@ -20,11 +20,11 @@
 #define LOG_TAG "bt_hf_client"
 
 #include "bt_trace.h"  // Legacy trace logging
-
 #include "bta/hf_client/bta_hf_client_int.h"
 #include "osi/include/allocator.h"
 #include "osi/include/compat.h"
 #include "osi/include/log.h"
+#include "osi/include/properties.h"
 #include "stack/include/port_api.h"
 
 /* Uncomment to enable AT traffic dumping */
@@ -124,7 +124,7 @@
   tBTA_HF_CLIENT_AT_QCMD* new_cmd =
       (tBTA_HF_CLIENT_AT_QCMD*)osi_malloc(sizeof(tBTA_HF_CLIENT_AT_QCMD));
 
-  APPL_TRACE_DEBUG("%s", __func__);
+  APPL_TRACE_DEBUG("%s: cmd:%d", __func__, (int)cmd);
 
   new_cmd->cmd = cmd;
   new_cmd->buf_len = buf_len;
@@ -169,7 +169,7 @@
 static void bta_hf_client_send_at(tBTA_HF_CLIENT_CB* client_cb,
                                   tBTA_HF_CLIENT_AT_CMD cmd, const char* buf,
                                   uint16_t buf_len) {
-  APPL_TRACE_DEBUG("%s", __func__);
+  APPL_TRACE_DEBUG("%s %d", __func__, cmd);
   if ((client_cb->at_cb.current_cmd == BTA_HF_CLIENT_AT_NONE ||
        !client_cb->svc_conn) &&
       !alarm_is_scheduled(client_cb->at_cb.hold_timer)) {
@@ -197,6 +197,7 @@
     return;
   }
 
+  APPL_TRACE_DEBUG("%s: busy! queued: %d", __func__, cmd);
   bta_hf_client_queue_at(client_cb, cmd, buf, buf_len);
 }
 
@@ -240,7 +241,8 @@
  ******************************************************************************/
 
 static void bta_hf_client_handle_ok(tBTA_HF_CLIENT_CB* client_cb) {
-  APPL_TRACE_DEBUG("%s", __func__);
+  APPL_TRACE_DEBUG("%s: current_cmd:%d", __func__,
+                   client_cb->at_cb.current_cmd);
 
   bta_hf_client_stop_at_resp_timer(client_cb);
 
@@ -265,6 +267,9 @@
     case BTA_HF_CLIENT_AT_NONE:
       bta_hf_client_stop_at_hold_timer(client_cb);
       break;
+    case BTA_HF_CLIENT_AT_ANDROID:
+      bta_hf_client_at_result(client_cb, BTA_HF_CLIENT_AT_RESULT_OK, 0);
+      break;
     default:
       if (client_cb->send_at_reply) {
         bta_hf_client_at_result(client_cb, BTA_HF_CLIENT_AT_RESULT_OK, 0);
@@ -280,7 +285,8 @@
 static void bta_hf_client_handle_error(tBTA_HF_CLIENT_CB* client_cb,
                                        tBTA_HF_CLIENT_AT_RESULT_TYPE type,
                                        uint16_t cme) {
-  APPL_TRACE_DEBUG("%s: %u %u", __func__, type, cme);
+  APPL_TRACE_DEBUG("%s: type:%u cme:%u current_cmd:%d", __func__, type, cme,
+                   client_cb->at_cb.current_cmd);
 
   bta_hf_client_stop_at_resp_timer(client_cb);
 
@@ -301,6 +307,9 @@
         client_cb->send_at_reply = true;
       }
       break;
+    case BTA_HF_CLIENT_AT_ANDROID:
+      bta_hf_client_at_result(client_cb, type, cme);
+      break;
     default:
       if (client_cb->send_at_reply) {
         bta_hf_client_at_result(client_cb, type, cme);
@@ -2139,19 +2148,19 @@
 
   at_len = snprintf(buf, sizeof(buf), "AT+BIA=");
 
+  const int32_t position = osi_property_get_int32(
+      "bluetooth.headsetclient.disable_indicator.position", -1);
+
   for (i = 0; i < BTA_HF_CLIENT_AT_INDICATOR_COUNT; i++) {
     int sup = client_cb->at_cb.indicator_lookup[i] == -1 ? 0 : 1;
 
-/* If this value matches the position of SIGNAL in the indicators array,
- * then hardcode disable signal strength indicators.
- * indicator_lookup[i] points to the position in the bta_hf_client_indicators
- * array defined at the top of this file */
-#ifdef BTA_HF_CLIENT_INDICATOR_SIGNAL_POS
-    if (client_cb->at_cb.indicator_lookup[i] ==
-        BTA_HF_CLIENT_INDICATOR_SIGNAL_POS) {
+    /* If this value matches the position of SIGNAL in the indicators array,
+     * then hardcode disable signal strength indicators.
+     * indicator_lookup[i] points to the position in the
+     * bta_hf_client_indicators array defined at the top of this file */
+    if (client_cb->at_cb.indicator_lookup[i] == position) {
       sup = 0;
     }
-#endif
 
     at_len += snprintf(buf + at_len, sizeof(buf) - at_len, "%u,", sup);
   }
@@ -2185,6 +2194,22 @@
                         at_len);
 }
 
+void bta_hf_client_send_at_android(tBTA_HF_CLIENT_CB* client_cb,
+                                   const char* str) {
+  char buf[BTA_HF_CLIENT_AT_MAX_LEN];
+  int at_len;
+
+  APPL_TRACE_DEBUG("%s", __func__);
+
+  at_len = snprintf(buf, sizeof(buf), "AT%s\r", str);
+  if (at_len < 0) {
+    APPL_TRACE_ERROR("%s: AT command Framing error", __func__);
+    return;
+  }
+
+  bta_hf_client_send_at(client_cb, BTA_HF_CLIENT_AT_ANDROID, buf, at_len);
+}
+
 void bta_hf_client_at_init(tBTA_HF_CLIENT_CB* client_cb) {
   alarm_free(client_cb->at_cb.resp_timer);
   alarm_free(client_cb->at_cb.hold_timer);
@@ -2284,6 +2309,9 @@
     case BTA_HF_CLIENT_AT_CMD_VENDOR_SPECIFIC_CMD:
       bta_hf_client_send_at_vendor_specific_cmd(client_cb, p_val->str);
       break;
+    case BTA_HF_CLIENT_AT_CMD_ANDROID:
+      bta_hf_client_send_at_android(client_cb, p_val->str);
+      break;
     default:
       APPL_TRACE_ERROR("Default case");
       snprintf(buf, BTA_HF_CLIENT_AT_MAX_LEN,
diff --git a/system/bta/hf_client/bta_hf_client_int.h b/system/bta/hf_client/bta_hf_client_int.h
index 3c13e6c..cfa63bf 100755
--- a/system/bta/hf_client/bta_hf_client_int.h
+++ b/system/bta/hf_client/bta_hf_client_int.h
@@ -105,6 +105,7 @@
   BTA_HF_CLIENT_AT_BIND_READ_ENABLED_IND,
   BTA_HF_CLIENT_AT_BIEV,
   BTA_HF_CLIENT_AT_VENDOR_SPECIFIC,
+  BTA_HF_CLIENT_AT_ANDROID,
 };
 
 /*****************************************************************************
diff --git a/system/bta/include/bta_hf_client_api.h b/system/bta/include/bta_hf_client_api.h
index f37be30..0b01da6 100644
--- a/system/bta/include/bta_hf_client_api.h
+++ b/system/bta/include/bta_hf_client_api.h
@@ -172,6 +172,7 @@
 #define BTA_HF_CLIENT_AT_CMD_NREC 15
 #define BTA_HF_CLIENT_AT_CMD_VENDOR_SPECIFIC_CMD 16
 #define BTA_HF_CLIENT_AT_CMD_BIEV 17
+#define BTA_HF_CLIENT_AT_CMD_ANDROID 18
 
 typedef uint8_t tBTA_HF_CLIENT_AT_CMD_TYPE;
 
diff --git a/system/bta/le_audio/devices.cc b/system/bta/le_audio/devices.cc
index f12c923..13cfa78 100644
--- a/system/bta/le_audio/devices.cc
+++ b/system/bta/le_audio/devices.cc
@@ -867,7 +867,8 @@
 }
 
 bool LeAudioDeviceGroup::IsReleasingOrIdle(void) {
-  return target_state_ == AseState::BTA_LE_AUDIO_ASE_STATE_IDLE;
+  return (target_state_ == AseState::BTA_LE_AUDIO_ASE_STATE_IDLE) ||
+         (current_state_ == AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
 }
 
 bool LeAudioDeviceGroup::IsGroupStreamReady(void) {
diff --git a/system/bta/le_audio/state_machine.cc b/system/bta/le_audio/state_machine.cc
index d891850..49f80f5 100644
--- a/system/bta/le_audio/state_machine.cc
+++ b/system/bta/le_audio/state_machine.cc
@@ -665,6 +665,11 @@
      */
     group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
     group->SetTargetState(AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
+
+    /* Clear group pending status */
+    group->ClearPendingAvailableContextsChange();
+    group->ClearPendingConfiguration();
+
     if (alarm_is_scheduled(watchdog_)) alarm_cancel(watchdog_);
     ReleaseCisIds(group);
     state_machine_callbacks_->StatusReportCb(group->group_id_,
@@ -892,6 +897,10 @@
          * In such an event, there is need to notify upper layer about state
          * from here.
          */
+        if (alarm_is_scheduled(watchdog_)) {
+          alarm_cancel(watchdog_);
+        }
+
         if (current_group_state == AseState::BTA_LE_AUDIO_ASE_STATE_IDLE) {
           LOG_INFO(
               "Cises disconnected for group %d, we are good in Idle state.",
@@ -906,12 +915,11 @@
               "Cises disconnected for group: %d, we are good in Configured "
               "state, reconfig=%d.",
               group->group_id_, reconfig);
+
           if (reconfig) {
             group->ClearPendingConfiguration();
             state_machine_callbacks_->StatusReportCb(
                 group->group_id_, GroupStreamStatus::CONFIGURED_BY_USER);
-            /* No more transition for group */
-            alarm_cancel(watchdog_);
           } else {
             /* This is Autonomous change if both, target and current state
              * is CODEC_CONFIGURED
@@ -1873,6 +1881,8 @@
             return;
           }
 
+          if (alarm_is_scheduled(watchdog_)) alarm_cancel(watchdog_);
+
           state_machine_callbacks_->StatusReportCb(
               group->group_id_, GroupStreamStatus::CONFIGURED_AUTONOMOUS);
         }
diff --git a/system/bta/le_audio/state_machine_test.cc b/system/bta/le_audio/state_machine_test.cc
index 18e8125..1cafe06 100644
--- a/system/bta/le_audio/state_machine_test.cc
+++ b/system/bta/le_audio/state_machine_test.cc
@@ -449,6 +449,11 @@
   }
 
   void TearDown() override {
+    /* Clear the alarm on tear down in case test case ends when the
+     * alarm is scheduled
+     */
+    alarm_cancel(nullptr);
+
     iso_manager_->Stop();
     mock_iso_manager_ = nullptr;
     codec_manager_->Stop();
@@ -1238,6 +1243,9 @@
   // Check if group has transitioned to a proper state
   ASSERT_EQ(group->GetState(),
             types::AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
+
+  /* Cancel is called when group goes to streaming. */
+  ASSERT_EQ(0, mock_function_count_map["alarm_cancel"]);
 }
 
 TEST_F(StateMachineTest, testConfigureCodecMulti) {
@@ -1280,6 +1288,9 @@
   // Check if group has transitioned to a proper state
   ASSERT_EQ(group->GetState(),
             types::AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
+
+  /* Cancel is called when group goes to streaming. */
+  ASSERT_EQ(0, mock_function_count_map["alarm_cancel"]);
 }
 
 TEST_F(StateMachineTest, testConfigureQosSingle) {
@@ -1322,6 +1333,8 @@
   // Check if group has transitioned to a proper state
   ASSERT_EQ(group->GetState(),
             types::AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
+
+  ASSERT_EQ(0, mock_function_count_map["alarm_cancel"]);
 }
 
 TEST_F(StateMachineTest, testConfigureQosSingleRecoverCig) {
@@ -1368,6 +1381,7 @@
   // Check if group has transitioned to a proper state
   ASSERT_EQ(group->GetState(),
             types::AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
+  ASSERT_EQ(0, mock_function_count_map["alarm_cancel"]);
 }
 
 TEST_F(StateMachineTest, testConfigureQosMultiple) {
@@ -1413,6 +1427,7 @@
   // Check if group has transitioned to a proper state
   ASSERT_EQ(group->GetState(),
             types::AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
+  ASSERT_EQ(0, mock_function_count_map["alarm_cancel"]);
 }
 
 TEST_F(StateMachineTest, testStreamSingle) {
@@ -1463,6 +1478,7 @@
   // Check if group has transitioned to a proper state
   ASSERT_EQ(group->GetState(),
             types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
 }
 
 TEST_F(StateMachineTest, testStreamSkipEnablingSink) {
@@ -1511,6 +1527,8 @@
   // Check if group has transitioned to a proper state
   ASSERT_EQ(group->GetState(),
             types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
 }
 
 TEST_F(StateMachineTest, testStreamSkipEnablingSinkSource) {
@@ -1562,6 +1580,7 @@
   // Check if group has transitioned to a proper state
   ASSERT_EQ(group->GetState(),
             types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
 }
 
 TEST_F(StateMachineTest, testStreamMultipleConversational) {
@@ -1615,6 +1634,7 @@
   // Check if group has transitioned to a proper state
   ASSERT_EQ(group->GetState(),
             types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
 }
 
 TEST_F(StateMachineTest, testStreamMultiple) {
@@ -1667,6 +1687,7 @@
   // Check if group has transitioned to a proper state
   ASSERT_EQ(group->GetState(),
             types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
 }
 
 TEST_F(StateMachineTest, testUpdateMetadataMultiple) {
@@ -1722,6 +1743,9 @@
   ASSERT_EQ(group->GetState(),
             types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
 
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+  mock_function_count_map["alarm_cancel"] = 0;
+
   // Make sure all devices get the metadata update
   leAudioDevice = group->GetFirstDevice();
   expected_devices_written = 0;
@@ -1741,6 +1765,9 @@
   ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream(
       group, static_cast<LeAudioContextType>(context_type),
       metadata_context_type));
+
+  /* This is just update metadata - watchdog is not used */
+  ASSERT_EQ(0, mock_function_count_map["alarm_cancel"]);
 }
 
 TEST_F(StateMachineTest, testDisableSingle) {
@@ -1781,6 +1808,11 @@
 
   InjectInitialIdleNotification(group);
 
+  EXPECT_CALL(
+      mock_callbacks_,
+      StatusReportCb(leaudio_group_id,
+                     bluetooth::le_audio::GroupStreamStatus::STREAMING));
+
   // Start the configuration and stream Media content
   LeAudioGroupStateMachine::Get()->StartStream(
       group, static_cast<LeAudioContextType>(context_type),
@@ -1790,6 +1822,10 @@
   ASSERT_EQ(group->GetState(),
             types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
 
+  testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+  mock_function_count_map["alarm_cancel"] = 0;
+
   // Validate GroupStreamStatus
   EXPECT_CALL(
       mock_callbacks_,
@@ -1806,6 +1842,9 @@
   // Check if group has transition to a proper state
   ASSERT_EQ(group->GetState(),
             types::AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
+
+  testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
 }
 
 TEST_F(StateMachineTest, testDisableMultiple) {
@@ -1857,6 +1896,8 @@
   // Check if group has transitioned to a proper state
   ASSERT_EQ(group->GetState(),
             types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+  mock_function_count_map["alarm_cancel"] = 0;
 
   // Validate GroupStreamStatus
   EXPECT_CALL(
@@ -1874,6 +1915,8 @@
   // Check if group has transitioned to a proper state
   ASSERT_EQ(group->GetState(),
             types::AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
+  testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
 }
 
 TEST_F(StateMachineTest, testDisableBidirectional) {
@@ -1958,6 +2001,19 @@
   ASSERT_EQ(group->GetState(),
             types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
 
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+  mock_function_count_map["alarm_cancel"] = 0;
+
+  // Validate GroupStreamStatus
+  EXPECT_CALL(
+      mock_callbacks_,
+      StatusReportCb(leaudio_group_id,
+                     bluetooth::le_audio::GroupStreamStatus::SUSPENDING));
+  EXPECT_CALL(
+      mock_callbacks_,
+      StatusReportCb(leaudio_group_id,
+                     bluetooth::le_audio::GroupStreamStatus::SUSPENDED));
+
   // Suspend the stream
   LeAudioGroupStateMachine::Get()->SuspendStream(group);
 
@@ -1966,6 +2022,9 @@
             types::AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
   ASSERT_EQ(removed_bidirectional, true);
   ASSERT_EQ(removed_unidirectional, true);
+
+  testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
 }
 
 TEST_F(StateMachineTest, testReleaseSingle) {
@@ -2012,7 +2071,8 @@
   // Check if group has transitioned to a proper state
   ASSERT_EQ(group->GetState(),
             types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
-
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+  mock_function_count_map["alarm_cancel"] = 0;
   // Validate GroupStreamStatus
   EXPECT_CALL(
       mock_callbacks_,
@@ -2027,6 +2087,7 @@
 
   // Check if group has transitioned to a proper state
   ASSERT_EQ(group->GetState(), types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
 }
 
 TEST_F(StateMachineTest, testReleaseCachingSingle) {
@@ -2090,12 +2151,17 @@
   ASSERT_EQ(group->GetState(),
             types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
 
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+  mock_function_count_map["alarm_cancel"] = 0;
+
   // Stop the stream
   LeAudioGroupStateMachine::Get()->StopStream(group);
 
   // Check if group has transitioned to a proper state
   ASSERT_EQ(group->GetState(),
             types::AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
+
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
 }
 
 TEST_F(StateMachineTest,
@@ -2167,6 +2233,9 @@
   ASSERT_EQ(group->GetState(),
             types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
 
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+  mock_function_count_map["alarm_cancel"] = 0;
+
   // Stop the stream
   LeAudioGroupStateMachine::Get()->StopStream(group);
 
@@ -2174,6 +2243,9 @@
   ASSERT_EQ(group->GetState(),
             types::AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
 
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+  mock_function_count_map["alarm_cancel"] = 0;
+
   // Start the configuration and stream Media content
   LeAudioGroupStateMachine::Get()->StartStream(
       group, static_cast<LeAudioContextType>(context_type),
@@ -2182,6 +2254,9 @@
   // Check if group has transitioned to a proper state
   ASSERT_EQ(group->GetState(),
             types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+  mock_function_count_map["alarm_cancel"] = 0;
 }
 
 TEST_F(StateMachineTest,
@@ -2267,6 +2342,9 @@
   ASSERT_EQ(group->GetState(),
             types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
 
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+  mock_function_count_map["alarm_cancel"] = 0;
+
   // Stop the stream
   LeAudioGroupStateMachine::Get()->StopStream(group);
 
@@ -2274,6 +2352,9 @@
   ASSERT_EQ(group->GetState(),
             types::AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
 
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+  mock_function_count_map["alarm_cancel"] = 0;
+
   // Start the configuration and stream Media content
   context_type = kContextTypeMedia;
   LeAudioGroupStateMachine::Get()->StartStream(
@@ -2283,6 +2364,7 @@
   // Check if group has transitioned to a proper state
   ASSERT_EQ(group->GetState(),
             types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
 }
 
 TEST_F(StateMachineTest, testReleaseMultiple) {
@@ -2332,6 +2414,9 @@
   ASSERT_EQ(group->GetState(),
             types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
 
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+  mock_function_count_map["alarm_cancel"] = 0;
+
   // Validate GroupStreamStatus
   EXPECT_CALL(
       mock_callbacks_,
@@ -2346,6 +2431,7 @@
 
   // Check if group has transitioned to a proper state
   ASSERT_EQ(group->GetState(), types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
 }
 
 TEST_F(StateMachineTest, testReleaseBidirectional) {
@@ -2393,11 +2479,16 @@
   ASSERT_EQ(group->GetState(),
             types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
 
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+  mock_function_count_map["alarm_cancel"] = 0;
+
   // Stop the stream
   LeAudioGroupStateMachine::Get()->StopStream(group);
 
   // Check if group has transitioned to a proper state
   ASSERT_EQ(group->GetState(), types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+  mock_function_count_map["alarm_cancel"] = 0;
 }
 
 TEST_F(StateMachineTest, testDisableAndReleaseBidirectional) {
@@ -2558,6 +2649,9 @@
   /* Single disconnect as it is bidirectional Cis*/
   EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(2);
 
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+  mock_function_count_map["alarm_cancel"] = 0;
+
   for (auto* device = group->GetFirstDevice(); device != nullptr;
        device = group->GetNextDevice(device)) {
     for (auto& ase : device->ases_) {
@@ -2581,6 +2675,8 @@
       ASSERT_EQ(ase.state, types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
     }
   }
+
+  ASSERT_EQ(0, mock_function_count_map["alarm_cancel"]);
 }
 
 TEST_F(StateMachineTest, testAseAutonomousRelease2Devices) {
@@ -3120,6 +3216,9 @@
 
   testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
 
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+  mock_function_count_map["alarm_cancel"] = 0;
+
   // Validate GroupStreamStatus
   EXPECT_CALL(
       mock_callbacks_,
@@ -3136,6 +3235,9 @@
 
   testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
 
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+  mock_function_count_map["alarm_cancel"] = 0;
+
   // Restart stream
   EXPECT_CALL(
       mock_callbacks_,
@@ -3147,6 +3249,7 @@
       group, context_type, types::AudioContexts(context_type));
 
   testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
 }
 
 TEST_F(StateMachineTest, BoundedHeadphonesConversationalToMediaChannelCount_2) {
@@ -3304,6 +3407,9 @@
 
   testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
 
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+  mock_function_count_map["alarm_cancel"] = 0;
+
   // Validate GroupStreamStatus
   EXPECT_CALL(
       mock_callbacks_,
@@ -3319,6 +3425,8 @@
   LeAudioGroupStateMachine::Get()->StopStream(group);
 
   testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+  mock_function_count_map["alarm_cancel"] = 0;
 
   // Restart stream
   EXPECT_CALL(
@@ -3331,6 +3439,7 @@
       group, new_context_type, types::AudioContexts(new_context_type));
 
   testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
 }
 
 TEST_F(StateMachineTest, lateCisDisconnectedEvent_ConfiguredByUser) {
@@ -3383,6 +3492,9 @@
             types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
   testing::Mock::VerifyAndClearExpectations(&mock_iso_manager_);
 
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+  mock_function_count_map["alarm_cancel"] = 0;
+
   /* Prepare DisconnectCis mock to not symulate CisDisconnection */
   ON_CALL(*mock_iso_manager_, DisconnectCis).WillByDefault(Return());
 
@@ -3404,6 +3516,8 @@
 
   testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
 
+  ASSERT_EQ(0, mock_function_count_map["alarm_cancel"]);
+
   EXPECT_CALL(mock_callbacks_,
               StatusReportCb(
                   leaudio_group_id,
@@ -3412,6 +3526,7 @@
   // Inject CIS and ACL disconnection of first device
   InjectCisDisconnected(group, leAudioDevice, HCI_ERR_CONN_CAUSE_LOCAL_HOST);
   testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
 }
 
 TEST_F(StateMachineTest, lateCisDisconnectedEvent_AutonomousConfigured) {
@@ -3464,6 +3579,9 @@
             types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
   testing::Mock::VerifyAndClearExpectations(&mock_iso_manager_);
 
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+  mock_function_count_map["alarm_cancel"] = 0;
+
   /* Prepare DisconnectCis mock to not symulate CisDisconnection */
   ON_CALL(*mock_iso_manager_, DisconnectCis).WillByDefault(Return());
 
@@ -3489,6 +3607,8 @@
 
   testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
 
+  ASSERT_EQ(0, mock_function_count_map["alarm_cancel"]);
+
   EXPECT_CALL(
       mock_callbacks_,
       StatusReportCb(
@@ -3498,6 +3618,7 @@
   // Inject CIS and ACL disconnection of first device
   InjectCisDisconnected(group, leAudioDevice, HCI_ERR_CONN_CAUSE_LOCAL_HOST);
   testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
 }
 
 TEST_F(StateMachineTest, lateCisDisconnectedEvent_Idle) {
@@ -3550,6 +3671,8 @@
             types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
   testing::Mock::VerifyAndClearExpectations(&mock_iso_manager_);
 
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+  mock_function_count_map["alarm_cancel"] = 0;
   /* Prepare DisconnectCis mock to not symulate CisDisconnection */
   ON_CALL(*mock_iso_manager_, DisconnectCis).WillByDefault(Return());
 
@@ -3569,6 +3692,7 @@
 
   // Check if group has transitioned to a proper state
   ASSERT_EQ(group->GetState(), types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
+  ASSERT_EQ(0, mock_function_count_map["alarm_cancel"]);
 
   testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
 
@@ -3579,6 +3703,7 @@
   // Inject CIS and ACL disconnection of first device
   InjectCisDisconnected(group, leAudioDevice, HCI_ERR_CONN_CAUSE_LOCAL_HOST);
   testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
+  ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
 }
 }  // namespace internal
 }  // namespace le_audio
diff --git a/system/bta/vc/vc.cc b/system/bta/vc/vc.cc
index dd608b1..196816e 100644
--- a/system/bta/vc/vc.cc
+++ b/system/bta/vc/vc.cc
@@ -102,6 +102,21 @@
       volume_control_devices_.Add(address, true);
     } else {
       device->connecting_actively = true;
+
+      if (device->IsConnected()) {
+        LOG(WARNING) << __func__ << ": address=" << address
+                     << ", connection_id=" << device->connection_id
+                     << " already connected.";
+
+        if (device->IsReady()) {
+          callbacks_->OnConnectionState(ConnectionState::CONNECTED,
+                                        device->address);
+        } else {
+          OnGattConnected(GATT_SUCCESS, device->connection_id, gatt_if_,
+                          device->address, BT_TRANSPORT_LE, GATT_MAX_MTU_SIZE);
+        }
+        return;
+      }
     }
 
     BTA_GATTC_Open(gatt_if_, address, BTM_BLE_DIRECT_CONNECTION, false);
@@ -628,13 +643,22 @@
       return;
     }
 
+    if (!device->IsConnected()) {
+      LOG(ERROR) << __func__
+                 << " Skipping disconnect of the already disconnected device, "
+                    "connection_id="
+                 << loghex(connection_id);
+      return;
+    }
+
     // If we get here, it means, device has not been exlicitly disconnected.
     bool device_ready = device->IsReady();
 
     device_cleanup_helper(device, device->connecting_actively);
 
     if (device_ready) {
-      volume_control_devices_.Add(remote_bda, true);
+      device->first_connection = true;
+      device->connecting_actively = true;
 
       /* Add device into BG connection to accept remote initiated connection */
       BTA_GATTC_Open(gatt_if_, remote_bda, BTM_BLE_BKG_CONNECT_ALLOW_LIST,
@@ -1068,7 +1092,6 @@
     if (notify)
       callbacks_->OnConnectionState(ConnectionState::DISCONNECTED,
                                     device->address);
-    volume_control_devices_.Remove(device->address);
   }
 
   void devices_control_point_helper(std::vector<RawAddress>& devices,
diff --git a/system/bta/vc/vc_test.cc b/system/bta/vc/vc_test.cc
index 9faa319..ca1bb02 100644
--- a/system/bta/vc/vc_test.cc
+++ b/system/bta/vc/vc_test.cc
@@ -547,6 +547,55 @@
   TestAppUnregister();
 }
 
+TEST_F(VolumeControlTest, test_reconnect_after_interrupted_discovery) {
+  const RawAddress test_address = GetTestAddress(0);
+
+  // Initial connection - no callback calls yet as we want to disconnect in the
+  // middle
+  SetSampleDatabaseVOCS(1);
+  TestAppRegister();
+  TestConnect(test_address);
+  EXPECT_CALL(*callbacks,
+              OnConnectionState(ConnectionState::CONNECTED, test_address))
+      .Times(0);
+  EXPECT_CALL(*callbacks, OnDeviceAvailable(test_address, 2)).Times(0);
+  GetConnectedEvent(test_address, 1);
+  Mock::VerifyAndClearExpectations(callbacks.get());
+
+  // Remote disconnects in the middle of the service discovery
+  EXPECT_CALL(*callbacks,
+              OnConnectionState(ConnectionState::DISCONNECTED, test_address));
+  GetDisconnectedEvent(test_address, 1);
+  Mock::VerifyAndClearExpectations(callbacks.get());
+
+  // This time let the service discovery pass
+  ON_CALL(gatt_interface, ServiceSearchRequest(_, _))
+      .WillByDefault(Invoke(
+          [&](uint16_t conn_id, const bluetooth::Uuid* p_srvc_uuid) -> void {
+            if (*p_srvc_uuid == kVolumeControlUuid)
+              GetSearchCompleteEvent(conn_id);
+          }));
+
+  // Remote is being connected by another GATT client
+  EXPECT_CALL(*callbacks,
+              OnConnectionState(ConnectionState::CONNECTED, test_address));
+  EXPECT_CALL(*callbacks, OnDeviceAvailable(test_address, 2));
+  GetConnectedEvent(test_address, 1);
+  Mock::VerifyAndClearExpectations(callbacks.get());
+
+  // Request connect when the remote was already connected by another service
+  EXPECT_CALL(*callbacks, OnDeviceAvailable(test_address, 2)).Times(0);
+  EXPECT_CALL(*callbacks,
+              OnConnectionState(ConnectionState::CONNECTED, test_address));
+  VolumeControl::Get()->Connect(test_address);
+  // The GetConnectedEvent(test_address, 1); should not be triggered here, since
+  // GATT implementation will not send this event for the already connected
+  // device
+  Mock::VerifyAndClearExpectations(callbacks.get());
+
+  TestAppUnregister();
+}
+
 TEST_F(VolumeControlTest, test_add_from_storage) {
   TestAppRegister();
   TestAddFromStorage(GetTestAddress(0), true);
diff --git a/system/btif/co/bta_hh_co.cc b/system/btif/co/bta_hh_co.cc
index 6115627..90fe1df 100644
--- a/system/btif/co/bta_hh_co.cc
+++ b/system/btif/co/bta_hh_co.cc
@@ -673,16 +673,15 @@
     ev.type = UHID_FEATURE_ANSWER;
     ev.u.feature_answer.id = *get_rpt_id;
     ev.u.feature_answer.err = status;
-    ev.u.feature_answer.size = len - GET_RPT_RSP_OFFSET;
+    ev.u.feature_answer.size = len;
     osi_free(get_rpt_id);
-    if (len > GET_RPT_RSP_OFFSET) {
-      if (len - GET_RPT_RSP_OFFSET > UHID_DATA_MAX) {
+    if (len > 0) {
+      if (len > UHID_DATA_MAX) {
         APPL_TRACE_WARNING("%s: Report size greater than allowed size",
                            __func__);
         return;
       }
-      memcpy(ev.u.feature_answer.data, p_rpt + GET_RPT_RSP_OFFSET,
-             len - GET_RPT_RSP_OFFSET);
+      memcpy(ev.u.feature_answer.data, p_rpt + GET_RPT_RSP_OFFSET, len);
       uhid_write(p_dev->fd, &ev);
     }
   }
diff --git a/system/btif/src/btif_hf_client.cc b/system/btif/src/btif_hf_client.cc
index b7a93fa..177ea7c 100644
--- a/system/btif/src/btif_hf_client.cc
+++ b/system/btif/src/btif_hf_client.cc
@@ -743,6 +743,27 @@
   return BT_STATUS_SUCCESS;
 }
 
+/*******************************************************************************
+ *
+ * Function         send_hfp_audio_policy
+ *
+ * Description      Send requested audio policies to remote device.
+ *
+ * Returns          bt_status_t
+ *
+ ******************************************************************************/
+static bt_status_t send_android_at(const RawAddress* bd_addr, const char* arg) {
+  btif_hf_client_cb_t* cb = btif_hf_client_get_cb_by_bda(*bd_addr);
+  if (cb == NULL || !is_connected(cb)) return BT_STATUS_FAIL;
+
+  CHECK_BTHF_CLIENT_SLC_CONNECTED(cb);
+
+  BTIF_TRACE_EVENT("%s: val1 %s", __func__, arg);
+  BTA_HfClientSendAT(cb->handle, BTA_HF_CLIENT_AT_CMD_ANDROID, 0, 0, arg);
+
+  return BT_STATUS_SUCCESS;
+}
+
 static const bthf_client_interface_t bthfClientInterface = {
     .size = sizeof(bthf_client_interface_t),
     .init = init,
@@ -763,6 +784,7 @@
     .request_last_voice_tag_number = request_last_voice_tag_number,
     .cleanup = cleanup,
     .send_at_cmd = send_at_cmd,
+    .send_android_at = send_android_at,
 };
 
 static void process_ind_evt(tBTA_HF_CLIENT_IND* ind) {
diff --git a/system/gd/common/init_flags.fbs b/system/gd/common/init_flags.fbs
index d1d8c12..b6dbe0a 100644
--- a/system/gd/common/init_flags.fbs
+++ b/system/gd/common/init_flags.fbs
@@ -14,6 +14,7 @@
 
     asynchronously_start_l2cap_coc_is_enabled:bool (privacy:"Any");
     btaa_hci_is_enabled:bool (privacy:"Any");
+    bta_dm_clear_conn_id_on_client_close_is_enabled:bool (privacy:"Any");
     btm_dm_flush_discovery_queue_on_search_cancel_is_enabled:bool (privacy:"Any");
     finite_att_timeout_is_enabled:bool (privacy:"Any");
     gatt_robust_caching_client_is_enabled:bool (privacy:"Any");
diff --git a/system/gd/dumpsys/bundler/Android.bp b/system/gd/dumpsys/bundler/Android.bp
index 3754611..41f5549 100644
--- a/system/gd/dumpsys/bundler/Android.bp
+++ b/system/gd/dumpsys/bundler/Android.bp
@@ -69,9 +69,8 @@
     ],
 }
 
-cc_test {
+cc_test_host {
     name: "bluetooth_flatbuffer_bundler_test",
-    host_supported: true,
     srcs: [
         ":BluetoothFlatbufferBundlerTestSources",
     ],
diff --git a/system/gd/dumpsys/init_flags.cc b/system/gd/dumpsys/init_flags.cc
index 79e1d8d..08d3bf9 100644
--- a/system/gd/dumpsys/init_flags.cc
+++ b/system/gd/dumpsys/init_flags.cc
@@ -35,6 +35,8 @@
 
   builder.add_asynchronously_start_l2cap_coc_is_enabled(initFlags::asynchronously_start_l2cap_coc_is_enabled());
   builder.add_btaa_hci_is_enabled(initFlags::btaa_hci_is_enabled());
+  builder.add_bta_dm_clear_conn_id_on_client_close_is_enabled(
+      initFlags::bta_dm_clear_conn_id_on_client_close_is_enabled());
   builder.add_btm_dm_flush_discovery_queue_on_search_cancel_is_enabled(
       initFlags::btm_dm_flush_discovery_queue_on_search_cancel_is_enabled());
   builder.add_finite_att_timeout_is_enabled(initFlags::finite_att_timeout_is_enabled());
diff --git a/system/gd/rust/common/src/init_flags.rs b/system/gd/rust/common/src/init_flags.rs
index 21e0c41..8e06df7 100644
--- a/system/gd/rust/common/src/init_flags.rs
+++ b/system/gd/rust/common/src/init_flags.rs
@@ -207,6 +207,7 @@
     flags: {
         asynchronously_start_l2cap_coc = true,
         btaa_hci = true,
+        bta_dm_clear_conn_id_on_client_close = true,
         btm_dm_flush_discovery_queue_on_search_cancel,
         finite_att_timeout = true,
         gatt_robust_caching_client = true,
diff --git a/system/gd/rust/shim/src/init_flags.rs b/system/gd/rust/shim/src/init_flags.rs
index 071d658..46c379f 100644
--- a/system/gd/rust/shim/src/init_flags.rs
+++ b/system/gd/rust/shim/src/init_flags.rs
@@ -6,6 +6,7 @@
 
         fn asynchronously_start_l2cap_coc_is_enabled() -> bool;
         fn btaa_hci_is_enabled() -> bool;
+        fn bta_dm_clear_conn_id_on_client_close_is_enabled() -> bool;
         fn btm_dm_flush_discovery_queue_on_search_cancel_is_enabled() -> bool;
         fn finite_att_timeout_is_enabled() -> bool;
         fn gatt_robust_caching_client_is_enabled() -> bool;
diff --git a/system/include/hardware/bt_hf_client.h b/system/include/hardware/bt_hf_client.h
index 7263805..bf8b1bb 100644
--- a/system/include/hardware/bt_hf_client.h
+++ b/system/include/hardware/bt_hf_client.h
@@ -392,6 +392,9 @@
   /** Send AT Command. */
   bt_status_t (*send_at_cmd)(const RawAddress* bd_addr, int cmd, int val1,
                              int val2, const char* arg);
+
+  /** Send hfp audio policy to remote */
+  bt_status_t (*send_android_at)(const RawAddress* bd_addr, const char* arg);
 } bthf_client_interface_t;
 
 __END_DECLS
diff --git a/system/stack/sdp/sdp_discovery.cc b/system/stack/sdp/sdp_discovery.cc
index 22d6e7c..9988c54 100644
--- a/system/stack/sdp/sdp_discovery.cc
+++ b/system/stack/sdp/sdp_discovery.cc
@@ -337,7 +337,7 @@
   uint8_t* p_end;
   uint8_t type;
 
-  if (p_ccb->p_db->raw_data) {
+  if (p_ccb->p_db && p_ccb->p_db->raw_data) {
     cpy_len = p_ccb->p_db->raw_size - p_ccb->p_db->raw_used;
     list_len = p_ccb->list_len;
     p = &p_ccb->rsp_list[0];
diff --git a/system/test/mock/mock_bta_gatts_api.cc b/system/test/mock/mock_bta_gatts_api.cc
index 68380e4..ba86ae5 100644
--- a/system/test/mock/mock_bta_gatts_api.cc
+++ b/system/test/mock/mock_bta_gatts_api.cc
@@ -85,3 +85,4 @@
                                 BTA_GATTS_AddServiceCb cb) {
   mock_function_count_map[__func__]++;
 }
+void BTA_GATTS_InitBonded(void) { mock_function_count_map[__func__]++; }
diff --git a/system/test/mock/mock_stack_btm_dev.cc b/system/test/mock/mock_stack_btm_dev.cc
index 8a65ab8..bfd3081 100644
--- a/system/test/mock/mock_stack_btm_dev.cc
+++ b/system/test/mock/mock_stack_btm_dev.cc
@@ -112,3 +112,9 @@
 void btm_dev_consolidate_existing_connections(const RawAddress& bd_addr) {
   mock_function_count_map[__func__]++;
 }
+void BTM_SecDump(const std::string& label) {
+  mock_function_count_map[__func__]++;
+}
+void BTM_SecDumpDev(const RawAddress& bd_addr) {
+  mock_function_count_map[__func__]++;
+}