Merge changes If2b3b414,I1039748d,Ib3cd38f5

* changes:
  AudioService: fix disconnect/connect of A2DP device
  audioservice: re send BT_SCO parameter to HAL when audioserver restarts
  audioservice: fix hasMediaDynamicPolicy() for loopback and render policies
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 4d09c3b..6010b1dc 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -15,9 +15,6 @@
  */
 package com.android.server.audio;
 
-import static com.android.server.audio.AudioService.CONNECTION_STATE_CONNECTED;
-import static com.android.server.audio.AudioService.CONNECTION_STATE_DISCONNECTED;
-
 import android.annotation.NonNull;
 import android.bluetooth.BluetoothA2dp;
 import android.bluetooth.BluetoothDevice;
@@ -95,13 +92,28 @@
     /*package*/ AudioDeviceBroker(@NonNull Context context, @NonNull AudioService service) {
         mContext = context;
         mAudioService = service;
-        setupMessaging(context);
         mBtHelper = new BtHelper(this);
         mDeviceInventory = new AudioDeviceInventory(this);
 
+        init();
+    }
+
+    /** for test purposes only, inject AudioDeviceInventory */
+    AudioDeviceBroker(@NonNull Context context, @NonNull AudioService service,
+                      @NonNull AudioDeviceInventory mockDeviceInventory) {
+        mContext = context;
+        mAudioService = service;
+        mBtHelper = new BtHelper(this);
+        mDeviceInventory = mockDeviceInventory;
+
+        init();
+    }
+
+    private void init() {
+        setupMessaging(mContext);
+
         mForcedUseForComm = AudioSystem.FORCE_NONE;
         mForcedUseForCommExt = mForcedUseForComm;
-
     }
 
     /*package*/ Context getContext() {
@@ -125,6 +137,8 @@
     /*package*/ void onAudioServerDied() {
         // Restore forced usage for communications and record
         synchronized (mDeviceStateLock) {
+            AudioSystem.setParameters(
+                    "BT_SCO=" + (mForcedUseForComm == AudioSystem.FORCE_BT_SCO ? "on" : "off"));
             onSetForceUse(AudioSystem.FOR_COMMUNICATION, mForcedUseForComm, "onAudioServerDied");
             onSetForceUse(AudioSystem.FOR_RECORD, mForcedUseForComm, "onAudioServerDied");
         }
@@ -230,17 +244,42 @@
             mSupprNoisy = suppressNoisyIntent;
             mVolume = vol;
         }
+
+        // redefine equality op so we can match messages intended for this device
+        @Override
+        public boolean equals(Object o) {
+            return mDevice.equals(o);
+        }
     }
 
+
     /*package*/ void postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(
             @NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state,
             int profile, boolean suppressNoisyIntent, int a2dpVolume) {
         final BtDeviceConnectionInfo info = new BtDeviceConnectionInfo(device, state, profile,
                 suppressNoisyIntent, a2dpVolume);
 
-        // TODO add a check to try to remove unprocessed messages for the same device (the old
-        //      check didn't work), and  make sure it doesn't conflict with config change message
-        sendLMsgNoDelay(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT, SENDMSG_QUEUE, info);
+        // when receiving a request to change the connection state of a device, this last request
+        // is the source of truth, so cancel all previous requests
+        removeAllA2dpConnectionEvents(device);
+
+        sendLMsgNoDelay(
+                state == BluetoothProfile.STATE_CONNECTED
+                        ? MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION
+                        : MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION,
+                SENDMSG_QUEUE, info);
+    }
+
+    /** remove all previously scheduled connection and disconnection events for the given device */
+    private void removeAllA2dpConnectionEvents(@NonNull BluetoothDevice device) {
+        mBrokerHandler.removeMessages(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION,
+                device);
+        mBrokerHandler.removeMessages(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION,
+                device);
+        mBrokerHandler.removeMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED,
+                device);
+        mBrokerHandler.removeMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED,
+                device);
     }
 
     private static final class HearingAidDeviceConnectionInfo {
@@ -428,13 +467,16 @@
         sendMsgNoDelay(MSG_BROADCAST_AUDIO_BECOMING_NOISY, SENDMSG_REPLACE);
     }
 
