Merge "Remove media players if their backing session dissappears"
diff --git a/res/raw/silent.wav b/res/raw/silent.wav
new file mode 100755
index 0000000..d6d93ef
--- /dev/null
+++ b/res/raw/silent.wav
Binary files differ
diff --git a/src/com/android/bluetooth/a2dp/A2dpCodecConfig.java b/src/com/android/bluetooth/a2dp/A2dpCodecConfig.java
index 4de89df..918c2ce 100644
--- a/src/com/android/bluetooth/a2dp/A2dpCodecConfig.java
+++ b/src/com/android/bluetooth/a2dp/A2dpCodecConfig.java
@@ -17,13 +17,17 @@
 package com.android.bluetooth.a2dp;
 
 import android.bluetooth.BluetoothCodecConfig;
+import android.bluetooth.BluetoothCodecStatus;
 import android.bluetooth.BluetoothDevice;
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.Resources.NotFoundException;
+import android.util.Log;
 
 import com.android.bluetooth.R;
 
+import java.util.Arrays;
+import java.util.Objects;
 /*
  * A2DP Codec Configuration setup.
  */
@@ -52,13 +56,47 @@
     }
 
     void setCodecConfigPreference(BluetoothDevice device,
+                                  BluetoothCodecStatus codecStatus,
                                   BluetoothCodecConfig codecConfig) {
+        Objects.requireNonNull(codecStatus);
+
+        // Check whether the codecConfig is selectable for this Bluetooth device.
+        BluetoothCodecConfig[] selectableCodecs = codecStatus.getCodecsSelectableCapabilities();
+        if (!Arrays.asList(selectableCodecs).stream().anyMatch(codec ->
+                codec.isMandatoryCodec())) {
+            // Do not set codec preference to native if the selectableCodecs not contain mandatory
+            // codec. The reason could be remote codec negotiation is not completed yet.
+            Log.w(TAG, "Cannot find mandatory codec in selectableCodecs.");
+            return;
+        }
+        if (!isCodecConfigSelectable(codecConfig, selectableCodecs)) {
+            Log.w(TAG, "Codec is not selectable: " + codecConfig);
+            return;
+        }
+
+        // Check whether the codecConfig would change current codec config.
+        int prioritizedCodecType = getPrioitizedCodecType(codecConfig, selectableCodecs);
+        BluetoothCodecConfig currentCodecConfig = codecStatus.getCodecConfig();
+        if (prioritizedCodecType == currentCodecConfig.getCodecType()
+                && (currentCodecConfig.getCodecType() != codecConfig.getCodecType()
+                || currentCodecConfig.sameAudioFeedingParameters(codecConfig))) {
+            // Same codec with same parameters, no need to send this request to native.
+            Log.i(TAG, "setCodecConfigPreference: codec not changed.");
+            return;
+        }
+
         BluetoothCodecConfig[] codecConfigArray = new BluetoothCodecConfig[1];
         codecConfigArray[0] = codecConfig;
         mA2dpNativeInterface.setCodecConfigPreference(device, codecConfigArray);
     }
 