-    /*package*/ void postA2dpSinkConnection(int state,
+    /*package*/ void postA2dpSinkConnection(@AudioService.BtProfileConnectionState int state,
             @NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo, int delay) {
-        sendILMsg(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE, SENDMSG_QUEUE,
+        sendILMsg(state == BluetoothA2dp.STATE_CONNECTED
+                        ? MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED
+                        : MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED,
+                SENDMSG_QUEUE,
                 state, btDeviceInfo, delay);
     }
 
-    /*package*/ void postA2dpSourceConnection(int state,
+    /*package*/ void postA2dpSourceConnection(@AudioService.BtProfileConnectionState int state,
             @NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo, int delay) {
         sendILMsg(MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE, SENDMSG_QUEUE,
                 state, btDeviceInfo, delay);
@@ -520,25 +562,6 @@
         }
     }
 
-    @GuardedBy("mDeviceStateLock")
-    /*package*/ void handleSetA2dpSinkConnectionState(@BluetoothProfile.BtProfileState int state,
-                @NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo) {
-        final int intState = (state == BluetoothA2dp.STATE_CONNECTED)
-                ? CONNECTION_STATE_CONNECTED : CONNECTION_STATE_DISCONNECTED;
-        final int delay = mDeviceInventory.checkSendBecomingNoisyIntent(
-                    AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, intState,
-                    AudioSystem.DEVICE_NONE);
-        final String addr = btDeviceInfo == null ? "null" : btDeviceInfo.getBtDevice().getAddress();
-
-        if (AudioService.DEBUG_DEVICES) {
-            Log.d(TAG, "handleSetA2dpSinkConnectionState btDevice= " + btDeviceInfo
-                    + " state= " + state
-                    + " is dock: " + btDeviceInfo.getBtDevice().isBluetoothDock());
-        }
-        sendILMsg(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE, SENDMSG_QUEUE,
-                state, btDeviceInfo, delay);
-    }
-
     /*package*/ void postSetA2dpSourceConnectionState(@BluetoothProfile.BtProfileState int state,
             @NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo) {
         final int intState = (state == BluetoothA2dp.STATE_CONNECTED) ? 1 : 0;
@@ -573,8 +596,10 @@
 
     // must be called synchronized on mConnectedDevices
     /*package*/ boolean hasScheduledA2dpSinkConnectionState(BluetoothDevice btDevice) {
-        return mBrokerHandler.hasMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE,
-                new BtHelper.BluetoothA2dpDeviceInfo(btDevice));
+        return (mBrokerHandler.hasMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED,
+                        new BtHelper.BluetoothA2dpDeviceInfo(btDevice))
+                || mBrokerHandler.hasMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED,
+                        new BtHelper.BluetoothA2dpDeviceInfo(btDevice)));
     }
 
     /*package*/ void setA2dpDockTimeout(String address, int a2dpCodec, int delayMs) {
@@ -709,7 +734,8 @@
                         mDeviceInventory.onReportNewRoutes();
                     }
                     break;
-                case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE:
+                case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED:
+                case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED:
                     synchronized (mDeviceStateLock) {
                         mDeviceInventory.onSetA2dpSinkConnectionState(
                                 (BtHelper.BluetoothA2dpDeviceInfo) msg.obj, msg.arg1);
@@ -834,7 +860,8 @@
                         }
                     }
                     break;