-    void enableOptionalCodecs(BluetoothDevice device) {
+    void enableOptionalCodecs(BluetoothDevice device, BluetoothCodecConfig currentCodecConfig) {
+        if (currentCodecConfig != null && !currentCodecConfig.isMandatoryCodec()) {
+            Log.i(TAG, "enableOptionalCodecs: already using optional codec: "
+                    + currentCodecConfig.getCodecType());
+            return;
+        }
+
         BluetoothCodecConfig[] codecConfigArray = assignCodecConfigPriorities();
         if (codecConfigArray == null) {
             return;
@@ -75,7 +113,12 @@
         mA2dpNativeInterface.setCodecConfigPreference(device, codecConfigArray);
     }
 
-    void disableOptionalCodecs(BluetoothDevice device) {
+    void disableOptionalCodecs(BluetoothDevice device, BluetoothCodecConfig currentCodecConfig) {
+        if (currentCodecConfig != null && currentCodecConfig.isMandatoryCodec()) {
+            Log.i(TAG, "disableOptionalCodecs: already using mandatory codec");
+            return;
+        }
+
         BluetoothCodecConfig[] codecConfigArray = assignCodecConfigPriorities();
         if (codecConfigArray == null) {
             return;
@@ -92,6 +135,35 @@
         mA2dpNativeInterface.setCodecConfigPreference(device, codecConfigArray);
     }
 
+    // Get the codec type of the highest priority of selectableCodecs and codecConfig.
+    private int getPrioitizedCodecType(BluetoothCodecConfig codecConfig,
+            BluetoothCodecConfig[] selectableCodecs) {
+        BluetoothCodecConfig prioritizedCodecConfig = codecConfig;
+        for (BluetoothCodecConfig config : selectableCodecs) {
+            if (prioritizedCodecConfig == null) {
+                prioritizedCodecConfig = config;
+            }
+            if (config.getCodecPriority() > prioritizedCodecConfig.getCodecPriority()) {
+                prioritizedCodecConfig = config;
+            }
+        }
+        return prioritizedCodecConfig.getCodecType();
+    }
+
+    // Check whether the codecConfig is selectable
+    private static boolean isCodecConfigSelectable(BluetoothCodecConfig codecConfig,
+            BluetoothCodecConfig[] selectableCodecs) {
+        for (BluetoothCodecConfig config : selectableCodecs) {
+            if (codecConfig.getCodecType() == config.getCodecType()
+                    && (codecConfig.getSampleRate() & config.getSampleRate()) != 0
+                    && (codecConfig.getBitsPerSample() & config.getBitsPerSample()) != 0
+                    && (codecConfig.getChannelMode() & config.getChannelMode()) != 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     // Assign the A2DP Source codec config priorities
     private BluetoothCodecConfig[] assignCodecConfigPriorities() {
         Resources resources = mContext.getResources();
diff --git a/src/com/android/bluetooth/a2dp/A2dpService.java b/src/com/android/bluetooth/a2dp/A2dpService.java
index 810a7c0..085bbb1 100644
--- a/src/com/android/bluetooth/a2dp/A2dpService.java
+++ b/src/com/android/bluetooth/a2dp/A2dpService.java
@@ -719,7 +719,17 @@
             Log.e(TAG, "Cannot set codec config preference: no active A2DP device");
             return;
         }
-        mA2dpCodecConfig.setCodecConfigPreference(device, codecConfig);
+        if (getSupportsOptionalCodecs(device) != BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED) {
+            Log.e(TAG, "Cannot set codec config preference: not supported");
+            return;
+        }
+
+        BluetoothCodecStatus codecStatus = getCodecStatus(device);
+        if (codecStatus == null) {
+            Log.e(TAG, "Codec status is null on " + device);
+            return;
+        }
+        mA2dpCodecConfig.setCodecConfigPreference(device, codecStatus, codecConfig);
     }
 
     /**
@@ -741,7 +751,16 @@
             Log.e(TAG, "Cannot enable optional codecs: no active A2DP device");
             return;
         }
-        mA2dpCodecConfig.enableOptionalCodecs(device);
+        if (getSupportsOptionalCodecs(device) != BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED) {
+            Log.e(TAG, "Cannot enable optional codecs: not supported");
+            return;
+        }
+        BluetoothCodecStatus codecStatus = getCodecStatus(device);
+        if (codecStatus == null) {
+            Log.e(TAG, "Cannot enable optional codecs: codec status is null");
+            return;
+        }
+        mA2dpCodecConfig.enableOptionalCodecs(device, codecStatus.getCodecConfig());
     }
 
     /**
@@ -763,7 +782,16 @@
             Log.e(TAG, "Cannot disable optional codecs: no active A2DP device");
             return;
         }
-        mA2dpCodecConfig.disableOptionalCodecs(device);
+        if (getSupportsOptionalCodecs(device) != BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED) {
+            Log.e(TAG, "Cannot disable optional codecs: not supported");
+            return;
+        }
+        BluetoothCodecStatus codecStatus = getCodecStatus(device);
+        if (codecStatus == null) {
+            Log.e(TAG, "Cannot disable optional codecs: codec status is null");
+            return;
+        }
+        mA2dpCodecConfig.disableOptionalCodecs(device, codecStatus.getCodecConfig());
     }
 
     public int getSupportsOptionalCodecs(BluetoothDevice device) {
@@ -835,7 +863,8 @@
      * @param sameAudioFeedingParameters if true the audio feeding parameters
      * haven't been changed
      */
-    void codecConfigUpdated(BluetoothDevice device, BluetoothCodecStatus codecStatus,
+    @VisibleForTesting
+    public void codecConfigUpdated(BluetoothDevice device, BluetoothCodecStatus codecStatus,
                             boolean sameAudioFeedingParameters) {
         // Log codec config and capability metrics
         BluetoothCodecConfig codecConfig = codecStatus.getCodecConfig();
@@ -987,9 +1016,17 @@
         }
     }
 
-    private void updateOptionalCodecsSupport(BluetoothDevice device) {
+
+    /**
+     * Update and initiate optional codec status change to native.
+     *
+     * @param device the device to change optional codec status
+     */
+    @VisibleForTesting
+    public void updateOptionalCodecsSupport(BluetoothDevice device) {
         int previousSupport = getSupportsOptionalCodecs(device);
         boolean supportsOptional = false;
+        boolean hasMandatoryCodec = false;
 
         synchronized (mStateMachines) {
             A2dpStateMachine sm = mStateMachines.get(device);
@@ -999,13 +1036,22 @@
             BluetoothCodecStatus codecStatus = sm.getCodecStatus();
             if (codecStatus != null) {
                 for (BluetoothCodecConfig config : codecStatus.getCodecsSelectableCapabilities()) {
-                    if (!config.isMandatoryCodec()) {
+                    if (config.isMandatoryCodec()) {
+                        hasMandatoryCodec = true;
+                    } else {
                         supportsOptional = true;
-                        break;
                     }
                 }
             }
         }
+        if (!hasMandatoryCodec) {
+            // Mandatory codec(SBC) is not selectable. It could be caused by the remote device
+            // select codec before native finish get codec capabilities. Stop use this codec
+            // status as the reference to support/enable optional codecs.
+            Log.i(TAG, "updateOptionalCodecsSupport: Mandatory codec is not selectable.");
+            return;
+        }
+
         if (previousSupport == BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN
                 || supportsOptional != (previousSupport
                                     == BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED)) {
@@ -1034,10 +1080,6 @@
         }
         synchronized (mStateMachines) {
             if (toState == BluetoothProfile.STATE_CONNECTED) {
-                // Each time a device connects, we want to re-check if it supports optional
-                // codecs (perhaps it's had a firmware update, etc.) and save that state if
-                // it differs from what we had saved before.
-                updateOptionalCodecsSupport(device);
                 MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.A2DP);
             }
             // Set the active device if only one connected device is supported and it was connected
diff --git a/src/com/android/bluetooth/a2dp/A2dpStateMachine.java b/src/com/android/bluetooth/a2dp/A2dpStateMachine.java
index 9c8a221..e0df8b2 100644
--- a/src/com/android/bluetooth/a2dp/A2dpStateMachine.java
+++ b/src/com/android/bluetooth/a2dp/A2dpStateMachine.java
@@ -89,7 +89,8 @@
 
     private A2dpService mA2dpService;
     private A2dpNativeInterface mA2dpNativeInterface;
-    private boolean mA2dpOffloadEnabled = false;
+    @VisibleForTesting
+    boolean mA2dpOffloadEnabled = false;
     private final BluetoothDevice mDevice;
     private boolean mIsPlaying = false;
     private BluetoothCodecStatus mCodecStatus;
@@ -467,6 +468,10 @@
 
             removeDeferredMessages(CONNECT);
 
+            // Each time a device connects, we want to re-check if it supports optional
+            // codecs (perhaps it's had a firmware update, etc.) and save that state if
+            // it differs from what we had saved before.
+            mA2dpService.updateOptionalCodecsSupport(mDevice);
             broadcastConnectionState(mConnectionState, mLastConnectionState);
             // Upon connected, the audio starts out as stopped
             broadcastAudioState(BluetoothA2dp.STATE_NOT_PLAYING,
@@ -608,8 +613,11 @@
     }
 
     // NOTE: This event is processed in any state
-    private void processCodecConfigEvent(BluetoothCodecStatus newCodecStatus) {
+    @VisibleForTesting
+    void processCodecConfigEvent(BluetoothCodecStatus newCodecStatus) {
         BluetoothCodecConfig prevCodecConfig = null;
+        BluetoothCodecStatus prevCodecStatus = mCodecStatus;
+
         synchronized (this) {
             if (mCodecStatus != null) {
                 prevCodecConfig = mCodecStatus.getCodecConfig();
@@ -630,6 +638,12 @@
             }
         }
 
+        if (isConnected() && !sameSelectableCodec(prevCodecStatus, mCodecStatus)) {
+            // Remote selectable codec could be changed if codec config changed
+            // in connected state, we need to re-check optional codec status
+            // for this codec change event.
+            mA2dpService.updateOptionalCodecsSupport(mDevice);
+        }
         if (mA2dpOffloadEnabled) {
             boolean update = false;
             BluetoothCodecConfig newCodecConfig = mCodecStatus.getCodecConfig();
@@ -696,6 +710,16 @@
         return builder.toString();
     }
 
+    private static boolean sameSelectableCodec(BluetoothCodecStatus prevCodecStatus,
+            BluetoothCodecStatus newCodecStatus) {
+        if (prevCodecStatus == null) {
+            return false;
+        }
+        return BluetoothCodecStatus.sameCapabilities(
+                prevCodecStatus.getCodecsSelectableCapabilities(),
+                newCodecStatus.getCodecsSelectableCapabilities());
+    }
+
     private static String messageWhatToString(int what) {
         switch (what) {
             case CONNECT:
diff --git a/src/com/android/bluetooth/avrcpcontroller/AvrcpPlayer.java b/src/com/android/bluetooth/avrcpcontroller/AvrcpPlayer.java
index e3cc2db..8e1af83 100644
--- a/src/com/android/bluetooth/avrcpcontroller/AvrcpPlayer.java
+++ b/src/com/android/bluetooth/avrcpcontroller/AvrcpPlayer.java
@@ -53,7 +53,8 @@
         mId = INVALID_ID;
         //Set Default Actions in case Player data isn't available.
         mAvailableActions = PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_PLAY
-            | PlaybackState.ACTION_SKIP_TO_NEXT | PlaybackState.ACTION_SKIP_TO_PREVIOUS;
+            | PlaybackState.ACTION_SKIP_TO_NEXT | PlaybackState.ACTION_SKIP_TO_PREVIOUS
+            | PlaybackState.ACTION_STOP;
     }
 
     AvrcpPlayer(int id, String name, byte[] playerFeatures, int playStatus, int playerType) {
diff --git a/src/com/android/bluetooth/avrcpcontroller/BluetoothMediaBrowserService.java b/src/com/android/bluetooth/avrcpcontroller/BluetoothMediaBrowserService.java
index 323dbfb..4dfb8d4 100644
--- a/src/com/android/bluetooth/avrcpcontroller/BluetoothMediaBrowserService.java
+++ b/src/com/android/bluetooth/avrcpcontroller/BluetoothMediaBrowserService.java
@@ -23,7 +23,10 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
 import android.media.MediaMetadata;
+import android.media.MediaPlayer;
 import android.media.browse.MediaBrowser;
 import android.media.browse.MediaBrowser.MediaItem;
 import android.media.session.MediaController;
@@ -95,8 +98,16 @@
             "com.android.bluetooth.avrcpcontroller.CUSTOM_ACTION_GET_PLAY_STATUS_NATIVE";
 
     private static BluetoothMediaBrowserService sBluetoothMediaBrowserService;
+
+    // In order to be considered as an audio source capable of receiving media key events (In the
+    // eyes of MediaSessionService), we need an active MediaPlayer in addition to a MediaSession.
+    // Because of this, the media player below plays an incredibly short, silent audio sample so
+    // that MediaSessionService and AudioPlaybackStateMonitor will believe that we're the current
+    // active player and send us media events. This is a restriction currently imposed by the media
+    // framework code and could be reconsidered in the future.
     private MediaSession mSession;
     private MediaMetadata mA2dpMetadata;
+    private MediaPlayer mMediaPlayer;
 
     private AvrcpControllerService mAvrcpCtrlSrvc;
     private boolean mBrowseConnected = false;
@@ -158,23 +169,37 @@
         }
     }
 
+    /**
+     * Initialize this BluetoothMediaBrowserService, creating our MediaSession, MediaPlayer and
+     * MediaMetaData, and setting up mechanisms to talk with the AvrcpControllerService.
+     */
     @Override
     public void onCreate() {
         if (DBG) Log.d(TAG, "onCreate");
         super.onCreate();
 
+        // Create and configure the MediaSession
         mSession = new MediaSession(this, TAG);
-        setSessionToken(mSession.getSessionToken());
         mSession.setCallback(mSessionCallbacks);
         mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS
                 | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
-        mSession.setActive(true);
         mSession.setQueueTitle(getString(R.string.bluetooth_a2dp_sink_queue_name));
         mSession.setQueue(mMediaQueue);
+
+        // Create and setup the MediaPlayer
+        initMediaPlayer();
+
+        // Associate the held MediaSession with this browser and activate it
+        setSessionToken(mSession.getSessionToken());
+        mSession.setActive(true);
+
+        // Internal handler to process events and requests
         mAvrcpCommandQueue = new AvrcpCommandQueueHandler(Looper.getMainLooper(), this);
 
+        // Set the initial Media state (sets current playback state and media meta data)
         refreshInitialPlayingState();
 
+        // Set up communication with the controller service
         IntentFilter filter = new IntentFilter();
         filter.addAction(BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED);
         filter.addAction(AvrcpControllerService.ACTION_BROWSE_CONNECTION_STATE_CHANGED);
@@ -185,19 +210,82 @@
         synchronized (this) {
             mParentIdToRequestMap.clear();
         }
-        setBluetoothMediaBrowserService(this);
 
+        setBluetoothMediaBrowserService(this);
     }
 
+    /**
+     * Clean up this instance in the reverse order that we created it.
+     */
     @Override
     public void onDestroy() {
         if (DBG) Log.d(TAG, "onDestroy");
         setBluetoothMediaBrowserService(null);
-        mSession.release();
         unregisterReceiver(mBtReceiver);
+        destroyMediaPlayer();
+        mSession.release();
         super.onDestroy();
     }
 
+    /**
+     * Initializes the silent MediaPlayer object which aids in receiving media key focus.
+     *
+     * The created MediaPlayer is already prepared and will release and stop itself on error. All
+     * you need to do is start() it.
+     */
+    private void initMediaPlayer() {
+        if (DBG) Log.d(TAG, "initMediaPlayer()");
+
+        // Parameters for create
+        AudioAttributes attrs = new AudioAttributes.Builder()
+                .setUsage(AudioAttributes.USAGE_MEDIA)
+                .build();
+        AudioManager am = getSystemService(AudioManager.class);
+
+        // Create our player object. Returns a prepared player on success, null on failure
+        mMediaPlayer = MediaPlayer.create(this, R.raw.silent, attrs, am.generateAudioSessionId());
+        if (mMediaPlayer == null) {
+            Log.e(TAG, "Failed to initialize media player. You may not get media key events");
+            return;
+        }
+
+        // Set other player attributes
+        mMediaPlayer.setLooping(false);
+        mMediaPlayer.setOnErrorListener((mp, what, extra) -> {
+            Log.e(TAG, "Silent media player error: " + what + ", " + extra);
+            destroyMediaPlayer();
+            return false;
+        });
+    }
+
+    /**
+     * Safely tears down our local MediaPlayer
+     */
+    private void destroyMediaPlayer() {
+        if (DBG) Log.d(TAG, "destroyMediaPlayer()");
+        if (mMediaPlayer == null) {
+            return;
+        }
+        mMediaPlayer.stop();
+        mMediaPlayer.release();
+        mMediaPlayer = null;
+    }
+
+    /**
+     * Uses the internal MediaPlayer to play a silent, short audio sample so that AudioService will
+     * treat us as the active MediaSession/MediaPlayer combo and properly route us media key events.
+     *
+     * If the MediaPlayer failed to initialize properly, this call will fail gracefully and log the
+     * failed attempt. Media keys will not be routed.
+     */
+    private void getMediaKeyFocus() {
+        if (DBG) Log.d(TAG, "getMediaKeyFocus()");
+        if (mMediaPlayer == null) {
+            Log.w(TAG, "Media player is null. Can't get media key focus. Media keys may not route");
+            return;
+        }
+        mMediaPlayer.start();
+    }
 
     /**
      *  getBluetoothMediaBrowserService()
@@ -311,6 +399,7 @@
             if (DBG) Log.d(TAG, "onPrepare");
             if (mA2dpSinkService != null) {
                 mA2dpSinkService.requestAudioFocus(mA2dpDevice, true);
+                getMediaKeyFocus();
             }
         }
 
@@ -336,6 +425,7 @@
                 // Play the item if possible.
                 if (mA2dpSinkService != null) {
                     mA2dpSinkService.requestAudioFocus(mA2dpDevice, true);
+                    getMediaKeyFocus();
                 }
                 mAvrcpCtrlSrvc.fetchAttrAndPlayItem(mA2dpDevice, mediaId);
             }
diff --git a/src/com/android/bluetooth/btservice/RemoteDevices.java b/src/com/android/bluetooth/btservice/RemoteDevices.java
index 6d0c430..ee7fb58 100644
--- a/src/com/android/bluetooth/btservice/RemoteDevices.java
+++ b/src/com/android/bluetooth/btservice/RemoteDevices.java
@@ -455,6 +455,7 @@
         intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
         intent.putExtra(BluetoothDevice.EXTRA_BATTERY_LEVEL, batteryLevel);
         intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+        intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
         sAdapterService.sendBroadcast(intent, AdapterService.BLUETOOTH_PERM);
     }
 
diff --git a/src/com/android/bluetooth/btservice/storage/DatabaseManager.java b/src/com/android/bluetooth/btservice/storage/DatabaseManager.java
index 17f094a..00fd7ba 100644
--- a/src/com/android/bluetooth/btservice/storage/DatabaseManager.java
+++ b/src/com/android/bluetooth/btservice/storage/DatabaseManager.java
@@ -38,6 +38,7 @@
 import com.android.bluetooth.btservice.AdapterService;
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -586,17 +587,15 @@
         BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices();
         synchronized (mMetadataCache) {
             mMetadataCache.forEach((address, metadata) -> {
-                for (BluetoothDevice device : bondedDevices) {
-                    if (!device.getAddress().equals(address)
-                            && !address.equals(LOCAL_STORAGE)) {
-                        // Report metadata change to null
-                        List<Integer> list = metadata.getChangedCustomizedMeta();
-                        for (int key : list) {
-                            mAdapterService.metadataChanged(address, key, null);
-                        }
-                        Log.i(TAG, "remove unpaired device from database " + address);
-                        deleteDatabase(mMetadataCache.get(address));
+                if (!address.equals(LOCAL_STORAGE)
+                        && !Arrays.asList(bondedDevices).stream().anyMatch(device ->
+                        address.equals(device.getAddress()))) {
+                    List<Integer> list = metadata.getChangedCustomizedMeta();
+                    for (int key : list) {
+                        mAdapterService.metadataChanged(address, key, null);
                     }
+                    Log.i(TAG, "remove unpaired device from database " + address);
+                    deleteDatabase(mMetadataCache.get(address));
                 }
             });
         }
diff --git a/src/com/android/bluetooth/gatt/ContextMap.java b/src/com/android/bluetooth/gatt/ContextMap.java
index 96022df..d54a25a 100644
--- a/src/com/android/bluetooth/gatt/ContextMap.java
+++ b/src/com/android/bluetooth/gatt/ContextMap.java
@@ -89,6 +89,9 @@
         /** Whether the calling app has location permission */
         boolean hasLocationPermission;
 
+        /** Whether the calling app has bluetooth privileged permission */
+        boolean hasBluetoothPrivilegedPermission;
+
         /** The user handle of the app that started the scan */
         UserHandle mUserHandle;
 
diff --git a/src/com/android/bluetooth/gatt/GattService.java b/src/com/android/bluetooth/gatt/GattService.java
index 1323bb1..8a3a1aa 100644
--- a/src/com/android/bluetooth/gatt/GattService.java
+++ b/src/com/android/bluetooth/gatt/GattService.java
@@ -300,6 +300,20 @@
                 == PERMISSION_GRANTED);
     }
 
+    private boolean permissionCheck(ClientMap.App app, int connId, int handle) {
+        Set<Integer> restrictedHandles = mRestrictedHandles.get(connId);
+        if (restrictedHandles == null || !restrictedHandles.contains(handle)) {
+            return true;
+        }
+
+        if (!app.hasBluetoothPrivilegedPermission
+                && checkCallingOrSelfPermission(BLUETOOTH_PRIVILEGED)== PERMISSION_GRANTED) {
+            app.hasBluetoothPrivilegedPermission = true;
+        }
+
+        return app.hasBluetoothPrivilegedPermission;
+    }
+
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
         if (GattDebugUtils.handleDebugAction(this, intent)) {
@@ -1390,13 +1404,12 @@
                     + data.length);
         }
 
-        if (!permissionCheck(connId, handle)) {
-            Log.w(TAG, "onNotify() - permission check failed!");
-            return;
-        }
-
         ClientMap.App app = mClientMap.getByConnId(connId);
         if (app != null) {
+            if (!permissionCheck(app, connId, handle)) {
+                Log.w(TAG, "onNotify() - permission check failed!");
+                return;
+            }
             app.callback.onNotify(address, handle, data);
         }
     }
diff --git a/src/com/android/bluetooth/pbapclient/PbapClientConnectionHandler.java b/src/com/android/bluetooth/pbapclient/PbapClientConnectionHandler.java
index 5c3fa4f..6d14e59 100644
--- a/src/com/android/bluetooth/pbapclient/PbapClientConnectionHandler.java
+++ b/src/com/android/bluetooth/pbapclient/PbapClientConnectionHandler.java
@@ -195,17 +195,17 @@
                     }
                 } else {
                     Log.w(TAG, "Socket CONNECT Failure ");
-                    mPbapClientStateMachine.obtainMessage(
-                            PbapClientStateMachine.MSG_CONNECTION_FAILED).sendToTarget();
+                    mPbapClientStateMachine.sendMessage(
+                            PbapClientStateMachine.MSG_CONNECTION_FAILED);
                     return;
                 }
 
                 if (connectObexSession()) {
-                    mPbapClientStateMachine.obtainMessage(
-                            PbapClientStateMachine.MSG_CONNECTION_COMPLETE).sendToTarget();
+                    mPbapClientStateMachine.sendMessage(
+                            PbapClientStateMachine.MSG_CONNECTION_COMPLETE);
                 } else {
-                    mPbapClientStateMachine.obtainMessage(
-                            PbapClientStateMachine.MSG_CONNECTION_FAILED).sendToTarget();
+                    mPbapClientStateMachine.sendMessage(
+                            PbapClientStateMachine.MSG_CONNECTION_FAILED);
                 }
                 break;
 
@@ -235,8 +235,7 @@
                 removeAccount(mAccount);
                 removeCallLog(mAccount);
 
-                mPbapClientStateMachine.obtainMessage(PbapClientStateMachine.MSG_CONNECTION_CLOSED)
-                    .sendToTarget();
+                mPbapClientStateMachine.sendMessage(PbapClientStateMachine.MSG_CONNECTION_CLOSED);
                 break;
 
             case MSG_DOWNLOAD:
diff --git a/tests/unit/src/com/android/bluetooth/a2dp/A2dpCodecConfigTest.java b/tests/unit/src/com/android/bluetooth/a2dp/A2dpCodecConfigTest.java
new file mode 100644
index 0000000..4e6c351
--- /dev/null
+++ b/tests/unit/src/com/android/bluetooth/a2dp/A2dpCodecConfigTest.java
@@ -0,0 +1,414 @@
+/*
+ * Copyright 2019 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.a2dp;
+
+import static org.mockito.Mockito.*;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothCodecConfig;
+import android.bluetooth.BluetoothCodecStatus;
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+
+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 A2dpCodecConfigTest {
+    private Context mTargetContext;
+    private A2dpCodecConfig mA2dpCodecConfig;
+    private BluetoothAdapter mAdapter;
+    private BluetoothCodecConfig mCodecConfigSbc;
+    private BluetoothCodecConfig mCodecConfigAac;
+    private BluetoothCodecConfig mCodecConfigAptx;
+    private BluetoothCodecConfig mCodecConfigAptxHd;
+    private BluetoothCodecConfig mCodecConfigLdac;
+    private BluetoothDevice mTestDevice;
+
+    @Mock private A2dpNativeInterface mA2dpNativeInterface;
+
+    static final int SBC_PRIORITY_DEFAULT = 1001;
+    static final int AAC_PRIORITY_DEFAULT = 2001;
+    static final int APTX_PRIORITY_DEFAULT = 3001;
+    static final int APTX_HD_PRIORITY_DEFAULT = 4001;
+    static final int LDAC_PRIORITY_DEFAULT = 5001;
+    static final int PRIORITY_HIGH = 1000000;
+
+
+    @Before
+    public void setUp() throws Exception {
+        mTargetContext = InstrumentationRegistry.getTargetContext();
+        // Set up mocks and test assets
+        MockitoAnnotations.initMocks(this);
+
+        mA2dpCodecConfig = new A2dpCodecConfig(mTargetContext, mA2dpNativeInterface);
+        mTestDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice("00:01:02:03:04:05");
+
+        doReturn(true).when(mA2dpNativeInterface).setCodecConfigPreference(
+                any(BluetoothDevice.class),
+                any(BluetoothCodecConfig[].class));
+
+        // Create sample codec configs
+        mCodecConfigSbc = new BluetoothCodecConfig(
+            BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+            SBC_PRIORITY_DEFAULT,
+            BluetoothCodecConfig.SAMPLE_RATE_44100,
+            BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+            BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+            0, 0, 0, 0);       // Codec-specific fields
+        mCodecConfigAac = new BluetoothCodecConfig(
+            BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC,
+            AAC_PRIORITY_DEFAULT,
+            BluetoothCodecConfig.SAMPLE_RATE_44100,
+            BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+            BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+            0, 0, 0, 0);       // Codec-specific fields
+        mCodecConfigAptx = new BluetoothCodecConfig(
+            BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX,
+            APTX_PRIORITY_DEFAULT,
+            BluetoothCodecConfig.SAMPLE_RATE_44100,
+            BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+            BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+            0, 0, 0, 0);       // Codec-specific fields
+        mCodecConfigAptxHd = new BluetoothCodecConfig(
+            BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD,
+            APTX_HD_PRIORITY_DEFAULT,
+            BluetoothCodecConfig.SAMPLE_RATE_44100,
+            BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+            BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+            0, 0, 0, 0);       // Codec-specific fields
+        mCodecConfigLdac = new BluetoothCodecConfig(
+            BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+            LDAC_PRIORITY_DEFAULT,
+            BluetoothCodecConfig.SAMPLE_RATE_44100,
+            BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+            BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+            0, 0, 0, 0);       // Codec-specific fields
+
+    }
+
+    @After
+    public void tearDown() throws Exception {
+    }
+
+    @Test
+    public void testAssignCodecConfigPriorities() {
+        BluetoothCodecConfig[] codecConfigs = mA2dpCodecConfig.codecConfigPriorities();
+        for (BluetoothCodecConfig config : codecConfigs) {
+            switch(config.getCodecType()) {
+                case BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC:
+                    Assert.assertEquals(config.getCodecPriority(), SBC_PRIORITY_DEFAULT);
+                    break;
+                case BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC:
+                    Assert.assertEquals(config.getCodecPriority(), AAC_PRIORITY_DEFAULT);
+                    break;
+                case BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX:
+                    Assert.assertEquals(config.getCodecPriority(), APTX_PRIORITY_DEFAULT);
+                    break;
+                case BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD:
+                    Assert.assertEquals(config.getCodecPriority(), APTX_HD_PRIORITY_DEFAULT);
+                    break;
+                case BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC:
+                    Assert.assertEquals(config.getCodecPriority(), LDAC_PRIORITY_DEFAULT);
+                    break;
+            }
+        }
+    }
+
+    @Test
+    public void testSetCodecPreference_priorityHighToDefault() {
+        testSetCodecPreference_codecPriorityChangeCase(
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC, SBC_PRIORITY_DEFAULT,
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC, PRIORITY_HIGH,
+                true);
+        testSetCodecPreference_codecPriorityChangeCase(
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC, AAC_PRIORITY_DEFAULT,
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC, PRIORITY_HIGH,
+                true);
+        testSetCodecPreference_codecPriorityChangeCase(
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX, APTX_PRIORITY_DEFAULT,
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX, PRIORITY_HIGH,
+                true);
+        testSetCodecPreference_codecPriorityChangeCase(
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD, APTX_HD_PRIORITY_DEFAULT,
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD, PRIORITY_HIGH,
+                true);
+        testSetCodecPreference_codecPriorityChangeCase(
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC, LDAC_PRIORITY_DEFAULT,
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC, PRIORITY_HIGH,
+                false);
+    }
+
+    @Test
+    public void testSetCodecPreference_priorityDefaultToHigh() {
+        testSetCodecPreference_codecPriorityChangeCase(
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC, PRIORITY_HIGH,
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC, LDAC_PRIORITY_DEFAULT,
+                true);
+        testSetCodecPreference_codecPriorityChangeCase(
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC, PRIORITY_HIGH,
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC, LDAC_PRIORITY_DEFAULT,
+                true);
+        testSetCodecPreference_codecPriorityChangeCase(
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX, PRIORITY_HIGH,
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC, LDAC_PRIORITY_DEFAULT,
+                true);
+        testSetCodecPreference_codecPriorityChangeCase(
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD, PRIORITY_HIGH,
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC, LDAC_PRIORITY_DEFAULT,
+                true);
+        testSetCodecPreference_codecPriorityChangeCase(
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC, PRIORITY_HIGH,
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC, LDAC_PRIORITY_DEFAULT,
+                false);
+    }
+
+    @Test
+    public void testSetCodecPreference_priorityHighToHigh() {
+        testSetCodecPreference_codecPriorityChangeCase(
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC, PRIORITY_HIGH,
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC, PRIORITY_HIGH,
+                false);
+        testSetCodecPreference_codecPriorityChangeCase(
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC, PRIORITY_HIGH,
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC, PRIORITY_HIGH,
+                true);
+        testSetCodecPreference_codecPriorityChangeCase(
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX, PRIORITY_HIGH,
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC, PRIORITY_HIGH,
+                true);
+        testSetCodecPreference_codecPriorityChangeCase(
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD, PRIORITY_HIGH,
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC, PRIORITY_HIGH,
+                true);
+        testSetCodecPreference_codecPriorityChangeCase(
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC, PRIORITY_HIGH,
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC, PRIORITY_HIGH,
+                true);
+    }
+
+    @Test
+    public void testSetCodecPreference_parametersChange() {
+        int unSupportedParameter = 200;
+
+        testSetCodecPreference_parametersChangeCase(
+                BluetoothCodecConfig.SAMPLE_RATE_44100,
+                BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+                false);
+        testSetCodecPreference_parametersChangeCase(
+                BluetoothCodecConfig.SAMPLE_RATE_44100,
+                BluetoothCodecConfig.BITS_PER_SAMPLE_24,
+                true);
+        testSetCodecPreference_parametersChangeCase(
+                BluetoothCodecConfig.SAMPLE_RATE_44100,
+                unSupportedParameter,
+                false);
+
+        testSetCodecPreference_parametersChangeCase(
+                BluetoothCodecConfig.SAMPLE_RATE_48000,
+                BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+                true);
+        testSetCodecPreference_parametersChangeCase(
+                BluetoothCodecConfig.SAMPLE_RATE_48000,
+                BluetoothCodecConfig.BITS_PER_SAMPLE_24,
+                true);
+        testSetCodecPreference_parametersChangeCase(
+                BluetoothCodecConfig.SAMPLE_RATE_48000,
+                unSupportedParameter,
+                false);
+
+        testSetCodecPreference_parametersChangeCase(
+                unSupportedParameter,
+                BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+                false);
+        testSetCodecPreference_parametersChangeCase(
+                unSupportedParameter,
+                BluetoothCodecConfig.BITS_PER_SAMPLE_24,
+                false);
+        testSetCodecPreference_parametersChangeCase(
+                unSupportedParameter,
+                unSupportedParameter,
+                false);
+    }
+
+    @Test
+    public void testDisableOptionalCodecs() {
+        int verifyCount = 0;
+        BluetoothCodecConfig[] codecConfigArray =
+                new BluetoothCodecConfig[BluetoothCodecConfig.SOURCE_CODEC_TYPE_MAX];
+        codecConfigArray[0] = new BluetoothCodecConfig(
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+                BluetoothCodecConfig.CODEC_PRIORITY_HIGHEST,
+                BluetoothCodecConfig.SAMPLE_RATE_NONE,
+                BluetoothCodecConfig.BITS_PER_SAMPLE_NONE,
+                BluetoothCodecConfig.CHANNEL_MODE_NONE,
+                0, 0, 0, 0);       // Codec-specific fields
+
+        // Test don't invoke to native when current codec is SBC
+        mA2dpCodecConfig.disableOptionalCodecs(mTestDevice, mCodecConfigSbc);
+        verify(mA2dpNativeInterface, times(verifyCount))
+                .setCodecConfigPreference(mTestDevice, codecConfigArray);
+
+        // Test invoke to native when current codec is not SBC
+        mA2dpCodecConfig.disableOptionalCodecs(mTestDevice, mCodecConfigAac);
+        verify(mA2dpNativeInterface, times(++verifyCount))
+                .setCodecConfigPreference(mTestDevice, codecConfigArray);
+        mA2dpCodecConfig.disableOptionalCodecs(mTestDevice, mCodecConfigAptx);
+        verify(mA2dpNativeInterface, times(++verifyCount))
+                .setCodecConfigPreference(mTestDevice, codecConfigArray);
+        mA2dpCodecConfig.disableOptionalCodecs(mTestDevice, mCodecConfigAptxHd);
+        verify(mA2dpNativeInterface, times(++verifyCount))
+                .setCodecConfigPreference(mTestDevice, codecConfigArray);
+        mA2dpCodecConfig.disableOptionalCodecs(mTestDevice, mCodecConfigLdac);
+        verify(mA2dpNativeInterface, times(++verifyCount))
+                .setCodecConfigPreference(mTestDevice, codecConfigArray);
+    }
+
+    @Test
+    public void testEnableOptionalCodecs() {
+        int verifyCount = 0;
+        BluetoothCodecConfig[] codecConfigArray =
+                new BluetoothCodecConfig[BluetoothCodecConfig.SOURCE_CODEC_TYPE_MAX];
+        codecConfigArray[0] = new BluetoothCodecConfig(
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+                SBC_PRIORITY_DEFAULT,
+                BluetoothCodecConfig.SAMPLE_RATE_NONE,
+                BluetoothCodecConfig.BITS_PER_SAMPLE_NONE,
+                BluetoothCodecConfig.CHANNEL_MODE_NONE,
+                0, 0, 0, 0);       // Codec-specific fields
+
+        // Test invoke to native when current codec is SBC
+        mA2dpCodecConfig.enableOptionalCodecs(mTestDevice, mCodecConfigSbc);
+        verify(mA2dpNativeInterface, times(++verifyCount))
+                .setCodecConfigPreference(mTestDevice, codecConfigArray);
+
+        // Test don't invoke to native when current codec is not SBC
+        mA2dpCodecConfig.enableOptionalCodecs(mTestDevice, mCodecConfigAac);
+        verify(mA2dpNativeInterface, times(verifyCount))
+                .setCodecConfigPreference(mTestDevice, codecConfigArray);
+        mA2dpCodecConfig.enableOptionalCodecs(mTestDevice, mCodecConfigAptx);
+        verify(mA2dpNativeInterface, times(verifyCount))
+                .setCodecConfigPreference(mTestDevice, codecConfigArray);
+        mA2dpCodecConfig.enableOptionalCodecs(mTestDevice, mCodecConfigAptxHd);
+        verify(mA2dpNativeInterface, times(verifyCount))
+                .setCodecConfigPreference(mTestDevice, codecConfigArray);
+        mA2dpCodecConfig.enableOptionalCodecs(mTestDevice, mCodecConfigLdac);
+        verify(mA2dpNativeInterface, times(verifyCount))
+                .setCodecConfigPreference(mTestDevice, codecConfigArray);
+    }
+
+    private void testSetCodecPreference_parametersChangeCase(int sampleRate, int bitPerSample,
+            boolean invokeNative) {
+        BluetoothCodecConfig[] invalidSelectableCodecs = new BluetoothCodecConfig[1];
+        invalidSelectableCodecs[0] = new BluetoothCodecConfig(
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+                LDAC_PRIORITY_DEFAULT,
+                (BluetoothCodecConfig.SAMPLE_RATE_44100 | BluetoothCodecConfig.SAMPLE_RATE_48000),
+                (BluetoothCodecConfig.BITS_PER_SAMPLE_16 | BluetoothCodecConfig.BITS_PER_SAMPLE_24),
+                BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+                0, 0, 0, 0);       // Codec-specific fields
+
+
+        BluetoothCodecConfig[] selectableCodecs = new BluetoothCodecConfig[2];
+        selectableCodecs[0] = mCodecConfigSbc;
+        selectableCodecs[1] = new BluetoothCodecConfig(
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+                LDAC_PRIORITY_DEFAULT,
+                (BluetoothCodecConfig.SAMPLE_RATE_44100 | BluetoothCodecConfig.SAMPLE_RATE_48000),
+                (BluetoothCodecConfig.BITS_PER_SAMPLE_16 | BluetoothCodecConfig.BITS_PER_SAMPLE_24),
+                BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+                0, 0, 0, 0);       // Codec-specific fields
+
+        BluetoothCodecConfig[] codecConfigArray = new BluetoothCodecConfig[1];
+        codecConfigArray[0] = new BluetoothCodecConfig(
+                BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+                LDAC_PRIORITY_DEFAULT,
+                sampleRate,
+                bitPerSample,
+                BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+                0, 0, 0, 0);       // Codec-specific fields
+
+        BluetoothCodecStatus codecStatus = new BluetoothCodecStatus(mCodecConfigLdac,
+                                                                    invalidSelectableCodecs,
+                                                                    invalidSelectableCodecs);
+        mA2dpCodecConfig.setCodecConfigPreference(mTestDevice, codecStatus, codecConfigArray[0]);
+
+        codecStatus = new BluetoothCodecStatus(mCodecConfigLdac,
+                                               selectableCodecs,
+                                               selectableCodecs);
+
+        mA2dpCodecConfig.setCodecConfigPreference(mTestDevice, codecStatus, codecConfigArray[0]);
+        verify(mA2dpNativeInterface, times(invokeNative ? 1 : 0))
+                .setCodecConfigPreference(mTestDevice, codecConfigArray);
+
+    }
+
+    private void testSetCodecPreference_codecPriorityChangeCase(int newCodecType,
+            int newCodecPriority, int oldCodecType, int oldCodecPriority, boolean invokeNative) {
+        BluetoothCodecConfig[] selectableCodecs = new BluetoothCodecConfig[5];
+        selectableCodecs[0] = mCodecConfigSbc;
+        selectableCodecs[1] = mCodecConfigAac;
+        selectableCodecs[2] = mCodecConfigAptx;
+        selectableCodecs[3] = mCodecConfigAptxHd;
+        selectableCodecs[4] = mCodecConfigLdac;
+
+        BluetoothCodecConfig[] invalidSelectableCodecs = new BluetoothCodecConfig[4];
+        invalidSelectableCodecs[0] = mCodecConfigAac;
+        invalidSelectableCodecs[1] = mCodecConfigAptx;
+        invalidSelectableCodecs[2] = mCodecConfigAptxHd;
+        invalidSelectableCodecs[3] = mCodecConfigLdac;
+
+        BluetoothCodecConfig oldCodecConfig =  new BluetoothCodecConfig(
+                oldCodecType,
+                oldCodecPriority,
+                BluetoothCodecConfig.SAMPLE_RATE_44100,
+                BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+                BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+                0, 0, 0, 0);       // Codec-specific fields
+
+        BluetoothCodecConfig[] codecConfigArray = new BluetoothCodecConfig[1];
+        codecConfigArray[0] = new BluetoothCodecConfig(
+            newCodecType,
+            newCodecPriority,
+            BluetoothCodecConfig.SAMPLE_RATE_44100,
+            BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+            BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+            0, 0, 0, 0);       // Codec-specific fields
+
+        BluetoothCodecStatus codecStatus = new BluetoothCodecStatus(oldCodecConfig,
+                                                                    invalidSelectableCodecs,
+                                                                    invalidSelectableCodecs);
+        mA2dpCodecConfig.setCodecConfigPreference(mTestDevice, codecStatus, codecConfigArray[0]);
+
+        codecStatus = new BluetoothCodecStatus(oldCodecConfig,
+                                               selectableCodecs,
+                                               selectableCodecs);
+
+        mA2dpCodecConfig.setCodecConfigPreference(mTestDevice, codecStatus, codecConfigArray[0]);
+        verify(mA2dpNativeInterface, times(invokeNative ? 1 : 0))
+                .setCodecConfigPreference(mTestDevice, codecConfigArray);
+    }
+}
diff --git a/tests/unit/src/com/android/bluetooth/a2dp/A2dpServiceTest.java b/tests/unit/src/com/android/bluetooth/a2dp/A2dpServiceTest.java
index af872d1..9c01854 100644
--- a/tests/unit/src/com/android/bluetooth/a2dp/A2dpServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/a2dp/A2dpServiceTest.java
@@ -1073,7 +1073,6 @@
                         BluetoothCodecConfig.BITS_PER_SAMPLE_16,
                         BluetoothCodecConfig.CHANNEL_MODE_STEREO,
                         0, 0, 0, 0);       // Codec-specific fields
-        BluetoothCodecConfig codecConfig = codecConfigSbc;
 
         BluetoothCodecConfig[] codecsLocalCapabilities;
         BluetoothCodecConfig[] codecsSelectableCapabilities;
@@ -1090,24 +1089,36 @@
             codecsLocalCapabilities[0] = codecConfigSbc;
             codecsSelectableCapabilities[0] = codecConfigSbc;
         }
-        BluetoothCodecStatus codecStatus = new BluetoothCodecStatus(codecConfig,
-                                                                    codecsLocalCapabilities,
-                                                                    codecsSelectableCapabilities);
+        BluetoothCodecConfig[] badCodecsSelectableCapabilities;
+        badCodecsSelectableCapabilities = new BluetoothCodecConfig[1];
+        badCodecsSelectableCapabilities[0] = codecConfigAac;
+
+        BluetoothCodecStatus codecStatus = new BluetoothCodecStatus(codecConfigSbc,
+                codecsLocalCapabilities, codecsSelectableCapabilities);
+        BluetoothCodecStatus badCodecStatus = new BluetoothCodecStatus(codecConfigAac,
+                codecsLocalCapabilities, badCodecsSelectableCapabilities);
 
         when(mDatabaseManager.getA2dpSupportsOptionalCodecs(mTestDevice))
                 .thenReturn(previousSupport);
         when(mDatabaseManager.getA2dpOptionalCodecsEnabled(mTestDevice))
                 .thenReturn(previousEnabled);
-        connectDeviceWithCodecStatus(mTestDevice, codecStatus);
 
+        // Generate connection request from native with bad codec status
+        connectDeviceWithCodecStatus(mTestDevice, badCodecStatus);
+        generateConnectionMessageFromNative(mTestDevice, BluetoothProfile.STATE_DISCONNECTED,
+                BluetoothProfile.STATE_CONNECTED);
+
+        // Generate connection request from native with good codec status
+        connectDeviceWithCodecStatus(mTestDevice, codecStatus);
+        generateConnectionMessageFromNative(mTestDevice, BluetoothProfile.STATE_DISCONNECTED,
+                BluetoothProfile.STATE_CONNECTED);
+
+        // Check optional codec status is set properly
         verify(mDatabaseManager, times(verifyNotSupportTime)).setA2dpSupportsOptionalCodecs(
                 mTestDevice, BluetoothA2dp.OPTIONAL_CODECS_NOT_SUPPORTED);
         verify(mDatabaseManager, times(verifySupportTime)).setA2dpSupportsOptionalCodecs(
                 mTestDevice, BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED);
         verify(mDatabaseManager, times(verifyEnabledTime)).setA2dpOptionalCodecsEnabled(
                 mTestDevice, BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED);
-
-        generateConnectionMessageFromNative(mTestDevice, BluetoothProfile.STATE_DISCONNECTED,
-                                            BluetoothProfile.STATE_CONNECTED);
     }
 }
diff --git a/tests/unit/src/com/android/bluetooth/a2dp/A2dpStateMachineTest.java b/tests/unit/src/com/android/bluetooth/a2dp/A2dpStateMachineTest.java
index 0b825bf..b8b69c0 100644
--- a/tests/unit/src/com/android/bluetooth/a2dp/A2dpStateMachineTest.java
+++ b/tests/unit/src/com/android/bluetooth/a2dp/A2dpStateMachineTest.java
@@ -20,6 +20,8 @@
 
 import android.bluetooth.BluetoothA2dp;
 import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothCodecConfig;
+import android.bluetooth.BluetoothCodecStatus;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothProfile;
 import android.content.Context;
@@ -54,6 +56,9 @@
     private BluetoothDevice mTestDevice;
     private static final int TIMEOUT_MS = 1000;    // 1s
 
+    private BluetoothCodecConfig mCodecConfigSbc;
+    private BluetoothCodecConfig mCodecConfigAac;
+
     @Mock private AdapterService mAdapterService;
     @Mock private A2dpService mA2dpService;
     @Mock private A2dpNativeInterface mA2dpNativeInterface;
@@ -72,6 +77,22 @@
         // Get a device for testing
         mTestDevice = mAdapter.getRemoteDevice("00:01:02:03:04:05");
 
+        // Set up sample codec config
+        mCodecConfigSbc = new BluetoothCodecConfig(
+            BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+            BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+            BluetoothCodecConfig.SAMPLE_RATE_44100,
+            BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+            BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+            0, 0, 0, 0);       // Codec-specific fields
+        mCodecConfigAac = new BluetoothCodecConfig(
+            BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC,
+            BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+            BluetoothCodecConfig.SAMPLE_RATE_48000,
+            BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+            BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+            0, 0, 0, 0);       // Codec-specific fields
+
         // Set up thread and looper
         mHandlerThread = new HandlerThread("A2dpStateMachineTestHandlerThread");
         mHandlerThread.start();
@@ -255,4 +276,87 @@
         Assert.assertThat(mA2dpStateMachine.getCurrentState(),
                 IsInstanceOf.instanceOf(A2dpStateMachine.Disconnected.class));
     }
+
+    /**
+     * Test that codec config change been reported to A2dpService properly.
+     */
+    @Test
+    public void testProcessCodecConfigEvent() {
+        testProcessCodecConfigEventCase(false);
+    }
+
+    /**
+     * Test that codec config change been reported to A2dpService properly when
+     * A2DP hardware offloading is enabled.
+     */
+    @Test
+    public void testProcessCodecConfigEvent_OffloadEnabled() {
+        testProcessCodecConfigEventCase(true);
+    }
+
+    /**
+     * Helper methold to test processCodecConfigEvent()
+     */
+    public void testProcessCodecConfigEventCase(boolean offloadEnabled) {
+        if (offloadEnabled) {
+            mA2dpStateMachine.mA2dpOffloadEnabled = true;
+        }
+
+        doNothing().when(mA2dpService).codecConfigUpdated(any(BluetoothDevice.class),
+                any(BluetoothCodecStatus.class), anyBoolean());
+        doNothing().when(mA2dpService).updateOptionalCodecsSupport(any(BluetoothDevice.class));
+        allowConnection(true);
+
+        BluetoothCodecConfig[] codecsSelectableSbc;
+        codecsSelectableSbc = new BluetoothCodecConfig[1];
+        codecsSelectableSbc[0] = mCodecConfigSbc;
+
+        BluetoothCodecConfig[] codecsSelectableSbcAac;
+        codecsSelectableSbcAac = new BluetoothCodecConfig[2];
+        codecsSelectableSbcAac[0] = mCodecConfigSbc;
+        codecsSelectableSbcAac[1] = mCodecConfigAac;
+
+        BluetoothCodecStatus codecStatusSbcAndSbc = new BluetoothCodecStatus(mCodecConfigSbc,
+                codecsSelectableSbcAac, codecsSelectableSbc);
+        BluetoothCodecStatus codecStatusSbcAndSbcAac = new BluetoothCodecStatus(mCodecConfigSbc,
+                codecsSelectableSbcAac, codecsSelectableSbcAac);
+        BluetoothCodecStatus codecStatusAacAndSbcAac = new BluetoothCodecStatus(mCodecConfigAac,
+                codecsSelectableSbcAac, codecsSelectableSbcAac);
+
+        // Set default codec status when device disconnected
+        // Selected codec = SBC, selectable codec = SBC
+        mA2dpStateMachine.processCodecConfigEvent(codecStatusSbcAndSbc);
+        verify(mA2dpService).codecConfigUpdated(mTestDevice, codecStatusSbcAndSbc, false);
+
+        // Inject an event to change state machine to connected state
+        A2dpStackEvent connStCh =
+                new A2dpStackEvent(A2dpStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
+        connStCh.device = mTestDevice;
+        connStCh.valueInt = A2dpStackEvent.CONNECTION_STATE_CONNECTED;
+        mA2dpStateMachine.sendMessage(A2dpStateMachine.STACK_EVENT, connStCh);
+
+        // Verify that the expected number of broadcasts are executed:
+        // - two calls to broadcastConnectionState(): Disconnected -> Conecting -> Connected
+        // - one call to broadcastAudioState() when entering Connected state
+        ArgumentCaptor<Intent> intentArgument2 = ArgumentCaptor.forClass(Intent.class);
+        verify(mA2dpService, timeout(TIMEOUT_MS).times(2)).sendBroadcast(intentArgument2.capture(),
+                anyString());
+
+        // Verify that state machine update optional codec when enter connected state
+        verify(mA2dpService, times(1)).updateOptionalCodecsSupport(mTestDevice);
+
+        // Change codec status when device connected.
+        // Selected codec = SBC, selectable codec = SBC+AAC
+        mA2dpStateMachine.processCodecConfigEvent(codecStatusSbcAndSbcAac);
+        if (!offloadEnabled) {
+            verify(mA2dpService).codecConfigUpdated(mTestDevice, codecStatusSbcAndSbcAac, true);
+        }
+        verify(mA2dpService, times(2)).updateOptionalCodecsSupport(mTestDevice);
+
+        // Update selected codec with selectable codec unchanged.
+        // Selected codec = AAC, selectable codec = SBC+AAC
+        mA2dpStateMachine.processCodecConfigEvent(codecStatusAacAndSbcAac);
+        verify(mA2dpService).codecConfigUpdated(mTestDevice, codecStatusAacAndSbcAac, false);
+        verify(mA2dpService, times(2)).updateOptionalCodecsSupport(mTestDevice);
+    }
 }
diff --git a/tests/unit/src/com/android/bluetooth/btservice/RemoteDevicesTest.java b/tests/unit/src/com/android/bluetooth/btservice/RemoteDevicesTest.java
index 21a1228..651971c 100644
--- a/tests/unit/src/com/android/bluetooth/btservice/RemoteDevicesTest.java
+++ b/tests/unit/src/com/android/bluetooth/btservice/RemoteDevicesTest.java
@@ -440,7 +440,8 @@
         Assert.assertEquals(device, intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE));
         Assert.assertEquals(batteryLevel,
                 intent.getIntExtra(BluetoothDevice.EXTRA_BATTERY_LEVEL, -15));
-        Assert.assertEquals(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT, intent.getFlags());
+        Assert.assertEquals(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
+                        | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND, intent.getFlags());
     }
 
     private static Intent getHeadsetConnectionStateChangedIntent(BluetoothDevice device,
diff --git a/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java b/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
index 348204b..1b6cbc3 100644
--- a/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
+++ b/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
@@ -50,10 +50,12 @@
     private MetadataDatabase mDatabase;
     private DatabaseManager mDatabaseManager;
     private BluetoothDevice mTestDevice;
+    private BluetoothDevice mTestDevice2;
 
     private static final String LOCAL_STORAGE = "LocalStorage";
     private static final String TEST_BT_ADDR = "11:22:33:44:55:66";
-    private static final String OTHER_BT_ADDR = "11:11:11:11:11:11";
+    private static final String OTHER_BT_ADDR1 = "11:11:11:11:11:11";
+    private static final String OTHER_BT_ADDR2 = "22:22:22:22:22:22";
     private static final int A2DP_SUPPORT_OP_CODEC_TEST = 0;
     private static final int A2DP_ENALBED_OP_CODEC_TEST = 1;
     private static final int MAX_META_ID = 16;
@@ -201,15 +203,13 @@
     }
 
     @Test
-    public void testRemoveUnusedMetadata() {
-        // Insert three devices to database and cache, only mTestDevice is
+    public void testRemoveUnusedMetadata_WithSingleBondedDevice() {
+        // Insert two devices to database and cache, only mTestDevice is
         // in the bonded list
-        BluetoothDevice otherDevice = BluetoothAdapter.getDefaultAdapter()
-                .getRemoteDevice(OTHER_BT_ADDR);
-        Metadata otherData = new Metadata(OTHER_BT_ADDR);
+        Metadata otherData = new Metadata(OTHER_BT_ADDR1);
         // Add metadata for otherDevice
         otherData.setCustomizedMeta(0, "value");
-        mDatabaseManager.mMetadataCache.put(OTHER_BT_ADDR, otherData);
+        mDatabaseManager.mMetadataCache.put(OTHER_BT_ADDR1, otherData);
         mDatabase.insert(otherData);
 
         Metadata data = new Metadata(TEST_BT_ADDR);
@@ -221,7 +221,7 @@
         TestUtils.waitForLooperToFinishScheduledTask(mDatabaseManager.getHandlerLooper());
 
         // Check removed device report metadata changed to null
-        verify(mAdapterService).metadataChanged(OTHER_BT_ADDR, 0, null);
+        verify(mAdapterService).metadataChanged(OTHER_BT_ADDR1, 0, null);
 
         List<Metadata> list = mDatabase.load();
 
@@ -239,6 +239,61 @@
     }
 
     @Test
+    public void testRemoveUnusedMetadata_WithMultiBondedDevices() {
+        // Insert three devices to database and cache, otherDevice1 and otherDevice2
+        // are in the bonded list
+
+        // Add metadata for TEST_BT_ADDR
+        Metadata testData = new Metadata(TEST_BT_ADDR);
+        testData.setCustomizedMeta(0, "value");
+        mDatabaseManager.mMetadataCache.put(TEST_BT_ADDR, testData);
+        mDatabase.insert(testData);
+
+        // Add metadata for OTHER_BT_ADDR1
+        Metadata otherData1 = new Metadata(OTHER_BT_ADDR1);
+        otherData1.setCustomizedMeta(0, "value");
+        mDatabaseManager.mMetadataCache.put(OTHER_BT_ADDR1, otherData1);
+        mDatabase.insert(otherData1);
+
+        // Add metadata for OTHER_BT_ADDR2
+        Metadata otherData2 = new Metadata(OTHER_BT_ADDR2);
+        otherData2.setCustomizedMeta(0, "value");
+        mDatabaseManager.mMetadataCache.put(OTHER_BT_ADDR2, otherData2);
+        mDatabase.insert(otherData2);
+
+        // Add OTHER_BT_ADDR1 OTHER_BT_ADDR2 to bonded devices
+        BluetoothDevice otherDevice1 = BluetoothAdapter.getDefaultAdapter()
+                .getRemoteDevice(OTHER_BT_ADDR1);
+        BluetoothDevice otherDevice2 = BluetoothAdapter.getDefaultAdapter()
+                .getRemoteDevice(OTHER_BT_ADDR2);
+        BluetoothDevice[] bondedDevices = {otherDevice1, otherDevice2};
+        doReturn(bondedDevices).when(mAdapterService).getBondedDevices();
+
+        mDatabaseManager.removeUnusedMetadata();
+        TestUtils.waitForLooperToFinishScheduledTask(mDatabaseManager.getHandlerLooper());
+
+        // Check TEST_BT_ADDR report metadata changed to null
+        verify(mAdapterService).metadataChanged(TEST_BT_ADDR, 0, null);
+
+        // Check number of metadata in the database
+        List<Metadata> list = mDatabase.load();
+        // OTHER_BT_ADDR1 and OTHER_BT_ADDR2 should still in database
+        Assert.assertEquals(2, list.size());
+
+        // Check whether the devices are in the database
+        Metadata checkData1 = list.get(0);
+        Assert.assertEquals(OTHER_BT_ADDR1, checkData1.getAddress());
+        Metadata checkData2 = list.get(1);
+        Assert.assertEquals(OTHER_BT_ADDR2, checkData2.getAddress());
+
+        mDatabase.deleteAll();
+        // Wait for clear database
+        TestUtils.waitForLooperToFinishScheduledTask(mDatabaseManager.getHandlerLooper());
+        mDatabaseManager.mMetadataCache.clear();
+
+    }
+
+    @Test
     public void testSetGetCustomMeta() {
         int badKey = 100;
         String value = "input value";