-                case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT: {
+                case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION:
+                case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION: {
                     final BtDeviceConnectionInfo info = (BtDeviceConnectionInfo) msg.obj;
                     AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent(
                             "setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent "
@@ -885,7 +912,7 @@
     private static final int MSG_I_BROADCAST_BT_CONNECTION_STATE = 3;
     private static final int MSG_IIL_SET_FORCE_USE = 4;
     private static final int MSG_IIL_SET_FORCE_BT_A2DP_USE = 5;
-    private static final int MSG_IL_SET_A2DP_SINK_CONNECTION_STATE = 6;
+    private static final int MSG_TOGGLE_HDMI = 6;
     private static final int MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE = 7;
     private static final int MSG_IL_SET_HEARING_AID_CONNECTION_STATE = 8;
     private static final int MSG_BT_HEADSET_CNCT_FAILED = 9;
@@ -896,7 +923,6 @@
     private static final int MSG_II_SET_HEARING_AID_VOLUME = 14;
     private static final int MSG_I_SET_AVRCP_ABSOLUTE_VOLUME = 15;
     private static final int MSG_I_DISCONNECT_BT_SCO = 16;
-    private static final int MSG_TOGGLE_HDMI = 17;
     private static final int MSG_L_A2DP_ACTIVE_DEVICE_CHANGE = 18;
     private static final int MSG_DISCONNECT_A2DP = 19;
     private static final int MSG_DISCONNECT_A2DP_SINK = 20;
@@ -906,25 +932,30 @@
     private static final int MSG_L_BT_SERVICE_CONNECTED_PROFILE_A2DP_SINK = 24;
     private static final int MSG_L_BT_SERVICE_CONNECTED_PROFILE_HEARING_AID = 25;
     private static final int MSG_L_BT_SERVICE_CONNECTED_PROFILE_HEADSET = 26;
+    private static final int MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED = 27;
+    private static final int MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED = 28;
     // process external command to (dis)connect an A2DP device
-    private static final int MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT = 27;
+    private static final int MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION = 29;
+    private static final int MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION = 30;
     // process external command to (dis)connect a hearing aid device
-    private static final int MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT = 28;
+    private static final int MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT = 31;
     // a ScoClient died in BtHelper
-    private static final int MSG_L_SCOCLIENT_DIED = 29;
+    private static final int MSG_L_SCOCLIENT_DIED = 32;
 
 
     private static boolean isMessageHandledUnderWakelock(int msgId) {
         switch(msgId) {
             case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE:
-            case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE:
+            case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED:
+            case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED:
             case MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE:
             case MSG_IL_SET_HEARING_AID_CONNECTION_STATE:
             case MSG_IL_BTA2DP_DOCK_TIMEOUT:
             case MSG_L_A2DP_DEVICE_CONFIG_CHANGE:
             case MSG_TOGGLE_HDMI:
             case MSG_L_A2DP_ACTIVE_DEVICE_CHANGE:
-            case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT:
+            case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION:
+            case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION:
             case MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT:
                 return true;
             default:
@@ -1005,7 +1036,8 @@
 
             switch (msg) {
                 case MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE:
-                case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE:
+                case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED:
+                case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED:
                 case MSG_IL_SET_HEARING_AID_CONNECTION_STATE:
                 case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE:
                 case MSG_IL_BTA2DP_DOCK_TIMEOUT:
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index a9a8ef2..90973a8 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -41,14 +41,16 @@
 import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.ArrayList;
 
 /**
  * Class to manage the inventory of all connected devices.
  * This class is thread-safe.
+ * (non final for mocking/spying)
  */
-public final class AudioDeviceInventory {
+public class AudioDeviceInventory {
 
     private static final String TAG = "AS.AudioDeviceInventory";
 
@@ -56,11 +58,7 @@
     // Key for map created from DeviceInfo.makeDeviceListKey()
     private final ArrayMap<String, DeviceInfo> mConnectedDevices = new ArrayMap<>();
 
-    private final @NonNull AudioDeviceBroker mDeviceBroker;
-
-    AudioDeviceInventory(@NonNull AudioDeviceBroker broker) {
-        mDeviceBroker = broker;
-    }
+    private @NonNull AudioDeviceBroker mDeviceBroker;
 
     // cache of the address of the last dock the device was connected to
     private String mDockAddress;
@@ -70,6 +68,20 @@
     final RemoteCallbackList<IAudioRoutesObserver> mRoutesObservers =
             new RemoteCallbackList<IAudioRoutesObserver>();
 
+    /*package*/ AudioDeviceInventory(@NonNull AudioDeviceBroker broker) {
+        mDeviceBroker = broker;
+    }
+
+    //-----------------------------------------------------------
+    /** for mocking only */
+    /*package*/ AudioDeviceInventory() {
+        mDeviceBroker = null;
+    }
+
+    /*package*/ void setDeviceBroker(@NonNull AudioDeviceBroker broker) {
+        mDeviceBroker = broker;
+    }
+
     //------------------------------------------------------------
     /**
      * Class to store info about connected devices.
@@ -146,8 +158,10 @@
         }
     }
 
+    // only public for mocking/spying
     @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
-    /*package*/ void onSetA2dpSinkConnectionState(@NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo,
+    @VisibleForTesting
+    public void onSetA2dpSinkConnectionState(@NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo,
             @AudioService.BtProfileConnectionState int state) {
         final BluetoothDevice btDevice = btInfo.getBtDevice();
         int a2dpVolume = btInfo.getVolume();
@@ -159,30 +173,40 @@
         if (!BluetoothAdapter.checkBluetoothAddress(address)) {
             address = "";
         }
-        AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
-                "A2DP sink connected: device addr=" + address + " state=" + state
-                        + " vol=" + a2dpVolume));
 
         final int a2dpCodec = btInfo.getCodec();
 
+        AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
+                "A2DP sink connected: device addr=" + address + " state=" + state
+                        + " codec=" + a2dpCodec
+                        + " vol=" + a2dpVolume));
+
         synchronized (mConnectedDevices) {
             final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
                     btDevice.getAddress());
             final DeviceInfo di = mConnectedDevices.get(key);
             boolean isConnected = di != null;
 
-            if (isConnected && state != BluetoothProfile.STATE_CONNECTED) {
-                if (btDevice.isBluetoothDock()) {
-                    if (state == BluetoothProfile.STATE_DISCONNECTED) {
-                        // introduction of a delay for transient disconnections of docks when
-                        // power is rapidly turned off/on, this message will be canceled if
-                        // we reconnect the dock under a preset delay
-                        makeA2dpDeviceUnavailableLater(address,
-                                AudioDeviceBroker.BTA2DP_DOCK_TIMEOUT_MS);
-                        // the next time isConnected is evaluated, it will be false for the dock
+            if (isConnected) {
+                if (state == BluetoothProfile.STATE_CONNECTED) {
+                    // device is already connected, but we are receiving a connection again,
+                    // it could be for a codec change
+                    if (a2dpCodec != di.mDeviceCodecFormat) {
+                        mDeviceBroker.postBluetoothA2dpDeviceConfigChange(btDevice);
                     }
                 } else {
-                    makeA2dpDeviceUnavailableNow(address, di.mDeviceCodecFormat);
+                    if (btDevice.isBluetoothDock()) {
+                        if (state == BluetoothProfile.STATE_DISCONNECTED) {
+                            // introduction of a delay for transient disconnections of docks when
+                            // power is rapidly turned off/on, this message will be canceled if
+                            // we reconnect the dock under a preset delay
+                            makeA2dpDeviceUnavailableLater(address,
+                                    AudioDeviceBroker.BTA2DP_DOCK_TIMEOUT_MS);
+                            // the next time isConnected is evaluated, it will be false for the dock
+                        }
+                    } else {
+                        makeA2dpDeviceUnavailableNow(address, di.mDeviceCodecFormat);
+                    }
                 }
             } else if (!isConnected && state == BluetoothProfile.STATE_CONNECTED) {
                 if (btDevice.isBluetoothDock()) {
@@ -282,11 +306,9 @@
                     + " event=" + BtHelper.a2dpDeviceEventToString(event)));
 
         synchronized (mConnectedDevices) {
-            //TODO original CL is not consistent between BluetoothDevice and BluetoothA2dpDeviceInfo
-            // for this type of message
             if (mDeviceBroker.hasScheduledA2dpSinkConnectionState(btDevice)) {
                 AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
-                        "A2dp config change ignored"));
+                        "A2dp config change ignored (scheduled connection change)"));
                 return;
             }
             final String key = DeviceInfo.makeDeviceListKey(
@@ -534,8 +556,10 @@
         return mCurAudioRoutes;
     }
 
+    // only public for mocking/spying
     @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
-    /*package*/ void setBluetoothA2dpDeviceConnectionState(
+    @VisibleForTesting
+    public void setBluetoothA2dpDeviceConnectionState(
             @NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state,
             int profile, boolean suppressNoisyIntent, int musicDevice, int a2dpVolume) {
         int delay;
@@ -544,9 +568,12 @@
         }
         synchronized (mConnectedDevices) {
             if (profile == BluetoothProfile.A2DP && !suppressNoisyIntent) {
-                int intState = (state == BluetoothA2dp.STATE_CONNECTED) ? 1 : 0;
+                @AudioService.ConnectionState int asState =
+                        (state == BluetoothA2dp.STATE_CONNECTED)
+                                ? AudioService.CONNECTION_STATE_CONNECTED
+                                : AudioService.CONNECTION_STATE_DISCONNECTED;
                 delay = checkSendBecomingNoisyIntentInt(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
-                        intState, musicDevice);
+                        asState, musicDevice);
             } else {
                 delay = 0;
             }
@@ -785,7 +812,7 @@
                 return 0;
             }
             mDeviceBroker.postBroadcastBecomingNoisy();
-            delay = 1000;
+            delay = AudioService.BECOMING_NOISY_DELAY_MS;
         }
 
         return delay;
@@ -943,4 +970,21 @@
             intent.putExtra(AudioManager.EXTRA_MAX_CHANNEL_COUNT, maxChannels);
         }
     }
+
+    //----------------------------------------------------------
+    // For tests only
+
+    /**
+     * Check if device is in the list of connected devices
+     * @param device
+     * @return true if connected
+     */
+    @VisibleForTesting
+    public boolean isA2dpDeviceConnected(@NonNull BluetoothDevice device) {
+        final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
+                device.getAddress());
+        synchronized (mConnectedDevices) {
+            return (mConnectedDevices.get(key) != null);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 6c57be8..5bc2261 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -128,6 +128,7 @@
 import android.widget.Toast;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.Preconditions;
 import com.android.server.EventLogTags;
@@ -190,6 +191,13 @@
     private static final int UNMUTE_STREAM_DELAY = 350;
 
     /**
+     * Delay before disconnecting a device that would cause BECOMING_NOISY intent to be sent,
+     * to give a chance to applications to pause.
+     */
+    @VisibleForTesting
+    public static final int BECOMING_NOISY_DELAY_MS = 1000;
+
+    /**
      * Only used in the result from {@link #checkForRingerModeChange(int, int, int)}
      */
     private static final int FLAG_ADJUST_VOLUME = 1;
@@ -3950,7 +3958,9 @@
                 || adjust == AudioManager.ADJUST_TOGGLE_MUTE;
     }
 
-    /*package*/ boolean isInCommunication() {
+    /** only public for mocking/spying, do not call outside of AudioService */
+    @VisibleForTesting
+    public boolean isInCommunication() {
         boolean IsInCall = false;
 
         TelecomManager telecomManager =
@@ -4119,7 +4129,9 @@
         return false;
     }
 
-    /*package*/ int getDeviceForStream(int stream) {
+    /** only public for mocking/spying, do not call outside of AudioService */
+    @VisibleForTesting
+    public int getDeviceForStream(int stream) {
         int device = getDevicesForStream(stream);
         if ((device & (device - 1)) != 0) {
             // Multiple device selection is either:
@@ -4164,7 +4176,9 @@
         }
     }
 
-    /*package*/ void postObserveDevicesForAllStreams() {
+    /** only public for mocking/spying, do not call outside of AudioService */
+    @VisibleForTesting
+    public void postObserveDevicesForAllStreams() {
         sendMsg(mAudioHandler,
                 MSG_OBSERVE_DEVICES_FOR_ALL_STREAMS,
                 SENDMSG_QUEUE, 0 /*arg1*/, 0 /*arg2*/, null /*obj*/,
@@ -4275,7 +4289,9 @@
             AudioSystem.DEVICE_OUT_ALL_USB |
             AudioSystem.DEVICE_OUT_HDMI;
 
-    /*package*/ void postAccessoryPlugMediaUnmute(int newDevice) {
+    /** only public for mocking/spying, do not call outside of AudioService */
+    @VisibleForTesting
+    public void postAccessoryPlugMediaUnmute(int newDevice) {
         sendMsg(mAudioHandler, MSG_ACCESSORY_PLUG_MEDIA_UNMUTE, SENDMSG_QUEUE,
                 newDevice, 0, null, 0);
     }
@@ -4825,7 +4841,9 @@
         }
     }
 
-    /*package*/ void postSetVolumeIndexOnDevice(int streamType, int vssVolIndex, int device,
+    /** only public for mocking/spying, do not call outside of AudioService */
+    @VisibleForTesting
+    public void postSetVolumeIndexOnDevice(int streamType, int vssVolIndex, int device,
                                                 String caller) {
         sendMsg(mAudioHandler,
                 MSG_SET_DEVICE_STREAM_VOLUME,
@@ -5181,15 +5199,19 @@
 
     /**
      * @return true if there is currently a registered dynamic mixing policy that affects media
+     * and is not a render + loopback policy
      */
-    /*package*/ boolean hasMediaDynamicPolicy() {
+    // only public for mocking/spying
+    @VisibleForTesting
+    public boolean hasMediaDynamicPolicy() {
         synchronized (mAudioPolicies) {
             if (mAudioPolicies.isEmpty()) {
                 return false;
             }
             final Collection<AudioPolicyProxy> appColl = mAudioPolicies.values();
             for (AudioPolicyProxy app : appColl) {
-                if (app.hasMixAffectingUsage(AudioAttributes.USAGE_MEDIA)) {
+                if (app.hasMixAffectingUsage(AudioAttributes.USAGE_MEDIA,
+                        AudioMix.ROUTE_FLAG_LOOP_BACK_RENDER)) {
                     return true;
                 }
             }
@@ -5514,7 +5536,9 @@
         return mMediaFocusControl.getFocusRampTimeMs(focusGain, attr);
     }
 
-    /*package*/ boolean hasAudioFocusUsers() {
+    /** only public for mocking/spying, do not call outside of AudioService */
+    @VisibleForTesting
+    public boolean hasAudioFocusUsers() {
         return mMediaFocusControl.hasAudioFocusUsers();
     }
 
@@ -6961,9 +6985,10 @@
             Binder.restoreCallingIdentity(identity);
         }
 
-        boolean hasMixAffectingUsage(int usage) {
+        boolean hasMixAffectingUsage(int usage, int excludedFlags) {
             for (AudioMix mix : mMixes) {
-                if (mix.isAffectingUsage(usage)) {
+                if (mix.isAffectingUsage(usage)
+                        && ((mix.getRouteFlags() & excludedFlags) != excludedFlags)) {
                     return true;
                 }
             }
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index 1a63f8f..9f1a6bd 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -139,6 +139,12 @@
         public int getCodec() {
             return mCodec;
         }
+
+        // redefine equality op so we can match messages intended for this device
+        @Override
+        public boolean equals(Object o) {
+            return mBtDevice.equals(o);
+        }
     }
 
     // A2DP device events
@@ -441,9 +447,9 @@
             return;
         }
         final BluetoothDevice btDevice = deviceList.get(0);
-        final @BluetoothProfile.BtProfileState int state = mA2dp.getConnectionState(btDevice);
-        mDeviceBroker.handleSetA2dpSinkConnectionState(
-                state, new BluetoothA2dpDeviceInfo(btDevice));
+        // the device is guaranteed CONNECTED
+        mDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(btDevice,
+                BluetoothA2dp.STATE_CONNECTED, BluetoothProfile.A2DP_SINK, true, -1);
     }
 
     /*package*/ synchronized void onA2dpSinkProfileConnected(BluetoothProfile profile) {
diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml
index 25bd4ec..c1bbb30 100644
--- a/services/tests/servicestests/AndroidManifest.xml
+++ b/services/tests/servicestests/AndroidManifest.xml
@@ -69,7 +69,7 @@
     <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
     <uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG" />
     <uses-permission android:name="android.permission.HARDWARE_TEST"/>
-    <uses-permission android:name="android.permission.MANAGE_APPOPS"/>
+    <uses-permission android:name="android.permission.BLUETOOTH"/>
 
     <!-- Uses API introduced in O (26) -->
     <uses-sdk android:minSdkVersion="1"
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
new file mode 100644
index 0000000..5c2ad94
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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.server.audio;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.AudioSystem;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.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.ArgumentMatchers;
+import org.mockito.Mock;
+import org.mockito.Spy;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class AudioDeviceBrokerTest {
+
+    private static final String TAG = "AudioDeviceBrokerTest";
+    private static final int MAX_MESSAGE_HANDLING_DELAY_MS = 100;
+
+    private Context mContext;
+    // the actual class under test
+    private AudioDeviceBroker mAudioDeviceBroker;
+
+    @Mock private AudioService mMockAudioService;
+    @Spy private AudioDeviceInventory mSpyDevInventory;
+
+    private BluetoothDevice mFakeBtDevice;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getTargetContext();
+
+        mMockAudioService = mock(AudioService.class);
+        mSpyDevInventory = spy(new AudioDeviceInventory());
+        mAudioDeviceBroker = new AudioDeviceBroker(mContext, mMockAudioService, mSpyDevInventory);
+        mSpyDevInventory.setDeviceBroker(mAudioDeviceBroker);
+
+        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+        mFakeBtDevice = adapter.getRemoteDevice("00:01:02:03:04:05");
+        Assert.assertNotNull("invalid null BT device", mFakeBtDevice);
+    }
+
+    @After
+    public void tearDown() throws Exception { }
+
+    @Test
+    public void testSetUpAndTearDown() { }
+
+    /**
+     * Verify call to postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent() for connection
+     * calls into AudioDeviceInventory with the right params
+     * @throws Exception
+     */
+    @Test
+    public void testPostA2dpDeviceConnectionChange() throws Exception {
+        Log.i(TAG, "testPostA2dpDeviceConnectionChange");
+        Assert.assertNotNull("invalid null BT device", mFakeBtDevice);
+
+        mAudioDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(mFakeBtDevice,
+                BluetoothProfile.STATE_CONNECTED, BluetoothProfile.A2DP, true, 1);
+        Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS);
+        verify(mSpyDevInventory, times(1)).setBluetoothA2dpDeviceConnectionState(
+                any(BluetoothDevice.class),
+                ArgumentMatchers.eq(BluetoothProfile.STATE_CONNECTED) /*state*/,
+                ArgumentMatchers.eq(BluetoothProfile.A2DP) /*profile*/,
+                ArgumentMatchers.eq(true) /*suppressNoisyIntent*/, anyInt() /*musicDevice*/,
+                ArgumentMatchers.eq(1) /*a2dpVolume*/
+        );
+    }
+
+    /**
+     * Verify call to postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent() for
+     *    connection > pause > disconnection > connection
+     * keeps the device connected
+     * @throws Exception
+     */
+    @Test
+    public void testA2dpDeviceConnectionDisconnectionConnectionChange() throws Exception {
+        Log.i(TAG, "testA2dpDeviceConnectionDisconnectionConnectionChange");
+
+        doTestConnectionDisconnectionReconnection(0);
+    }
+
+    /**
+     * Verify device disconnection and reconnection within the BECOMING_NOISY window
+     * @throws Exception
+     */
+    @Test
+    public void testA2dpDeviceReconnectionWithinBecomingNoisyDelay() throws Exception {
+        Log.i(TAG, "testA2dpDeviceReconnectionWithinBecomingNoisyDelay");
+
+        doTestConnectionDisconnectionReconnection(AudioService.BECOMING_NOISY_DELAY_MS / 2);
+    }
+
+    private void doTestConnectionDisconnectionReconnection(int delayAfterDisconnection)
+            throws Exception {
+        when(mMockAudioService.getDeviceForStream(AudioManager.STREAM_MUSIC))
+                .thenReturn(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP);
+        when(mMockAudioService.isInCommunication()).thenReturn(false);
+        when(mMockAudioService.hasMediaDynamicPolicy()).thenReturn(false);
+        when(mMockAudioService.hasAudioFocusUsers()).thenReturn(false);
+
+        // first connection
+        mAudioDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(mFakeBtDevice,
+                BluetoothProfile.STATE_CONNECTED, BluetoothProfile.A2DP, true, 1);
+        Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS);
+
+        // disconnection
+        mAudioDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(mFakeBtDevice,
+                BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.A2DP, false, -1);
+        if (delayAfterDisconnection > 0) {
+            Thread.sleep(delayAfterDisconnection);
+        }
+
+        // reconnection
+        mAudioDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(mFakeBtDevice,
+                BluetoothProfile.STATE_CONNECTED, BluetoothProfile.A2DP, true, 2);
+        Thread.sleep(AudioService.BECOMING_NOISY_DELAY_MS + MAX_MESSAGE_HANDLING_DELAY_MS);
+
+        // Verify disconnection has been cancelled and we're seeing two connections attempts,
+        // with the device connected at the end of the test
+        verify(mSpyDevInventory, times(2)).onSetA2dpSinkConnectionState(
+                any(BtHelper.BluetoothA2dpDeviceInfo.class),
+                ArgumentMatchers.eq(BluetoothProfile.STATE_CONNECTED));
+        Assert.assertTrue("Mock device not connected",
+                mSpyDevInventory.isA2dpDeviceConnected(mFakeBtDevice));
+    }
+}